Skip to main content

soma_gateway/
config.rs

1//! Gateway configuration DTOs and local persistence-safe views.
2
3pub mod defaults;
4pub mod protected_routes;
5pub mod virtual_servers;
6
7pub mod upstream {
8    pub use soma_mcp_client::config::*;
9}
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14pub use defaults::GatewayPaths;
15pub use protected_routes::{ProtectedGatewaySubsetTarget, ProtectedMcpRouteConfig};
16pub use soma_mcp_client::config::{
17    GatewayUpstreamOauthConfig, GatewayUpstreamOauthMode, GatewayUpstreamOauthRegistration,
18    UpstreamConfig, UpstreamConfigView,
19};
20pub use virtual_servers::VirtualServerConfig;
21
22#[derive(Debug, Error)]
23pub enum ConfigError {
24    #[error("{field}: {message}")]
25    InvalidField {
26        field: &'static str,
27        message: String,
28    },
29    #[error("io error while handling {path}: {source}")]
30    Io {
31        path: String,
32        #[source]
33        source: std::io::Error,
34    },
35    #[error("toml serialization error: {0}")]
36    TomlSerialize(#[from] toml::ser::Error),
37    #[error("toml parse error: {0}")]
38    TomlDeserialize(#[from] toml::de::Error),
39}
40
41impl ConfigError {
42    pub(crate) fn invalid(field: &'static str, message: impl Into<String>) -> Self {
43        Self::InvalidField {
44            field,
45            message: message.into(),
46        }
47    }
48
49    pub(crate) fn io(path: &std::path::Path, source: std::io::Error) -> Self {
50        Self::Io {
51            path: path.display().to_string(),
52            source,
53        }
54    }
55}
56
57impl From<soma_mcp_client::ConfigError> for ConfigError {
58    fn from(error: soma_mcp_client::ConfigError) -> Self {
59        match error {
60            soma_mcp_client::ConfigError::InvalidField { field, message } => {
61                Self::InvalidField { field, message }
62            }
63        }
64    }
65}
66
67#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68pub struct GatewayConfig {
69    #[serde(default)]
70    pub upstream: Vec<UpstreamConfig>,
71    #[serde(default)]
72    pub protected_mcp_routes: Vec<ProtectedMcpRouteConfig>,
73    #[serde(default)]
74    pub virtual_servers: Vec<VirtualServerConfig>,
75}
76
77impl GatewayConfig {
78    pub fn validate(&self) -> Result<(), ConfigError> {
79        for upstream in &self.upstream {
80            upstream.validate()?;
81        }
82        for route in &self.protected_mcp_routes {
83            route.validate()?;
84        }
85        for server in &self.virtual_servers {
86            server.validate()?;
87        }
88        Ok(())
89    }
90
91    #[must_use]
92    pub fn redacted_view(&self) -> GatewayConfigView {
93        GatewayConfigView {
94            upstream: self
95                .upstream
96                .iter()
97                .map(UpstreamConfig::redacted_view)
98                .collect(),
99            protected_mcp_routes: self
100                .protected_mcp_routes
101                .iter()
102                .map(ProtectedMcpRouteConfigView::from)
103                .collect(),
104            virtual_servers: self.virtual_servers.clone(),
105        }
106    }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct GatewayConfigView {
111    pub upstream: Vec<UpstreamConfigView>,
112    pub protected_mcp_routes: Vec<ProtectedMcpRouteConfigView>,
113    pub virtual_servers: Vec<VirtualServerConfig>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct ProtectedMcpRouteConfigView {
118    pub name: String,
119    pub enabled: bool,
120    pub public_host: String,
121    pub public_path: String,
122    pub upstream: Option<String>,
123    pub has_backend_url: bool,
124    pub scopes: Vec<String>,
125    pub target: Option<ProtectedGatewaySubsetTarget>,
126}
127
128impl From<&ProtectedMcpRouteConfig> for ProtectedMcpRouteConfigView {
129    fn from(route: &ProtectedMcpRouteConfig) -> Self {
130        Self {
131            name: route.name.clone(),
132            enabled: route.enabled,
133            public_host: route.public_host.clone(),
134            public_path: route.public_path.clone(),
135            upstream: route.upstream.clone(),
136            has_backend_url: !route.backend_url.trim().is_empty(),
137            scopes: route.scopes.clone(),
138            target: route.target.clone(),
139        }
140    }
141}
142
143#[cfg(test)]
144#[path = "config_tests.rs"]
145mod tests;