mod chain;
mod content_hash;
mod filter_entry;
mod listen;
mod lowering;
mod observability;
mod resumption;
mod route;
mod state;
mod tls;
mod trust;
mod upstream;
mod validate;
use serde::{Deserialize, Serialize};
use crate::error::ControlError;
pub use chain::Chain;
pub use filter_entry::{
FilterEntry, IsolationKind, OutboundHttpConfig, OutboundTcpConfig, WasiKind,
};
#[allow(unused_imports)]
pub use filter_entry::{AllowDest, RateLimitConfig, SchemeKind, TcpAllowDest};
pub use listen::{ClientAuth, Listen, ProxyProtocolTrust};
#[allow(unused_imports)]
pub use listen::{Drain, ProxyProtocol};
pub use observability::Observability;
pub use resumption::Resumption;
pub(crate) use route::MAX_BACKEND_WEIGHT;
pub use route::{CompressionAlgorithm, RateLimitKeyKind, Route, RouteCompression, RouteRateLimit};
#[allow(unused_imports)]
pub use route::{Backend, RouteMatch, RouteUpgrade};
pub use state::{State, StateBackendKind};
pub use tls::TlsCert;
pub use trust::Trust;
pub use upstream::{
CircuitBreaker, HashKeyKind, HealthConfig, LbAlgorithm, OutlierDetection, Upstream, UpstreamTls,
};
#[allow(unused_imports)]
pub use upstream::{AddressSpec, HashConfig, WeightedAddress};
pub(crate) use upstream::{MAX_HASH_TABLE_SIZE, MAX_INSTANCE_WEIGHT};
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
#[serde(default)]
pub trust: Trust,
#[serde(default)]
pub state: State,
#[serde(default, rename = "filter")]
pub filters: Vec<FilterEntry>,
#[serde(default)]
pub chain: Chain,
#[serde(default, rename = "upstream")]
pub upstreams: Vec<Upstream>,
#[serde(default, rename = "route")]
pub routes: Vec<Route>,
#[serde(default, rename = "tls")]
pub tls: Vec<TlsCert>,
#[serde(default)]
pub resumption: Option<Resumption>,
#[serde(default, skip_serializing)]
pub observability: Observability,
#[serde(default, skip_serializing_if = "Listen::content_hash_exempt")]
pub listen: Listen,
}
impl Manifest {
pub fn from_toml(s: &str) -> Result<Self, ControlError> {
Ok(toml::from_str(s)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_routes_and_upstreams() {
let m = Manifest::from_toml(
r#"
[[filter]]
id = "auth"
source = "artifacts/auth"
digest = "sha256:abc"
[[upstream]]
name = "api-svc"
addresses = ["127.0.0.1:9000", "127.0.0.1:9001"]
[upstream.health]
path = "/healthz"
[[route]]
filters = ["auth"]
upstream = "api-svc"
strip_prefix = "/api"
[route.match]
host = "example.com"
path_prefix = "/api"
"#,
)
.unwrap();
assert_eq!(m.upstreams.len(), 1);
let addrs: Vec<&str> = m.upstreams[0]
.addresses
.iter()
.map(|a| a.address())
.collect();
assert_eq!(addrs, vec!["127.0.0.1:9000", "127.0.0.1:9001"]);
assert!(
m.upstreams[0].addresses.iter().all(|a| a.weight() == 1),
"bare addresses default to weight 1"
);
assert_eq!(m.upstreams[0].health.path, "/healthz");
assert_eq!(m.upstreams[0].health.interval_ms, 2000);
assert_eq!(m.upstreams[0].health.timeout_ms, 1000);
assert_eq!(m.upstreams[0].health.healthy_threshold, 2);
assert_eq!(m.upstreams[0].health.unhealthy_threshold, 3);
assert_eq!(m.routes.len(), 1);
let r = &m.routes[0];
assert_eq!(r.matcher.host.as_deref(), Some("example.com"));
assert_eq!(r.matcher.path_prefix, "/api");
assert_eq!(r.filters, vec!["auth".to_string()]);
assert_eq!(r.upstream.as_deref(), Some("api-svc"));
assert!(r.backends.is_empty(), "single upstream uses the shorthand");
assert_eq!(r.strip_prefix.as_deref(), Some("/api"));
let mut m2 = m.clone();
m2.routes[0].matcher.path_prefix = "/v2".to_string();
assert_ne!(
m.content_hash().unwrap(),
m2.content_hash().unwrap(),
"a route change must flip the content hash"
);
}
#[test]
fn rejects_unknown_fields() {
let parsed = Manifest::from_toml(
r#"
[[filter]]
id = "x"
source = "s"
digest = "sha256:abc"
typo_field = true
"#,
);
assert!(parsed.is_err(), "deny_unknown_fields should reject a typo");
}
}