use std::{collections::HashMap, sync::Arc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum PathMatch {
Exact {
path: String,
},
Prefix {
path_prefix: String,
},
}
impl PathMatch {
pub fn is_exact(&self) -> bool {
matches!(self, Self::Exact { .. })
}
pub fn len(&self) -> usize {
self.value().len()
}
pub fn is_empty(&self) -> bool {
self.value().is_empty()
}
pub fn value(&self) -> &str {
match self {
Self::Exact { path } => path,
Self::Prefix { path_prefix } => path_prefix,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Route {
#[serde(flatten)]
pub path_match: PathMatch,
pub cluster: Arc<str>,
#[serde(default)]
pub headers: Option<HashMap<String, String>>,
#[serde(default)]
pub host: Option<String>,
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::needless_raw_strings,
clippy::needless_raw_string_hashes,
reason = "tests use unwrap/expect/indexing/raw strings for brevity"
)]
mod tests {
use super::*;
#[test]
fn parse_route_without_host() {
let yaml = r#"
path_prefix: "/api"
cluster: "backend"
"#;
let route: Route = serde_yaml::from_str(yaml).unwrap();
assert_eq!(route.path_match.value(), "/api", "path value mismatch");
assert!(!route.path_match.is_exact(), "should be prefix match");
assert_eq!(&*route.cluster, "backend", "cluster mismatch");
assert!(route.host.is_none(), "host should be None when omitted");
}
#[test]
fn parse_route_with_headers() {
let yaml = r#"
path_prefix: "/"
cluster: "backend"
headers:
x-model: "model-alpha-1"
x-version: "v1"
"#;
let route: Route = serde_yaml::from_str(yaml).unwrap();
let headers = route.headers.unwrap();
assert_eq!(headers.len(), 2, "should have 2 header constraints");
assert_eq!(
headers.get("x-model").unwrap(),
"model-alpha-1",
"x-model header mismatch"
);
assert_eq!(headers.get("x-version").unwrap(), "v1", "x-version header mismatch");
}
#[test]
fn parse_route_with_host() {
let yaml = r#"
path_prefix: "/"
host: "api.example.com"
cluster: "api"
"#;
let route: Route = serde_yaml::from_str(yaml).unwrap();
assert_eq!(route.host.as_deref(), Some("api.example.com"), "host should be parsed");
}
#[test]
fn parse_exact_path() {
let yaml = r#"
path: "/exact"
cluster: "backend"
"#;
let route: Route = serde_yaml::from_str(yaml).unwrap();
assert!(route.path_match.is_exact(), "should be exact match");
assert_eq!(route.path_match.value(), "/exact", "exact path mismatch");
}
#[test]
fn path_match_len() {
let prefix = PathMatch::Prefix {
path_prefix: "/api/".to_owned(),
};
assert_eq!(prefix.len(), 5, "prefix length mismatch");
let exact = PathMatch::Exact {
path: "/one".to_owned(),
};
assert_eq!(exact.len(), 4, "exact length mismatch");
}
}