use http::Method;
use super::generated::{admin, definition, demographic, ehr, query, system};
type RouteTable = &'static [(&'static str, &'static str, &'static str)];
const GROUPS: &[(&str, RouteTable)] = &[
("admin", admin::ROUTES),
("definition", definition::ROUTES),
("demographic", demographic::ROUTES),
("ehr", ehr::ROUTES),
("query", query::ROUTES),
("system", system::ROUTES),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Lookup {
Matched(RouteMatch),
MethodNotAllowed {
allowed: Vec<&'static str>,
},
NotFound,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteMatch {
pub group: &'static str,
pub operation_id: &'static str,
pub template: &'static str,
pub method: Method,
pub path_params: Vec<PathParam>,
}
impl RouteMatch {
#[must_use]
pub fn path_param(&self, name: &str) -> Option<&PathParam> {
self.path_params.iter().find(|param| param.name == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathParam {
pub name: &'static str,
pub raw: String,
}
impl PathParam {
pub fn decoded(&self) -> Result<String, std::string::FromUtf8Error> {
urlencoding::decode(&self.raw).map(std::borrow::Cow::into_owned)
}
}
#[derive(Clone, Copy)]
enum Segment<'a> {
Literal(&'a str),
Param(&'a str),
}
fn template_segments(template: &str) -> Vec<Segment<'_>> {
let path = template.split("{?").next().unwrap_or(template);
path.strip_prefix('/')
.unwrap_or(path)
.split('/')
.map(|segment| {
segment
.strip_prefix('{')
.and_then(|rest| rest.strip_suffix('}'))
.map_or(Segment::Literal(segment), Segment::Param)
})
.collect()
}
fn specificity(segments: &[Segment<'_>]) -> Vec<bool> {
segments
.iter()
.map(|segment| matches!(segment, Segment::Literal(_)))
.collect()
}
fn matches_path(segments: &[Segment<'_>], parts: &[&str]) -> bool {
segments.len() == parts.len()
&& segments
.iter()
.zip(parts)
.all(|(segment, part)| match segment {
Segment::Literal(literal) => literal == part,
Segment::Param(_) => !part.is_empty(),
})
}
#[must_use]
pub fn lookup(method: &Method, path: &str) -> Lookup {
let path = path.split('?').next().unwrap_or(path);
let parts: Vec<&str> = path.strip_prefix('/').unwrap_or(path).split('/').collect();
let candidates: Vec<Candidate> = GROUPS
.iter()
.flat_map(|&(group, routes)| {
routes
.iter()
.map(move |&(method, template, operation_id)| Candidate {
group,
method,
template,
operation_id,
segments: template_segments(template),
})
})
.filter(|candidate| matches_path(&candidate.segments, &parts))
.collect();
let Some(best) = candidates
.iter()
.map(|candidate| specificity(&candidate.segments))
.max()
else {
return Lookup::NotFound;
};
let resource: Vec<Candidate> = candidates
.into_iter()
.filter(|candidate| specificity(&candidate.segments) == best)
.collect();
let Some(chosen) = resource
.iter()
.find(|candidate| candidate.method == method.as_str())
else {
let mut allowed: Vec<&'static str> = Vec::new();
for candidate in &resource {
if !allowed.contains(&candidate.method) {
allowed.push(candidate.method);
}
}
return Lookup::MethodNotAllowed { allowed };
};
let path_params = chosen
.segments
.iter()
.zip(&parts)
.filter_map(|(segment, part)| match *segment {
Segment::Param(name) => Some(PathParam {
name,
raw: (*part).to_owned(),
}),
Segment::Literal(_) => None,
})
.collect();
Lookup::Matched(RouteMatch {
group: chosen.group,
operation_id: chosen.operation_id,
template: chosen.template,
method: method.clone(),
path_params,
})
}
struct Candidate {
group: &'static str,
method: &'static str,
template: &'static str,
operation_id: &'static str,
segments: Vec<Segment<'static>>,
}