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 std::borrow::Cow;
9
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use systemprompt_identifiers::RouteId;
12
13use super::super::providers::ProviderRegistry;
14use super::config::{GatewayConfig, GatewayConfigSpec};
15
16#[derive(Debug, Clone)]
17pub enum GatewayState {
18    Spec(GatewayConfigSpec),
19    Resolved(GatewayConfig),
20}
21
22impl GatewayState {
23    /// The resolved runtime config, or `None` if the loader did not run.
24    /// A `None` here is a bootstrap-ordering bug; the log line is the
25    /// signal — production read paths fall through to the same "gateway
26    /// absent" path they already handle.
27    #[must_use]
28    pub fn resolved(&self) -> Option<&GatewayConfig> {
29        match self {
30            Self::Resolved(c) => Some(c),
31            Self::Spec(_) => {
32                tracing::error!(
33                    "gateway state is still Spec at runtime read; GatewayConfigSpec::resolve was \
34                     never called — treating gateway as absent"
35                );
36                None
37            },
38        }
39    }
40
41    #[must_use]
42    pub const fn as_spec_mut(&mut self) -> Option<&mut GatewayConfigSpec> {
43        match self {
44            Self::Spec(s) => Some(s),
45            Self::Resolved(_) => None,
46        }
47    }
48
49    pub fn into_spec(self) -> GatewayConfigSpec {
50        match self {
51            Self::Spec(s) => s,
52            Self::Resolved(c) => c.to_spec(),
53        }
54    }
55
56    #[must_use]
57    pub fn dispatchable_route_ids(&self, registry: &ProviderRegistry) -> Vec<RouteId> {
58        let config = match self {
59            Self::Resolved(c) => Cow::Borrowed(c),
60            Self::Spec(s) => Cow::Owned(s.clone().resolve()),
61        };
62        config.dispatchable_route_ids(registry)
63    }
64}
65
66impl<'de> Deserialize<'de> for GatewayState {
67    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68    where
69        D: Deserializer<'de>,
70    {
71        GatewayConfigSpec::deserialize(deserializer).map(Self::Spec)
72    }
73}
74
75impl Serialize for GatewayState {
76    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
77    where
78        S: Serializer,
79    {
80        match self {
81            Self::Spec(s) => s.serialize(serializer),
82            Self::Resolved(c) => c.to_spec().serialize(serializer),
83        }
84    }
85}
86
87impl schemars::JsonSchema for GatewayState {
88    fn schema_name() -> Cow<'static, str> {
89        GatewayConfigSpec::schema_name()
90    }
91
92    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
93        GatewayConfigSpec::json_schema(generator)
94    }
95}