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