use std::collections::BTreeMap;
use crate::{DeclaredOp, RouteDecl};
#[derive(Debug, Default)]
pub struct Reconciliation {
pub missing_handler: Vec<DeclaredOp>,
pub missing_spec: Vec<RouteDecl>,
pub mismatched: Vec<(RouteDecl, DeclaredOp, String)>,
pub matched: usize,
}
impl Reconciliation {
pub fn is_clean(&self) -> bool {
self.missing_handler.is_empty()
&& self.missing_spec.is_empty()
&& self.mismatched.is_empty()
}
}
pub fn reconcile(declared: &[DeclaredOp], routes: &[RouteDecl]) -> Reconciliation {
let mut rec = Reconciliation::default();
let by_op: BTreeMap<&str, &DeclaredOp> =
declared.iter().map(|d| (d.operation.as_str(), d)).collect();
for route in routes {
match by_op.get(route.operation.as_str()) {
None => rec.missing_spec.push(route.clone()),
Some(decl) => {
let mut problems = Vec::new();
if !route.method.is_empty() && !route.method.eq_ignore_ascii_case(&decl.method) {
problems.push(format!("method {} != spec {}", route.method, decl.method));
}
if !route.path.is_empty() && route.path != decl.path {
problems.push(format!("path {} != spec {}", route.path, decl.path));
}
if problems.is_empty() {
rec.matched += 1;
} else {
rec.mismatched
.push((route.clone(), (*decl).clone(), problems.join("; ")));
}
}
}
}
let implemented: std::collections::BTreeSet<&str> =
routes.iter().map(|r| r.operation.as_str()).collect();
for decl in declared {
if !implemented.contains(decl.operation.as_str()) {
rec.missing_handler.push(decl.clone());
}
}
rec
}
#[cfg(test)]
mod tests {
use super::*;
fn op(operation: &str, method: &str, path: &str) -> DeclaredOp {
DeclaredOp {
operation: operation.into(),
method: method.into(),
path: path.into(),
}
}
fn route(operation: &str, method: &str, path: &str) -> RouteDecl {
RouteDecl {
id: format!("app::{operation}"),
operation: operation.into(),
method: method.into(),
path: path.into(),
}
}
#[test]
fn clean_when_everything_matches() {
let declared = vec![op("getItem", "GET", "/items/{id}")];
let routes = vec![route("getItem", "GET", "/items/{id}")];
let rec = reconcile(&declared, &routes);
assert!(rec.is_clean());
assert_eq!(rec.matched, 1);
}
#[test]
fn catches_all_three_drift_kinds() {
let declared = vec![
op("getItem", "GET", "/items/{id}"),
op("listItems", "GET", "/items"),
];
let routes = vec![
route("getItem", "POST", "/items/{id}"), route("ghost", "GET", "/ghost"), ];
let rec = reconcile(&declared, &routes);
assert_eq!(rec.mismatched.len(), 1);
assert_eq!(rec.missing_spec.len(), 1);
assert_eq!(rec.missing_handler.len(), 1); }
#[test]
fn empty_route_fields_skip_cross_checks() {
let declared = vec![op("item", "QUERY", "item")];
let routes = vec![route("item", "", "")];
assert!(reconcile(&declared, &routes).is_clean());
}
}