use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Route {
#[serde(rename = "match")]
pub matcher: RouteMatch,
#[serde(default)]
pub filters: Vec<String>,
#[serde(default)]
pub upstream: Option<String>,
#[serde(default)]
pub backends: Vec<Backend>,
#[serde(default)]
pub strip_prefix: Option<String>,
#[serde(default)]
pub rate_limit: Option<RouteRateLimit>,
#[serde(default)]
pub upgrade: Option<RouteUpgrade>,
#[serde(default)]
pub compression: Option<RouteCompression>,
}
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RouteUpgrade {
pub protocols: Vec<String>,
#[serde(default = "default_upgrade_idle_timeout_ms")]
pub idle_timeout_ms: u64,
}
fn default_upgrade_idle_timeout_ms() -> u64 {
300_000
}
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RouteCompression {
#[serde(default = "default_compression_algorithms")]
pub algorithms: Vec<CompressionAlgorithm>,
#[serde(default = "default_compression_min_length")]
pub min_length: u64,
#[serde(default = "default_compression_content_types")]
pub content_types: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompressionAlgorithm {
Zstd,
Br,
Gzip,
}
impl CompressionAlgorithm {
pub fn token(self) -> &'static str {
match self {
CompressionAlgorithm::Zstd => "zstd",
CompressionAlgorithm::Br => "br",
CompressionAlgorithm::Gzip => "gzip",
}
}
}
fn default_compression_algorithms() -> Vec<CompressionAlgorithm> {
vec![
CompressionAlgorithm::Zstd,
CompressionAlgorithm::Br,
CompressionAlgorithm::Gzip,
]
}
fn default_compression_min_length() -> u64 {
1024
}
fn default_compression_content_types() -> Vec<String> {
[
"text/html",
"text/css",
"text/plain",
"text/xml",
"text/javascript",
"application/javascript",
"application/x-javascript",
"application/json",
"application/xml",
"application/xhtml+xml",
"application/rss+xml",
"application/atom+xml",
"image/svg+xml",
"application/wasm",
]
.map(str::to_string)
.to_vec()
}
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RouteMatch {
#[serde(default)]
pub host: Option<String>,
pub path_prefix: String,
#[serde(default)]
pub method: Option<String>,
#[serde(default)]
pub headers: BTreeMap<String, String>,
#[serde(default)]
pub query: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Backend {
pub upstream: String,
#[serde(default = "default_weight")]
pub weight: u32,
}
fn default_weight() -> u32 {
1
}
pub(crate) const MAX_BACKEND_WEIGHT: u32 = 1_000_000;
impl Route {
pub(crate) fn targets(&self) -> Result<Vec<(&str, u32)>, &'static str> {
match (self.upstream.as_deref(), self.backends.as_slice()) {
(Some(_), [_, ..]) => {
Err("a route sets both `upstream` and `backends`; set exactly one")
}
(None, []) => Err("a route sets neither `upstream` nor `backends`; set exactly one"),
(Some(name), []) => Ok(vec![(name, 1)]),
(None, backends) => Ok(backends
.iter()
.map(|b| (b.upstream.as_str(), b.weight))
.collect()),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RouteRateLimit {
pub rate: u64,
pub burst: u64,
#[serde(default)]
pub key: RateLimitKeyKind,
}
#[derive(
Debug, Clone, Copy, Default, Deserialize, schemars::JsonSchema, Serialize, PartialEq, Eq,
)]
#[serde(rename_all = "kebab-case")]
pub enum RateLimitKeyKind {
#[default]
Route,
ClientIp,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::Manifest;
#[test]
fn route_rate_limit_defaults_absent_and_parses_with_key() {
let m = Manifest::from_toml(
r#"
[[route]]
upstream = "a"
[route.match]
path_prefix = "/"
"#,
)
.unwrap();
assert!(
m.routes[0].rate_limit.is_none(),
"an absent rate_limit is unlimited"
);
let m2 = Manifest::from_toml(
r#"
[[route]]
upstream = "a"
[route.match]
path_prefix = "/"
[route.rate_limit]
rate = 100
burst = 200
"#,
)
.unwrap();
let rl = m2.routes[0].rate_limit.unwrap();
assert_eq!(rl.rate, 100);
assert_eq!(rl.burst, 200);
assert_eq!(rl.key, RateLimitKeyKind::Route, "key defaults to route");
let m3 = Manifest::from_toml(
r#"
[[route]]
upstream = "a"
[route.match]
path_prefix = "/"
[route.rate_limit]
rate = 5
burst = 5
key = "client-ip"
"#,
)
.unwrap();
assert_eq!(
m3.routes[0].rate_limit.unwrap().key,
RateLimitKeyKind::ClientIp
);
}
}