Skip to main content

systemprompt_models/profile/gateway/
state.rs

1//! Lifecycle wrapper for the gateway section of a profile.
2//!
3//! YAML deserialization always produces [`GatewayState::Spec`]; the
4//! profile loader projects it to [`GatewayState::Resolved`]. Runtime read
5//! paths must observe [`GatewayState::Resolved`] — they consult
6//! [`Self::resolved`] which logs and returns `None` if the loader has not run.
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use systemprompt_identifiers::RouteId;
10
11use super::config::{GatewayConfig, GatewayConfigSpec};
12
13#[derive(Debug, Clone)]
14pub enum GatewayState {
15    Spec(GatewayConfigSpec),
16    Resolved(GatewayConfig),
17}
18
19impl GatewayState {
20    /// The resolved runtime config, or `None` if the loader did not run.
21    /// A `None` here is a bootstrap-ordering bug; the log line is the
22    /// signal — production read paths fall through to the same "gateway
23    /// absent" path they already handle.
24    #[must_use]
25    pub fn resolved(&self) -> Option<&GatewayConfig> {
26        match self {
27            Self::Resolved(c) => Some(c),
28            Self::Spec(_) => {
29                tracing::error!(
30                    "gateway state is still Spec at runtime read; GatewayConfigSpec::resolve was \
31                     never called — treating gateway as absent"
32                );
33                None
34            },
35        }
36    }
37
38    #[must_use]
39    pub const fn as_spec_mut(&mut self) -> Option<&mut GatewayConfigSpec> {
40        match self {
41            Self::Spec(s) => Some(s),
42            Self::Resolved(_) => None,
43        }
44    }
45
46    pub fn into_spec(self) -> GatewayConfigSpec {
47        match self {
48            Self::Spec(s) => s,
49            Self::Resolved(c) => c.to_spec(),
50        }
51    }
52
53    /// The content-addressed id of every configured route, with synthesized ids
54    /// filled in. Works in either lifecycle state, so a caller can materialise
55    /// the authz entity catalog straight from the typed profile without having
56    /// run the loader.
57    #[must_use]
58    pub fn resolved_route_ids(&self) -> Vec<RouteId> {
59        let mut spec = self.clone().into_spec();
60        spec.routes
61            .iter_mut()
62            .map(|route| {
63                route.ensure_id();
64                route.id.clone()
65            })
66            .collect()
67    }
68}
69
70impl<'de> Deserialize<'de> for GatewayState {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        GatewayConfigSpec::deserialize(deserializer).map(Self::Spec)
76    }
77}
78
79impl Serialize for GatewayState {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: Serializer,
83    {
84        match self {
85            Self::Spec(s) => s.serialize(serializer),
86            Self::Resolved(c) => c.to_spec().serialize(serializer),
87        }
88    }
89}
90
91impl schemars::JsonSchema for GatewayState {
92    fn schema_name() -> std::borrow::Cow<'static, str> {
93        GatewayConfigSpec::schema_name()
94    }
95
96    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
97        GatewayConfigSpec::json_schema(generator)
98    }
99}