1pub 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;