1use crate::config::get_config_builder;
2use ::config::ConfigError;
3use derive_builder::Builder;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::collections::BTreeMap;
6
7pub mod config;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
10pub struct HealthCheck {
11 pub path: String,
12 pub interval_ms: u64,
13 pub timeout_ms: u64,
14 pub expect_status: u16,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub enum MiddlewareType {
19 Inbound,
20 Outbound,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
24pub struct Middleware {
25 pub r#type: MiddlewareType,
26 pub name: String,
27}
28
29#[derive(Debug, Clone)]
30pub enum Plugin {
31 Builtin(BuiltinPlugin),
32 Wasm(WasmPluginConfig),
33}
34
35impl Plugin {
36 pub fn name(&self) -> &str {
37 match self {
38 Plugin::Builtin(builtin) => &builtin.name,
39 Plugin::Wasm(wasm) => &wasm.name,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
45pub struct BuiltinPlugin {
46 pub name: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
50pub struct WasmPluginConfig {
51 pub name: String,
52 pub path: String,
53 pub memory_name: Option<String>,
54 pub handle_name: Option<String>,
55}
56
57#[derive(Deserialize)]
58#[serde(untagged)]
59enum PluginSerde {
60 Name(String),
61 Builtin { builtin: BuiltinPlugin },
62 Wasm { wasm: WasmPluginConfig },
63}
64
65impl<'de> Deserialize<'de> for Plugin {
66 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67 where
68 D: Deserializer<'de>,
69 {
70 match PluginSerde::deserialize(deserializer)? {
71 PluginSerde::Name(name) => Ok(Plugin::Builtin(BuiltinPlugin { name })),
72 PluginSerde::Builtin { builtin } => Ok(Plugin::Builtin(builtin)),
73 PluginSerde::Wasm { wasm } => Ok(Plugin::Wasm(wasm)),
74 }
75 }
76}
77
78impl Serialize for Plugin {
79 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80 where
81 S: Serializer,
82 {
83 match self {
84 Plugin::Builtin(builtin) => BuiltinPlugin::serialize(builtin, serializer),
85 Plugin::Wasm(wasm) => WasmPluginConfig::serialize(wasm, serializer),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
91pub struct Destination {
92 pub name: String,
93 pub url: String,
94 pub health_check: Option<HealthCheck>,
95 #[serde(default)]
96 pub routes: Vec<Route>,
97 #[serde(default)]
98 pub middleware: Vec<Middleware>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
102pub struct ServerConfig {
103 pub address: String,
104 pub force_path_parameter: bool,
105 pub log_upstream_response: bool,
106 pub global_request_middleware: Vec<String>,
107 pub global_response_middleware: Vec<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
111pub struct Route {
112 pub path: String,
113 pub method: String,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)]
117pub struct CardinalConfig {
118 pub server: ServerConfig,
119 pub destinations: BTreeMap<String, Destination>,
120 #[serde(default)]
121 pub plugins: Vec<Plugin>,
122}
123
124impl Default for ServerConfig {
125 fn default() -> Self {
126 ServerConfig {
127 address: "0.0.0.0:1704".into(),
128 force_path_parameter: true,
129 log_upstream_response: true,
130 global_response_middleware: vec![],
131 global_request_middleware: vec![],
132 }
133 }
134}
135
136pub fn load_config(paths: &[String]) -> Result<CardinalConfig, ConfigError> {
137 let builder = get_config_builder(paths)?;
138 let config: CardinalConfig = builder.build()?.try_deserialize()?;
139 validate_config(&config)?;
140
141 Ok(config)
142}
143
144pub fn validate_config(config: &CardinalConfig) -> Result<(), ConfigError> {
145 if config
146 .server
147 .address
148 .parse::<std::net::SocketAddr>()
149 .is_err()
150 {
151 return Err(ConfigError::Message(format!(
152 "Invalid server address: {}",
153 config.server.address
154 )));
155 }
156
157 let all_plugin_names = config
158 .plugins
159 .iter()
160 .map(|p| p.name())
161 .collect::<Vec<&str>>();
162
163 for middleware in config.destinations.values().flat_map(|d| &d.middleware) {
164 if !all_plugin_names.contains(&middleware.name.as_str()) {
165 return Err(ConfigError::Message(format!(
166 "Middleware {} not found. {0} must be included in the list of plugins.",
167 middleware.name
168 )));
169 }
170 }
171
172 for destination in config.destinations.values() {
173 for route in &destination.routes {
174 if !route.path.starts_with('/') {
175 return Err(ConfigError::Message(format!(
176 "Route path {} must start with a '/'.",
177 route.path
178 )));
179 }
180 }
181 }
182
183 for destination in config.destinations.values() {
184 for route in &destination.routes {
185 if !route.method.eq("GET")
186 && !route.method.eq("POST")
187 && !route.method.eq("PUT")
188 && !route.method.eq("DELETE")
189 && !route.method.eq("PATCH")
190 && !route.method.eq("HEAD")
191 && !route.method.eq("OPTIONS")
192 {
193 return Err(ConfigError::Message(format!(
194 "Route method {} is not supported.",
195 route.method
196 )));
197 }
198 }
199 }
200
201 Ok(())
202}