cardinal_config/
lib.rs

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}