greentic_types/
store.rs

1//! Storefront, catalog, subscription, and desired state shared models.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7#[cfg(feature = "schemars")]
8use schemars::JsonSchema;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::{
14    ArtifactRef, BundleId, CollectionId, ComponentRef, DistributorRef, EnvironmentRef,
15    MetadataRecordRef, PackId, PackRef, SemverReq, StoreFrontId, StorePlanId, StoreProductId,
16    SubscriptionId, TenantCtx,
17};
18
19/// Visual theme tokens for a storefront.
20#[derive(Clone, Debug, PartialEq)]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22#[cfg_attr(feature = "schemars", derive(JsonSchema))]
23pub struct Theme {
24    /// Primary color hex code.
25    pub primary_color: String,
26    /// Secondary color hex code.
27    pub secondary_color: String,
28    /// Accent color hex code.
29    pub accent_color: String,
30    /// Background color hex code.
31    pub background_color: String,
32    /// Text color hex code.
33    pub text_color: String,
34    /// Primary font family.
35    pub font_family: String,
36    /// Optional logo URL.
37    #[cfg_attr(
38        feature = "serde",
39        serde(default, skip_serializing_if = "Option::is_none")
40    )]
41    pub logo_url: Option<String>,
42    /// Optional favicon URL.
43    #[cfg_attr(
44        feature = "serde",
45        serde(default, skip_serializing_if = "Option::is_none")
46    )]
47    pub favicon_url: Option<String>,
48    /// Optional hero image URL.
49    #[cfg_attr(
50        feature = "serde",
51        serde(default, skip_serializing_if = "Option::is_none")
52    )]
53    pub hero_image_url: Option<String>,
54    /// Optional hero title.
55    #[cfg_attr(
56        feature = "serde",
57        serde(default, skip_serializing_if = "Option::is_none")
58    )]
59    pub hero_title: Option<String>,
60    /// Optional hero subtitle.
61    #[cfg_attr(
62        feature = "serde",
63        serde(default, skip_serializing_if = "Option::is_none")
64    )]
65    pub hero_subtitle: Option<String>,
66    /// Optional card corner radius in pixels.
67    #[cfg_attr(
68        feature = "serde",
69        serde(default, skip_serializing_if = "Option::is_none")
70    )]
71    pub card_radius: Option<u8>,
72    /// Optional card elevation hint.
73    #[cfg_attr(
74        feature = "serde",
75        serde(default, skip_serializing_if = "Option::is_none")
76    )]
77    pub card_elevation: Option<u8>,
78    /// Optional button style token.
79    #[cfg_attr(
80        feature = "serde",
81        serde(default, skip_serializing_if = "Option::is_none")
82    )]
83    pub button_style: Option<String>,
84}
85
86impl Default for Theme {
87    fn default() -> Self {
88        Self {
89            primary_color: "#0f766e".into(),
90            secondary_color: "#134e4a".into(),
91            accent_color: "#10b981".into(),
92            background_color: "#ffffff".into(),
93            text_color: "#0f172a".into(),
94            font_family: "Inter, sans-serif".into(),
95            logo_url: None,
96            favicon_url: None,
97            hero_image_url: None,
98            hero_title: None,
99            hero_subtitle: None,
100            card_radius: None,
101            card_elevation: None,
102            button_style: None,
103        }
104    }
105}
106
107/// Layout section kind for storefront composition.
108#[derive(Clone, Debug, PartialEq, Eq, Hash)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
111#[cfg_attr(feature = "schemars", derive(JsonSchema))]
112pub enum LayoutSectionKind {
113    /// Hero section.
114    Hero,
115    /// Featured collection of products.
116    FeaturedCollection,
117    /// Grid of products.
118    Grid,
119    /// Call-to-action section.
120    Cta,
121    /// Custom section identified by name.
122    Custom(String),
123}
124
125/// Layout section configuration.
126#[derive(Clone, Debug, PartialEq)]
127#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
128#[cfg_attr(feature = "schemars", derive(JsonSchema))]
129pub struct LayoutSection {
130    /// Stable section identifier.
131    pub id: String,
132    /// Section kind.
133    pub kind: LayoutSectionKind,
134    /// Optional collection backing the section.
135    #[cfg_attr(
136        feature = "serde",
137        serde(default, skip_serializing_if = "Option::is_none")
138    )]
139    pub collection_id: Option<CollectionId>,
140    /// Optional title.
141    #[cfg_attr(
142        feature = "serde",
143        serde(default, skip_serializing_if = "Option::is_none")
144    )]
145    pub title: Option<String>,
146    /// Optional subtitle.
147    #[cfg_attr(
148        feature = "serde",
149        serde(default, skip_serializing_if = "Option::is_none")
150    )]
151    pub subtitle: Option<String>,
152    /// Ordering hint for rendering.
153    pub sort_order: i32,
154    /// Free-form metadata for front-end rendering.
155    #[cfg_attr(feature = "serde", serde(default))]
156    pub metadata: BTreeMap<String, Value>,
157}
158
159/// Collection of products curated for a storefront.
160#[derive(Clone, Debug, PartialEq)]
161#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
162#[cfg_attr(feature = "schemars", derive(JsonSchema))]
163pub struct Collection {
164    /// Collection identifier.
165    pub id: CollectionId,
166    /// Storefront owning the collection.
167    pub storefront_id: StoreFrontId,
168    /// Display title.
169    pub title: String,
170    /// Products included in the collection.
171    #[cfg_attr(
172        feature = "serde",
173        serde(default, skip_serializing_if = "Vec::is_empty")
174    )]
175    pub product_ids: Vec<StoreProductId>,
176    /// Optional slug.
177    #[cfg_attr(
178        feature = "serde",
179        serde(default, skip_serializing_if = "Option::is_none")
180    )]
181    pub slug: Option<String>,
182    /// Optional description.
183    #[cfg_attr(
184        feature = "serde",
185        serde(default, skip_serializing_if = "Option::is_none")
186    )]
187    pub description: Option<String>,
188    /// Sort order hint.
189    pub sort_order: i32,
190    /// Additional metadata.
191    #[cfg_attr(feature = "serde", serde(default))]
192    pub metadata: BTreeMap<String, Value>,
193}
194
195/// Override applied to a product within a storefront.
196#[derive(Clone, Debug, PartialEq)]
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
198#[cfg_attr(feature = "schemars", derive(JsonSchema))]
199pub struct ProductOverride {
200    /// Storefront receiving the override.
201    pub storefront_id: StoreFrontId,
202    /// Product being overridden.
203    pub product_id: StoreProductId,
204    /// Optional display name override.
205    #[cfg_attr(
206        feature = "serde",
207        serde(default, skip_serializing_if = "Option::is_none")
208    )]
209    pub display_name: Option<String>,
210    /// Optional short description override.
211    #[cfg_attr(
212        feature = "serde",
213        serde(default, skip_serializing_if = "Option::is_none")
214    )]
215    pub short_description: Option<String>,
216    /// Badges to render on the product card.
217    #[cfg_attr(
218        feature = "serde",
219        serde(default, skip_serializing_if = "Vec::is_empty")
220    )]
221    pub badges: Vec<String>,
222    /// Additional metadata.
223    #[cfg_attr(feature = "serde", serde(default))]
224    pub metadata: BTreeMap<String, Value>,
225}
226
227/// Storefront configuration and content.
228#[derive(Clone, Debug, PartialEq)]
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230#[cfg_attr(feature = "schemars", derive(JsonSchema))]
231pub struct StoreFront {
232    /// Storefront identifier.
233    pub id: StoreFrontId,
234    /// Slug used for routing.
235    pub slug: String,
236    /// Display name.
237    pub name: String,
238    /// Visual theme.
239    #[cfg_attr(feature = "serde", serde(default))]
240    pub theme: Theme,
241    /// Layout sections composing the storefront.
242    #[cfg_attr(
243        feature = "serde",
244        serde(default, skip_serializing_if = "Vec::is_empty")
245    )]
246    pub sections: Vec<LayoutSection>,
247    /// Curated collections.
248    #[cfg_attr(
249        feature = "serde",
250        serde(default, skip_serializing_if = "Vec::is_empty")
251    )]
252    pub collections: Vec<Collection>,
253    /// Product overrides scoped to this storefront.
254    #[cfg_attr(
255        feature = "serde",
256        serde(default, skip_serializing_if = "Vec::is_empty")
257    )]
258    pub overrides: Vec<ProductOverride>,
259    /// Optional worker identifier used by messaging.
260    #[cfg_attr(
261        feature = "serde",
262        serde(default, skip_serializing_if = "Option::is_none")
263    )]
264    pub worker_id: Option<String>,
265    /// Additional metadata.
266    #[cfg_attr(feature = "serde", serde(default))]
267    pub metadata: BTreeMap<String, Value>,
268}
269
270/// Kinds of products exposed by the store catalog.
271#[derive(Clone, Debug, PartialEq, Eq, Hash)]
272#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
273#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
274#[cfg_attr(feature = "schemars", derive(JsonSchema))]
275pub enum StoreProductKind {
276    /// Component offering.
277    Component,
278    /// Flow offering.
279    Flow,
280    /// Pack offering.
281    Pack,
282}
283
284/// Strategy used to resolve versions.
285#[derive(Clone, Debug, PartialEq, Eq, Hash)]
286#[cfg_attr(feature = "schemars", derive(JsonSchema))]
287pub enum VersionStrategy {
288    /// Always track the latest version.
289    Latest,
290    /// Use a pinned semantic version requirement (legacy shape).
291    Pinned {
292        /// Version requirement (e.g. ^1.2).
293        requirement: SemverReq,
294    },
295    /// Track a long-term support channel (legacy shape).
296    Lts,
297    /// Custom strategy identified by name (legacy shape).
298    Custom(String),
299    /// Always track the latest published version for this component.
300    Fixed {
301        /// Exact version string (e.g. "1.2.3").
302        version: String,
303    },
304    /// A semver-style range (e.g. ">=1.2,<2.0").
305    Range {
306        /// Version range expression.
307        range: String,
308    },
309    /// A named channel (e.g. "stable", "beta", "canary").
310    Channel {
311        /// Channel name.
312        channel: String,
313    },
314    /// Forward-compatible escape hatch for unknown strategies.
315    CustomTagged {
316        /// Free-form value for the strategy.
317        value: String,
318    },
319}
320
321#[cfg(feature = "serde")]
322impl Serialize for VersionStrategy {
323    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
324    where
325        S: serde::Serializer,
326    {
327        #[derive(Serialize)]
328        #[serde(rename_all = "snake_case")]
329        enum Legacy<'a> {
330            Latest,
331            Pinned { requirement: &'a SemverReq },
332            Lts,
333            Custom(&'a str),
334        }
335
336        #[derive(Serialize)]
337        struct Tagged<'a> {
338            #[serde(rename = "kind")]
339            kind: &'static str,
340            #[serde(skip_serializing_if = "Option::is_none")]
341            version: Option<&'a String>,
342            #[serde(skip_serializing_if = "Option::is_none")]
343            range: Option<&'a String>,
344            #[serde(skip_serializing_if = "Option::is_none")]
345            channel: Option<&'a String>,
346            #[serde(skip_serializing_if = "Option::is_none")]
347            value: Option<&'a String>,
348        }
349
350        match self {
351            VersionStrategy::Latest => Legacy::Latest.serialize(serializer),
352            VersionStrategy::Pinned { requirement } => {
353                Legacy::Pinned { requirement }.serialize(serializer)
354            }
355            VersionStrategy::Lts => Legacy::Lts.serialize(serializer),
356            VersionStrategy::Custom(value) => Legacy::Custom(value).serialize(serializer),
357            VersionStrategy::Fixed { version } => Tagged {
358                kind: "fixed",
359                version: Some(version),
360                range: None,
361                channel: None,
362                value: None,
363            }
364            .serialize(serializer),
365            VersionStrategy::Range { range } => Tagged {
366                kind: "range",
367                version: None,
368                range: Some(range),
369                channel: None,
370                value: None,
371            }
372            .serialize(serializer),
373            VersionStrategy::Channel { channel } => Tagged {
374                kind: "channel",
375                version: None,
376                range: None,
377                channel: Some(channel),
378                value: None,
379            }
380            .serialize(serializer),
381            VersionStrategy::CustomTagged { value } => Tagged {
382                kind: "custom",
383                version: None,
384                range: None,
385                channel: None,
386                value: Some(value),
387            }
388            .serialize(serializer),
389        }
390    }
391}
392
393#[cfg(feature = "serde")]
394impl<'de> Deserialize<'de> for VersionStrategy {
395    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396    where
397        D: serde::Deserializer<'de>,
398    {
399        #[derive(Deserialize)]
400        #[serde(rename_all = "snake_case")]
401        enum Legacy {
402            Latest,
403            Pinned { requirement: SemverReq },
404            Lts,
405            Custom(String),
406        }
407
408        #[derive(Deserialize)]
409        #[serde(tag = "kind", rename_all = "snake_case")]
410        enum Tagged {
411            Latest,
412            Fixed { version: String },
413            Range { range: String },
414            Channel { channel: String },
415            Custom { value: String },
416        }
417
418        #[derive(Deserialize)]
419        #[serde(untagged)]
420        enum Wrapper {
421            Tagged(Tagged),
422            Legacy(Legacy),
423        }
424
425        match Wrapper::deserialize(deserializer)? {
426            Wrapper::Tagged(tagged) => match tagged {
427                Tagged::Latest => Ok(VersionStrategy::Latest),
428                Tagged::Fixed { version } => Ok(VersionStrategy::Fixed { version }),
429                Tagged::Range { range } => Ok(VersionStrategy::Range { range }),
430                Tagged::Channel { channel } => Ok(VersionStrategy::Channel { channel }),
431                Tagged::Custom { value } => Ok(VersionStrategy::CustomTagged { value }),
432            },
433            Wrapper::Legacy(legacy) => match legacy {
434                Legacy::Latest => Ok(VersionStrategy::Latest),
435                Legacy::Pinned { requirement } => Ok(VersionStrategy::Pinned { requirement }),
436                Legacy::Lts => Ok(VersionStrategy::Lts),
437                Legacy::Custom(value) => Ok(VersionStrategy::Custom(value)),
438            },
439        }
440    }
441}
442
443/// Map of capability group -> list of capability values.
444pub type CapabilityMap = BTreeMap<String, Vec<String>>;
445
446/// Catalog product describing a component, flow, or pack.
447#[derive(Clone, Debug, PartialEq)]
448#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
449#[cfg_attr(feature = "schemars", derive(JsonSchema))]
450pub struct StoreProduct {
451    /// Product identifier.
452    pub id: StoreProductId,
453    /// Product kind.
454    pub kind: StoreProductKind,
455    /// Display name.
456    pub name: String,
457    /// Slug for routing.
458    pub slug: String,
459    /// Description.
460    pub description: String,
461    /// Source repository reference.
462    pub source_repo: crate::RepoRef,
463    /// Optional component reference.
464    #[cfg_attr(
465        feature = "serde",
466        serde(default, skip_serializing_if = "Option::is_none")
467    )]
468    pub component_ref: Option<ComponentRef>,
469    /// Optional pack reference.
470    #[cfg_attr(
471        feature = "serde",
472        serde(default, skip_serializing_if = "Option::is_none")
473    )]
474    pub pack_ref: Option<PackId>,
475    /// Optional category label.
476    #[cfg_attr(
477        feature = "serde",
478        serde(default, skip_serializing_if = "Option::is_none")
479    )]
480    pub category: Option<String>,
481    /// Tags for filtering.
482    #[cfg_attr(
483        feature = "serde",
484        serde(default, skip_serializing_if = "Vec::is_empty")
485    )]
486    pub tags: Vec<String>,
487    /// Capabilities exposed by the product.
488    #[cfg_attr(feature = "serde", serde(default))]
489    pub capabilities: CapabilityMap,
490    /// Version resolution strategy.
491    pub version_strategy: VersionStrategy,
492    /// Default plan identifier, if any.
493    #[cfg_attr(
494        feature = "serde",
495        serde(default, skip_serializing_if = "Option::is_none")
496    )]
497    pub default_plan_id: Option<StorePlanId>,
498    /// Convenience flag indicating the default plan is free.
499    pub is_free: bool,
500    /// Additional metadata.
501    #[cfg_attr(feature = "serde", serde(default))]
502    pub metadata: BTreeMap<String, Value>,
503}
504
505/// Pricing model for a plan.
506#[derive(Clone, Debug, PartialEq, Eq, Hash)]
507#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
508#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
509#[cfg_attr(feature = "schemars", derive(JsonSchema))]
510pub enum PriceModel {
511    /// Free plan.
512    Free,
513    /// Flat recurring price.
514    Flat {
515        /// Amount in micro-units per period.
516        amount_micro: u64,
517        /// Billing period length in days.
518        period_days: u16,
519    },
520    /// Metered pricing with included units.
521    Metered {
522        /// Included units per period.
523        included_units: u64,
524        /// Overage rate per additional unit (micro-units).
525        overage_rate_micro: u64,
526        /// Unit label (for example `build-minute`).
527        unit_label: String,
528    },
529    /// Enterprise/custom pricing.
530    Enterprise {
531        /// Human-readable description.
532        description: String,
533    },
534}
535
536/// Plan limits used for entitlements.
537#[derive(Clone, Debug, PartialEq, Eq, Default)]
538#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
539#[cfg_attr(feature = "schemars", derive(JsonSchema))]
540pub struct PlanLimits {
541    /// Maximum environments allowed.
542    #[cfg_attr(
543        feature = "serde",
544        serde(default, skip_serializing_if = "Option::is_none")
545    )]
546    pub max_environments: Option<u32>,
547    /// Maximum subscriptions allowed.
548    #[cfg_attr(
549        feature = "serde",
550        serde(default, skip_serializing_if = "Option::is_none")
551    )]
552    pub max_subscriptions: Option<u32>,
553    /// Included units per period (semantic depends on product).
554    #[cfg_attr(
555        feature = "serde",
556        serde(default, skip_serializing_if = "Option::is_none")
557    )]
558    pub monthly_units_included: Option<u64>,
559    /// Additional metadata.
560    #[cfg_attr(feature = "serde", serde(default))]
561    pub metadata: BTreeMap<String, Value>,
562}
563
564/// Plan associated with a store product.
565#[derive(Clone, Debug, PartialEq)]
566#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
567#[cfg_attr(feature = "schemars", derive(JsonSchema))]
568pub struct StorePlan {
569    /// Plan identifier.
570    pub id: StorePlanId,
571    /// Plan name.
572    pub name: String,
573    /// Plan description.
574    pub description: String,
575    /// Pricing model.
576    pub price_model: PriceModel,
577    /// Plan limits.
578    #[cfg_attr(feature = "serde", serde(default))]
579    pub limits: PlanLimits,
580    /// Tags for classification.
581    #[cfg_attr(
582        feature = "serde",
583        serde(default, skip_serializing_if = "Vec::is_empty")
584    )]
585    pub tags: Vec<String>,
586    /// Additional metadata.
587    #[cfg_attr(feature = "serde", serde(default))]
588    pub metadata: BTreeMap<String, Value>,
589}
590
591/// Subscription lifecycle status.
592#[derive(Clone, Debug, PartialEq, Eq, Hash)]
593#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
594#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
595#[cfg_attr(feature = "schemars", derive(JsonSchema))]
596pub enum SubscriptionStatus {
597    /// Draft subscription (pending approval).
598    Draft,
599    /// Active subscription.
600    Active,
601    /// Paused subscription.
602    Paused,
603    /// Cancelled subscription.
604    Cancelled,
605    /// Subscription encountered an error.
606    Error,
607}
608
609/// Subscription entry linking a tenant to a product and plan.
610#[derive(Clone, Debug, PartialEq)]
611#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
612#[cfg_attr(feature = "schemars", derive(JsonSchema))]
613pub struct Subscription {
614    /// Subscription identifier.
615    pub id: SubscriptionId,
616    /// Tenant context owning the subscription.
617    pub tenant_ctx: TenantCtx,
618    /// Product identifier.
619    pub product_id: StoreProductId,
620    /// Plan identifier.
621    pub plan_id: StorePlanId,
622    /// Optional target environment.
623    #[cfg_attr(
624        feature = "serde",
625        serde(default, skip_serializing_if = "Option::is_none")
626    )]
627    pub environment_ref: Option<EnvironmentRef>,
628    /// Optional distributor responsible for the environment.
629    #[cfg_attr(
630        feature = "serde",
631        serde(default, skip_serializing_if = "Option::is_none")
632    )]
633    pub distributor_ref: Option<DistributorRef>,
634    /// Current status.
635    pub status: SubscriptionStatus,
636    /// Additional metadata.
637    #[cfg_attr(feature = "serde", serde(default))]
638    pub metadata: BTreeMap<String, Value>,
639}
640
641/// Choice between component or pack reference.
642#[derive(Clone, Debug, PartialEq, Eq, Hash)]
643#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
644#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
645#[cfg_attr(feature = "schemars", derive(JsonSchema))]
646pub enum PackOrComponentRef {
647    /// Component reference.
648    Component(ComponentRef),
649    /// Pack reference.
650    Pack(PackId),
651}
652
653/// Selector describing whether a component or pack should be deployed.
654#[derive(Clone, Debug, PartialEq, Eq)]
655#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
656#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
657#[cfg_attr(feature = "schemars", derive(JsonSchema))]
658pub enum ArtifactSelector {
659    /// Component reference.
660    Component(ComponentRef),
661    /// Pack reference.
662    Pack(PackRef),
663}
664
665/// Desired subscription entry supplied to the distributor.
666#[derive(Clone, Debug, PartialEq)]
667#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
668#[cfg_attr(feature = "schemars", derive(JsonSchema))]
669pub struct DesiredSubscriptionEntry {
670    /// Target artifact selection.
671    pub selector: ArtifactSelector,
672    /// Version strategy to apply.
673    pub version_strategy: VersionStrategy,
674    /// Configuration overrides.
675    #[cfg_attr(feature = "serde", serde(default))]
676    pub config_overrides: BTreeMap<String, Value>,
677    /// Policy tags for downstream enforcement.
678    #[cfg_attr(
679        feature = "serde",
680        serde(default, skip_serializing_if = "Vec::is_empty")
681    )]
682    pub policy_tags: Vec<String>,
683    /// Additional metadata.
684    #[cfg_attr(feature = "serde", serde(default))]
685    pub metadata: BTreeMap<String, Value>,
686}
687
688/// Desired state for an environment.
689#[derive(Clone, Debug, PartialEq)]
690#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
691#[cfg_attr(feature = "schemars", derive(JsonSchema))]
692pub struct DesiredState {
693    /// Tenant context owning the desired state.
694    pub tenant: TenantCtx,
695    /// Target environment reference.
696    pub environment_ref: EnvironmentRef,
697    /// Desired subscriptions.
698    #[cfg_attr(
699        feature = "serde",
700        serde(default, skip_serializing_if = "Vec::is_empty")
701    )]
702    pub entries: Vec<DesiredSubscriptionEntry>,
703    /// Desired state version.
704    pub version: u64,
705    /// Additional metadata.
706    #[cfg_attr(feature = "serde", serde(default))]
707    pub metadata: BTreeMap<String, Value>,
708}
709
710/// Connection kind for an environment.
711#[derive(Clone, Debug, PartialEq, Eq, Hash)]
712#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
713#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
714#[cfg_attr(feature = "schemars", derive(JsonSchema))]
715pub enum ConnectionKind {
716    /// Online environment with direct connectivity.
717    Online,
718    /// Offline or air-gapped environment.
719    Offline,
720}
721
722/// Environment registry entry.
723#[derive(Clone, Debug, PartialEq)]
724#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
725#[cfg_attr(feature = "schemars", derive(JsonSchema))]
726pub struct Environment {
727    /// Environment identifier.
728    pub id: EnvironmentRef,
729    /// Tenant context owning the environment.
730    pub tenant: TenantCtx,
731    /// Human-readable name.
732    pub name: String,
733    /// Labels for selection and grouping.
734    #[cfg_attr(feature = "serde", serde(default))]
735    pub labels: BTreeMap<String, String>,
736    /// Distributor responsible for this environment.
737    pub distributor_ref: DistributorRef,
738    /// Connection kind.
739    pub connection_kind: ConnectionKind,
740    /// Additional metadata.
741    #[cfg_attr(feature = "serde", serde(default))]
742    pub metadata: BTreeMap<String, Value>,
743}
744
745impl Environment {
746    /// Constructs a new environment with the required identifiers.
747    pub fn new(
748        id: EnvironmentRef,
749        tenant: TenantCtx,
750        distributor_ref: DistributorRef,
751        connection_kind: ConnectionKind,
752        name: impl Into<String>,
753    ) -> Self {
754        Self {
755            id,
756            tenant,
757            name: name.into(),
758            distributor_ref,
759            connection_kind,
760            labels: BTreeMap::new(),
761            metadata: BTreeMap::new(),
762        }
763    }
764}
765
766/// Rollout lifecycle state for an environment.
767#[derive(Clone, Debug, PartialEq, Eq, Hash)]
768#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
769#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
770#[cfg_attr(feature = "schemars", derive(JsonSchema))]
771pub enum RolloutState {
772    /// Rollout is pending scheduling or validation.
773    Pending,
774    /// Rollout plan generation is in progress.
775    Planning,
776    /// Rollout is actively executing.
777    InProgress,
778    /// Rollout completed successfully.
779    Succeeded,
780    /// Rollout failed.
781    Failed,
782    /// Rollout is blocked (for example policy or compliance).
783    Blocked,
784}
785
786/// Status record for an environment rollout.
787#[derive(Clone, Debug, PartialEq)]
788#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
789#[cfg_attr(feature = "schemars", derive(JsonSchema))]
790pub struct RolloutStatus {
791    /// Target environment.
792    pub environment_ref: EnvironmentRef,
793    /// Desired state version associated with this rollout.
794    #[cfg_attr(
795        feature = "serde",
796        serde(default, skip_serializing_if = "Option::is_none")
797    )]
798    pub desired_state_version: Option<u64>,
799    /// Current rollout state.
800    pub state: RolloutState,
801    /// Optional bundle used for offline rollouts.
802    #[cfg_attr(
803        feature = "serde",
804        serde(default, skip_serializing_if = "Option::is_none")
805    )]
806    pub bundle_id: Option<BundleId>,
807    /// Optional human-readable message.
808    #[cfg_attr(
809        feature = "serde",
810        serde(default, skip_serializing_if = "Option::is_none")
811    )]
812    pub message: Option<String>,
813    /// Additional metadata.
814    #[cfg_attr(feature = "serde", serde(default))]
815    pub metadata: BTreeMap<String, Value>,
816}
817
818/// Bundle specification for offline or air-gapped deployments.
819#[derive(Clone, Debug, PartialEq)]
820#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
821#[cfg_attr(feature = "schemars", derive(JsonSchema))]
822pub struct BundleSpec {
823    /// Identifier of the distribution-bundle `.gtpack` (pack id).
824    pub bundle_id: BundleId,
825    /// Tenant context for the bundle.
826    pub tenant: TenantCtx,
827    /// Target environment.
828    pub environment_ref: EnvironmentRef,
829    /// Version of the desired state used to construct the bundle.
830    pub desired_state_version: u64,
831    /// Artifact references included in the bundle.
832    #[cfg_attr(
833        feature = "serde",
834        serde(default, skip_serializing_if = "Vec::is_empty")
835    )]
836    pub artifact_refs: Vec<ArtifactRef>,
837    /// Metadata record references (SBOMs, attestations, signatures).
838    #[cfg_attr(
839        feature = "serde",
840        serde(default, skip_serializing_if = "Vec::is_empty")
841    )]
842    pub metadata_refs: Vec<MetadataRecordRef>,
843    /// Additional metadata.
844    #[cfg_attr(feature = "serde", serde(default))]
845    pub additional_metadata: BTreeMap<String, Value>,
846}
847
848/// Export specification used to request a bundle from a desired state.
849#[derive(Clone, Debug, PartialEq)]
850#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
851#[cfg_attr(feature = "schemars", derive(JsonSchema))]
852pub struct DesiredStateExportSpec {
853    /// Tenant context owning the desired state.
854    pub tenant: TenantCtx,
855    /// Target environment.
856    pub environment_ref: EnvironmentRef,
857    /// Desired state version to export.
858    pub desired_state_version: u64,
859    /// Whether to include artifacts in the bundle.
860    #[cfg_attr(feature = "serde", serde(default))]
861    pub include_artifacts: bool,
862    /// Whether to include metadata (SBOMs, attestations).
863    #[cfg_attr(feature = "serde", serde(default))]
864    pub include_metadata: bool,
865    /// Additional metadata.
866    #[cfg_attr(feature = "serde", serde(default))]
867    pub metadata: BTreeMap<String, Value>,
868}