Skip to main content

shared/models/
egg_configuration.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9    collections::BTreeMap,
10    sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14pub fn validate_config_allocations(
15    config_allocations: &EggConfigAllocations,
16    _context: &(),
17) -> Result<(), garde::Error> {
18    if !config_allocations.user_self_assign.is_valid() {
19        return Err(garde::Error::new(
20            "port ranges must be 1024-65535 and start_port < end_port",
21        ));
22    }
23
24    Ok(())
25}
26
27#[derive(ToSchema, Serialize, Deserialize, Clone, Copy)]
28pub struct EggConfigAllocationsUserSelfAssign {
29    pub enabled: bool,
30    pub require_primary_allocation: bool,
31
32    pub start_port: u16,
33    pub end_port: u16,
34}
35
36impl Default for EggConfigAllocationsUserSelfAssign {
37    fn default() -> Self {
38        Self {
39            enabled: false,
40            require_primary_allocation: true,
41            start_port: 49152,
42            end_port: 65535,
43        }
44    }
45}
46
47impl EggConfigAllocationsUserSelfAssign {
48    #[inline]
49    pub fn is_valid(&self) -> bool {
50        self.start_port < self.end_port && self.start_port >= 1024
51    }
52}
53
54#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
55#[serde(tag = "type", rename_all = "snake_case")]
56pub enum EggConfigAllocationDeploymentAdditionalAllocationMode {
57    Random,
58    Range {
59        #[garde(range(min = 1024, max = 65535))]
60        start_port: u16,
61        #[garde(range(min = 1024, max = 65535))]
62        end_port: u16,
63    },
64    AddPrimary {
65        #[garde(skip)]
66        value: u16,
67    },
68    SubtractPrimary {
69        #[garde(skip)]
70        value: u16,
71    },
72    MultiplyPrimary {
73        #[garde(skip)]
74        value: f64,
75    },
76    DividePrimary {
77        #[garde(skip)]
78        value: f64,
79    },
80}
81
82#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
83pub struct EggConfigAllocationDeploymentAdditionalAllocation {
84    #[schema(inline)]
85    #[garde(dive)]
86    pub mode: EggConfigAllocationDeploymentAdditionalAllocationMode,
87    #[garde(length(chars, min = 1, max = 255))]
88    pub assign_to_variable: Option<compact_str::CompactString>,
89}
90
91#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
92pub struct EggConfigAllocationDeploymentPrimaryAllocation {
93    #[garde(range(min = 1024, max = 65535))]
94    pub start_port: u16,
95    #[garde(range(min = 1024, max = 65535))]
96    pub end_port: u16,
97
98    #[garde(length(chars, min = 1, max = 255))]
99    pub assign_to_variable: Option<compact_str::CompactString>,
100}
101
102#[derive(ToSchema, Validate, Serialize, Deserialize, Default, Clone)]
103pub struct EggConfigAllocationsDeployment {
104    #[garde(skip)]
105    pub dedicated: bool,
106
107    #[schema(inline)]
108    #[garde(dive)]
109    pub primary: Option<EggConfigAllocationDeploymentPrimaryAllocation>,
110    #[schema(inline)]
111    #[garde(dive)]
112    pub additional: Vec<EggConfigAllocationDeploymentAdditionalAllocation>,
113}
114
115#[derive(ToSchema, Serialize, Deserialize, Default, Clone)]
116pub struct EggConfigAllocations {
117    #[serde(default)]
118    pub user_self_assign: EggConfigAllocationsUserSelfAssign,
119    #[serde(default)]
120    pub deployment: EggConfigAllocationsDeployment,
121}
122
123#[derive(ToSchema, Validate, Serialize, Deserialize, Default, Clone)]
124pub struct EggConfigStartup {
125    #[garde(skip)]
126    pub allow_custom_startup_command: bool,
127}
128
129#[derive(ToSchema, Validate, Serialize, Deserialize, Default, Clone)]
130pub struct EggConfigRoutes {
131    #[garde(length(max = 100))]
132    #[schema(max_length = 100)]
133    pub order: Vec<crate::settings::RouteOrderItem>,
134}
135
136#[derive(Serialize, Deserialize, Clone)]
137pub struct EggConfiguration {
138    pub uuid: uuid::Uuid,
139
140    pub name: compact_str::CompactString,
141    pub description: Option<compact_str::CompactString>,
142    pub order: i16,
143
144    pub eggs: Vec<uuid::Uuid>,
145
146    pub config_allocations: Option<EggConfigAllocations>,
147    pub config_startup: Option<EggConfigStartup>,
148    pub config_routes: Option<EggConfigRoutes>,
149
150    pub created: chrono::NaiveDateTime,
151
152    extension_data: super::ModelExtensionData,
153}
154
155impl BaseModel for EggConfiguration {
156    const NAME: &'static str = "egg_configuration";
157
158    fn get_extension_list() -> &'static super::ModelExtensionList {
159        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
160            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
161
162        &EXTENSIONS
163    }
164
165    fn get_extension_data(&self) -> &super::ModelExtensionData {
166        &self.extension_data
167    }
168
169    #[inline]
170    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
171        let prefix = prefix.unwrap_or_default();
172
173        BTreeMap::from([
174            (
175                "egg_configurations.uuid",
176                compact_str::format_compact!("{prefix}uuid"),
177            ),
178            (
179                "egg_configurations.name",
180                compact_str::format_compact!("{prefix}name"),
181            ),
182            (
183                "egg_configurations.description",
184                compact_str::format_compact!("{prefix}description"),
185            ),
186            (
187                "egg_configurations.order_",
188                compact_str::format_compact!("{prefix}order_"),
189            ),
190            (
191                "egg_configurations.eggs",
192                compact_str::format_compact!("{prefix}eggs"),
193            ),
194            (
195                "egg_configurations.config_allocations",
196                compact_str::format_compact!("{prefix}config_allocations"),
197            ),
198            (
199                "egg_configurations.config_startup",
200                compact_str::format_compact!("{prefix}config_startup"),
201            ),
202            (
203                "egg_configurations.config_routes",
204                compact_str::format_compact!("{prefix}config_routes"),
205            ),
206            (
207                "egg_configurations.created",
208                compact_str::format_compact!("{prefix}created"),
209            ),
210        ])
211    }
212
213    #[inline]
214    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
215        let prefix = prefix.unwrap_or_default();
216
217        Ok(Self {
218            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
219            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
220            description: row
221                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
222            order: row.try_get(compact_str::format_compact!("{prefix}order_").as_str())?,
223            eggs: row.try_get(compact_str::format_compact!("{prefix}eggs").as_str())?,
224            config_allocations: row
225                .try_get::<Option<serde_json::Value>, _>(
226                    compact_str::format_compact!("{prefix}config_allocations").as_str(),
227                )?
228                .and_then(|v| serde_json::from_value(v).ok()),
229            config_startup: row
230                .try_get::<Option<serde_json::Value>, _>(
231                    compact_str::format_compact!("{prefix}config_startup").as_str(),
232                )?
233                .and_then(|v| serde_json::from_value(v).ok()),
234            config_routes: row
235                .try_get::<Option<serde_json::Value>, _>(
236                    compact_str::format_compact!("{prefix}config_routes").as_str(),
237                )?
238                .and_then(|v| serde_json::from_value(v).ok()),
239            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
240            extension_data: Self::map_extensions(prefix, row)?,
241        })
242    }
243}
244
245impl EggConfiguration {
246    pub async fn all_with_pagination(
247        database: &crate::database::Database,
248        page: i64,
249        per_page: i64,
250        search: Option<&str>,
251    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
252        let offset = (page - 1) * per_page;
253
254        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
255            r#"
256            SELECT {}, COUNT(*) OVER() AS total_count
257            FROM egg_configurations
258            WHERE ($1 IS NULL OR egg_configurations.name ILIKE '%' || $1 || '%')
259            ORDER BY egg_configurations.order_, egg_configurations.created
260            LIMIT $2 OFFSET $3
261            "#,
262            Self::columns_sql(None)
263        )))
264        .bind(search)
265        .bind(per_page)
266        .bind(offset)
267        .fetch_all(database.read())
268        .await?;
269
270        Ok(super::Pagination {
271            total: rows
272                .first()
273                .map_or(Ok(0), |row| row.try_get("total_count"))?,
274            per_page,
275            page,
276            data: rows
277                .into_iter()
278                .map(|row| Self::map(None, &row))
279                .try_collect_vec()?,
280        })
281    }
282
283    pub async fn merged_by_egg_uuid(
284        database: &crate::database::Database,
285        egg_uuid: uuid::Uuid,
286    ) -> Result<MergedEggConfiguration, crate::database::DatabaseError> {
287        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
288            r#"
289            SELECT {}
290            FROM egg_configurations
291            WHERE $1 = ANY(egg_configurations.eggs)
292            ORDER BY egg_configurations.order_, egg_configurations.created
293            "#,
294            Self::columns_sql(None)
295        )))
296        .bind(egg_uuid)
297        .fetch_all(database.read())
298        .await?;
299
300        let rows = rows
301            .into_iter()
302            .map(|row| Self::map(None, &row))
303            .try_collect_vec()?;
304
305        let mut base = MergedEggConfiguration {
306            config_allocations: None,
307            config_startup: None,
308            config_routes: None,
309        };
310
311        for row in rows {
312            if row.config_allocations.is_some() {
313                base.config_allocations = row.config_allocations;
314            }
315            if row.config_startup.is_some() {
316                base.config_startup = row.config_startup;
317            }
318            if row.config_routes.is_some() {
319                base.config_routes = row.config_routes;
320            }
321        }
322
323        Ok(base)
324    }
325
326    pub async fn cleanup_uuid_arrays(
327        database: &crate::database::Database,
328    ) -> Result<u64, crate::database::DatabaseError> {
329        let result = sqlx::query(
330            "UPDATE egg_configurations
331            SET eggs = COALESCE(
332                (SELECT array_agg(u) FROM unnest(eggs) AS u
333                WHERE EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)),
334                '{}'::uuid[]
335            )
336            WHERE EXISTS (
337                SELECT 1 FROM unnest(eggs) AS u
338                WHERE NOT EXISTS (SELECT 1 FROM nest_eggs WHERE uuid = u)
339            )",
340        )
341        .execute(database.write())
342        .await?;
343
344        Ok(result.rows_affected())
345    }
346}
347
348#[async_trait::async_trait]
349impl IntoAdminApiObject for EggConfiguration {
350    type AdminApiObject = AdminApiEggConfiguration;
351    type ExtraArgs<'a> = ();
352
353    async fn into_admin_api_object<'a>(
354        self,
355        state: &crate::State,
356        _args: Self::ExtraArgs<'a>,
357    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
358        let api_object = AdminApiEggConfiguration::init_hooks(&self, state).await?;
359
360        let api_object = finish_extendible!(
361            AdminApiEggConfiguration {
362                uuid: self.uuid,
363                name: self.name,
364                description: self.description,
365                order: self.order,
366                eggs: self.eggs,
367                config_allocations: self.config_allocations,
368                config_startup: self.config_startup,
369                config_routes: self.config_routes,
370                created: self.created.and_utc(),
371            },
372            api_object,
373            state
374        )?;
375
376        Ok(api_object)
377    }
378}
379
380#[async_trait::async_trait]
381impl ByUuid for EggConfiguration {
382    async fn by_uuid(
383        database: &crate::database::Database,
384        uuid: uuid::Uuid,
385    ) -> Result<Self, crate::database::DatabaseError> {
386        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
387            r#"
388            SELECT {}
389            FROM egg_configurations
390            WHERE egg_configurations.uuid = $1
391            "#,
392            Self::columns_sql(None)
393        )))
394        .bind(uuid)
395        .fetch_one(database.read())
396        .await?;
397
398        Self::map(None, &row)
399    }
400
401    async fn by_uuid_with_transaction(
402        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
403        uuid: uuid::Uuid,
404    ) -> Result<Self, crate::database::DatabaseError> {
405        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
406            r#"
407            SELECT {}
408            FROM egg_configurations
409            WHERE egg_configurations.uuid = $1
410            "#,
411            Self::columns_sql(None)
412        )))
413        .bind(uuid)
414        .fetch_one(&mut **transaction)
415        .await?;
416
417        Self::map(None, &row)
418    }
419}
420
421#[derive(ToSchema, Deserialize, Validate)]
422pub struct CreateEggConfigurationOptions {
423    #[garde(length(chars, min = 1, max = 255))]
424    #[schema(min_length = 1, max_length = 255)]
425    pub name: compact_str::CompactString,
426    #[garde(length(chars, min = 1, max = 1024))]
427    #[schema(min_length = 1, max_length = 1024)]
428    pub description: Option<compact_str::CompactString>,
429    #[garde(skip)]
430    pub order: i16,
431    #[garde(length(max = 100))]
432    #[schema(max_length = 100)]
433    pub eggs: Vec<uuid::Uuid>,
434    #[garde(inner(custom(validate_config_allocations)))]
435    #[schema(inline)]
436    pub config_allocations: Option<EggConfigAllocations>,
437    #[garde(dive)]
438    #[schema(inline)]
439    pub config_startup: Option<EggConfigStartup>,
440    #[garde(dive)]
441    #[schema(inline)]
442    pub config_routes: Option<EggConfigRoutes>,
443}
444
445#[async_trait::async_trait]
446impl CreatableModel for EggConfiguration {
447    type CreateOptions<'a> = CreateEggConfigurationOptions;
448    type CreateResult = Self;
449
450    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
451        static CREATE_LISTENERS: LazyLock<CreateListenerList<EggConfiguration>> =
452            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
453
454        &CREATE_LISTENERS
455    }
456
457    async fn create_with_transaction(
458        state: &crate::State,
459        mut options: Self::CreateOptions<'_>,
460        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
461    ) -> Result<Self, crate::database::DatabaseError> {
462        options.validate()?;
463
464        let mut query_builder = InsertQueryBuilder::new("egg_configurations");
465
466        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
467
468        query_builder
469            .set("name", &options.name)
470            .set("description", &options.description)
471            .set("order_", options.order)
472            .set("eggs", &options.eggs)
473            .set(
474                "config_allocations",
475                options
476                    .config_allocations
477                    .as_ref()
478                    .map(serde_json::to_value)
479                    .transpose()?,
480            )
481            .set(
482                "config_startup",
483                options
484                    .config_startup
485                    .as_ref()
486                    .map(serde_json::to_value)
487                    .transpose()?,
488            )
489            .set(
490                "config_routes",
491                options
492                    .config_routes
493                    .as_ref()
494                    .map(serde_json::to_value)
495                    .transpose()?,
496            );
497
498        let row = query_builder
499            .returning(&Self::columns_sql(None))
500            .fetch_one(&mut **transaction)
501            .await?;
502        let mut egg_configuration = Self::map(None, &row)?;
503
504        Self::run_after_create_handlers(&mut egg_configuration, &options, state, transaction)
505            .await?;
506
507        Ok(egg_configuration)
508    }
509}
510
511#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
512pub struct UpdateEggConfigurationOptions {
513    #[garde(length(chars, min = 1, max = 255))]
514    #[schema(min_length = 1, max_length = 255)]
515    pub name: Option<compact_str::CompactString>,
516    #[garde(length(chars, min = 1, max = 1024))]
517    #[schema(min_length = 1, max_length = 1024)]
518    #[serde(
519        default,
520        skip_serializing_if = "Option::is_none",
521        with = "::serde_with::rust::double_option"
522    )]
523    pub description: Option<Option<compact_str::CompactString>>,
524    #[garde(skip)]
525    pub order: Option<i16>,
526    #[garde(length(max = 100))]
527    #[schema(max_length = 100)]
528    pub eggs: Option<Vec<uuid::Uuid>>,
529
530    #[garde(inner(inner(custom(validate_config_allocations))))]
531    #[schema(inline)]
532    #[serde(
533        default,
534        skip_serializing_if = "Option::is_none",
535        with = "::serde_with::rust::double_option"
536    )]
537    pub config_allocations: Option<Option<EggConfigAllocations>>,
538    #[garde(dive)]
539    #[schema(inline)]
540    #[serde(
541        default,
542        skip_serializing_if = "Option::is_none",
543        with = "::serde_with::rust::double_option"
544    )]
545    pub config_startup: Option<Option<EggConfigStartup>>,
546    #[garde(dive)]
547    #[schema(inline)]
548    #[serde(
549        default,
550        skip_serializing_if = "Option::is_none",
551        with = "::serde_with::rust::double_option"
552    )]
553    pub config_routes: Option<Option<EggConfigRoutes>>,
554}
555
556#[async_trait::async_trait]
557impl UpdatableModel for EggConfiguration {
558    type UpdateOptions = UpdateEggConfigurationOptions;
559
560    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
561        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<EggConfiguration>> =
562            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
563
564        &UPDATE_LISTENERS
565    }
566
567    async fn update_with_transaction(
568        &mut self,
569        state: &crate::State,
570        mut options: Self::UpdateOptions,
571        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
572    ) -> Result<(), crate::database::DatabaseError> {
573        options.validate()?;
574
575        let mut query_builder = UpdateQueryBuilder::new("egg_configurations");
576
577        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
578            .await?;
579
580        query_builder
581            .set("name", options.name.as_ref())
582            .set(
583                "description",
584                options.description.as_ref().map(|d| d.as_ref()),
585            )
586            .set("order_", options.order)
587            .set("eggs", options.eggs.as_ref())
588            .set(
589                "config_allocations",
590                options
591                    .config_allocations
592                    .as_ref()
593                    .map(|c| c.as_ref().map(serde_json::to_value).transpose())
594                    .transpose()?,
595            )
596            .set(
597                "config_startup",
598                options
599                    .config_startup
600                    .as_ref()
601                    .map(|c| c.as_ref().map(serde_json::to_value).transpose())
602                    .transpose()?,
603            )
604            .set(
605                "config_routes",
606                options
607                    .config_routes
608                    .as_ref()
609                    .map(|c| c.as_ref().map(serde_json::to_value).transpose())
610                    .transpose()?,
611            )
612            .where_eq("uuid", self.uuid);
613
614        query_builder.execute(&mut **transaction).await?;
615
616        if let Some(name) = options.name {
617            self.name = name;
618        }
619        if let Some(description) = options.description {
620            self.description = description;
621        }
622        if let Some(order) = options.order {
623            self.order = order;
624        }
625        if let Some(eggs) = options.eggs {
626            self.eggs = eggs;
627        }
628        if let Some(config_allocations) = options.config_allocations {
629            self.config_allocations = config_allocations;
630        }
631        if let Some(config_startup) = options.config_startup {
632            self.config_startup = config_startup;
633        }
634        if let Some(config_routes) = options.config_routes {
635            self.config_routes = config_routes;
636        }
637
638        self.run_after_update_handlers(state, transaction).await?;
639
640        Ok(())
641    }
642}
643
644#[async_trait::async_trait]
645impl DeletableModel for EggConfiguration {
646    type DeleteOptions = ();
647
648    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
649        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<EggConfiguration>> =
650            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
651
652        &DELETE_LISTENERS
653    }
654
655    async fn delete_with_transaction(
656        &self,
657        state: &crate::State,
658        options: Self::DeleteOptions,
659        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
660    ) -> Result<(), anyhow::Error> {
661        self.run_delete_handlers(&options, state, transaction)
662            .await?;
663
664        sqlx::query(
665            r#"
666            DELETE FROM egg_configurations
667            WHERE egg_configurations.uuid = $1
668            "#,
669        )
670        .bind(self.uuid)
671        .execute(&mut **transaction)
672        .await?;
673
674        self.run_after_delete_handlers(&options, state, transaction)
675            .await?;
676
677        Ok(())
678    }
679}
680
681#[derive(Validate)]
682pub struct DuplicateEggConfigurationOptions {
683    #[garde(length(chars, min = 1, max = 255))]
684    pub name: compact_str::CompactString,
685}
686
687#[async_trait::async_trait]
688impl DuplicableModel for EggConfiguration {
689    type DuplicateOptions<'a> = DuplicateEggConfigurationOptions;
690
691    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
692        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<EggConfiguration>> =
693            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
694
695        &DUPLICATE_LISTENERS
696    }
697
698    async fn duplicate_with_transaction(
699        &self,
700        state: &crate::State,
701        options: Self::DuplicateOptions<'_>,
702        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
703    ) -> Result<Self, crate::database::DatabaseError> {
704        options.validate()?;
705
706        self.run_duplicate_handlers(&options, state, transaction)
707            .await?;
708
709        let mut query_builder = InsertQueryBuilder::new("egg_configurations");
710
711        query_builder
712            .set("name", &options.name)
713            .set("description", &self.description)
714            .set("order_", self.order)
715            .set("eggs", &self.eggs)
716            .set(
717                "config_allocations",
718                self.config_allocations
719                    .as_ref()
720                    .map(serde_json::to_value)
721                    .transpose()?,
722            )
723            .set(
724                "config_startup",
725                self.config_startup
726                    .as_ref()
727                    .map(serde_json::to_value)
728                    .transpose()?,
729            )
730            .set(
731                "config_routes",
732                self.config_routes
733                    .as_ref()
734                    .map(serde_json::to_value)
735                    .transpose()?,
736            );
737
738        let row = query_builder
739            .returning(&Self::columns_sql(None))
740            .fetch_one(&mut **transaction)
741            .await?;
742        let mut egg_configuration = Self::map(None, &row)?;
743
744        self.run_after_duplicate_handlers(&mut egg_configuration, &options, state, transaction)
745            .await?;
746
747        Ok(egg_configuration)
748    }
749}
750
751#[schema_extension_derive::extendible]
752#[init_args(EggConfiguration, crate::State)]
753#[hook_args(crate::State)]
754#[derive(ToSchema, Serialize)]
755#[schema(title = "AdminEggConfiguration")]
756pub struct AdminApiEggConfiguration {
757    pub uuid: uuid::Uuid,
758
759    pub name: compact_str::CompactString,
760    pub description: Option<compact_str::CompactString>,
761    pub order: i16,
762
763    pub eggs: Vec<uuid::Uuid>,
764
765    #[schema(inline)]
766    pub config_allocations: Option<EggConfigAllocations>,
767    #[schema(inline)]
768    pub config_startup: Option<EggConfigStartup>,
769    #[schema(inline)]
770    pub config_routes: Option<EggConfigRoutes>,
771
772    pub created: chrono::DateTime<chrono::Utc>,
773}
774
775#[derive(Deserialize, Serialize)]
776pub struct MergedEggConfiguration {
777    pub config_allocations: Option<EggConfigAllocations>,
778    pub config_startup: Option<EggConfigStartup>,
779    pub config_routes: Option<EggConfigRoutes>,
780}
781
782impl MergedEggConfiguration {}
783
784#[async_trait::async_trait]
785impl IntoApiObject for MergedEggConfiguration {
786    type ApiObject = ApiEggConfiguration;
787    type ExtraArgs<'a> = ();
788
789    async fn into_api_object<'a>(
790        self,
791        state: &crate::State,
792        _args: Self::ExtraArgs<'a>,
793    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
794        let api_object = ApiEggConfiguration::init_hooks(&self, state).await?;
795
796        let api_object = finish_extendible!(
797            ApiEggConfiguration {
798                allocation_self_assign_enabled: self
799                    .config_allocations
800                    .as_ref()
801                    .is_some_and(|c| c.user_self_assign.enabled),
802                allocation_self_assign_require_primary: self
803                    .config_allocations
804                    .as_ref()
805                    .is_some_and(|c| c.user_self_assign.require_primary_allocation),
806                startup_allow_custom_command: self
807                    .config_startup
808                    .as_ref()
809                    .is_some_and(|c| c.allow_custom_startup_command),
810                route_order: self.config_routes.map(|c| c.order),
811            },
812            api_object,
813            state
814        )?;
815
816        Ok(api_object)
817    }
818}
819
820#[schema_extension_derive::extendible]
821#[init_args(MergedEggConfiguration, crate::State)]
822#[hook_args(crate::State)]
823#[derive(ToSchema, Serialize)]
824#[schema(title = "NestEggConfiguration")]
825pub struct ApiEggConfiguration {
826    pub allocation_self_assign_enabled: bool,
827    pub allocation_self_assign_require_primary: bool,
828    pub startup_allow_custom_command: bool,
829    pub route_order: Option<Vec<crate::settings::RouteOrderItem>>,
830}