use http::Method;
use super::generated::{admin, definition, demographic, ehr, query, system};
type RouteTable = &'static [(&'static str, &'static str, &'static str)];
type ParamTable = &'static [&'static [Param]];
type MediaTable = &'static [&'static [&'static str]];
const GROUPS: &[(&str, RouteTable, ParamTable, MediaTable)] = &[
(
"admin",
admin::ROUTES,
admin::ROUTE_PARAMS,
admin::ROUTE_REQUEST_MEDIA,
),
(
"definition",
definition::ROUTES,
definition::ROUTE_PARAMS,
definition::ROUTE_REQUEST_MEDIA,
),
(
"demographic",
demographic::ROUTES,
demographic::ROUTE_PARAMS,
demographic::ROUTE_REQUEST_MEDIA,
),
(
"ehr",
ehr::ROUTES,
ehr::ROUTE_PARAMS,
ehr::ROUTE_REQUEST_MEDIA,
),
(
"query",
query::ROUTES,
query::ROUTE_PARAMS,
query::ROUTE_REQUEST_MEDIA,
),
(
"system",
system::ROUTES,
system::ROUTE_PARAMS,
system::ROUTE_REQUEST_MEDIA,
),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Param {
pub name: &'static str,
pub location: ParamLocation,
pub required: bool,
pub explode: bool,
pub kind: ParamKind,
pub identifier: Option<IdentifierClass>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IdentifierClass {
HierObject,
ObjectVersion,
UidBased,
}
impl IdentifierClass {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::HierObject => "HIER_OBJECT_ID",
Self::ObjectVersion => "OBJECT_VERSION_ID",
Self::UidBased => "UID_BASED_ID",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ParamLocation {
Path,
Query,
Header,
Cookie,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParamKind {
Enum(&'static [&'static str]),
Text,
Uuid,
Date,
DateTime,
Formatted(&'static str),
Integer,
Number,
Boolean,
Object,
Array(&'static ParamKind),
Unspecified,
}
#[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>,
pub params: &'static [Param],
pub request_media: &'static [&'static str],
}
impl RouteMatch {
#[must_use]
pub fn path_param(&self, name: &str) -> Option<&PathParam> {
self.path_params.iter().find(|param| param.name == name)
}
#[must_use]
pub fn query_param(&self, name: &str) -> Option<&'static Param> {
self.declared(ParamLocation::Query)
.find(|param| param.name == name)
}
#[must_use]
pub fn header_param(&self, name: &str) -> Option<&'static Param> {
self.declared(ParamLocation::Header)
.find(|param| param.name.eq_ignore_ascii_case(name))
}
#[must_use]
pub fn query_key(&self, key: &str) -> Option<&'static Param> {
self.query_param(key).or_else(|| {
self.declared(ParamLocation::Query)
.find(|param| param.explode && param.kind == ParamKind::Object)
})
}
fn declared(&self, location: ParamLocation) -> impl Iterator<Item = &'static Param> {
self.params
.iter()
.filter(move |param| param.location == location)
}
}
#[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, params, media)| {
routes.iter().zip(params).zip(media).map(
move |((&(method, template, operation_id), ¶ms), &request_media)| Candidate {
group,
method,
template,
operation_id,
params,
request_media,
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,
params: chosen.params,
request_media: chosen.request_media,
})
}
struct Candidate {
group: &'static str,
method: &'static str,
template: &'static str,
operation_id: &'static str,
params: &'static [Param],
request_media: &'static [&'static str],
segments: Vec<Segment<'static>>,
}