use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use axum::extract::RawPathParams;
use axum::extract::rejection::RawPathParamsRejection;
use axum::response::IntoResponse as _;
use http::header::{ALLOW, CONTENT_TYPE};
use http::{HeaderMap, HeaderValue, StatusCode};
use super::generated::admin::server::AdminApi;
use super::generated::definition::server::DefinitionApi;
use super::generated::demographic::server::DemographicApi;
use super::generated::ehr::server::EhrApi;
use super::generated::query::server::QueryApi;
use super::generated::system::server::SystemApi;
use super::generated::{admin, definition, demographic, ehr, query, system};
use super::routes::{Lookup, lookup};
use super::runtime::{
ApiError, HeaderError, Refusal, ResponseHeaders, error_response, merge_headers,
};
const CANONICAL_JSON: &str = "application/json";
fn parse<T>(location: &str, name: &str, raw: &str) -> Result<T, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
raw.parse::<T>().map_err(|error| {
ApiError::BadRequest(format!(
"the {location} parameter `{name}` is not a valid value: {error}"
))
})
}
fn missing(location: &str, name: &str) -> ApiError {
ApiError::BadRequest(format!(
"the required {location} parameter `{name}` is missing"
))
}
pub(crate) struct PathCaptures(RawPathParams);
impl PathCaptures {
pub(crate) fn new(
captured: Result<RawPathParams, RawPathParamsRejection>,
) -> Result<Self, ApiError> {
captured.map(Self).map_err(|rejection| {
ApiError::BadRequest(format!("a path segment is not valid: {rejection}"))
})
}
pub(crate) fn value<T>(&self, capture: &str, name: &str) -> Result<T, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
let raw = self
.0
.iter()
.find_map(|(key, value)| (key == capture).then_some(value))
.ok_or_else(|| missing("path", name))?;
parse("path", name, raw)
}
}
pub(crate) struct QueryPairs(Vec<(String, String)>);
impl QueryPairs {
pub(crate) fn parse(query: Option<&str>) -> Result<Self, ApiError> {
let mut pairs = Vec::new();
for pair in query.unwrap_or_default().split('&') {
if pair.is_empty() {
continue;
}
let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
let decode = |text: &str| {
urlencoding::decode(text)
.map(std::borrow::Cow::into_owned)
.map_err(|_not_utf8| {
ApiError::BadRequest(
"the query string does not percent-decode to UTF-8 text".to_owned(),
)
})
};
pairs.push((decode(name)?, decode(value)?));
}
Ok(Self(pairs))
}
fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
self.0
.iter()
.filter(move |(key, _)| key == name)
.map(|(_, value)| value.as_str())
}
pub(crate) fn optional<T>(&self, name: &str) -> Result<Option<T>, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
let mut values = self.values(name);
let Some(first) = values.next() else {
return Ok(None);
};
if values.next().is_some() {
return Err(ApiError::BadRequest(format!(
"the query parameter `{name}` is given more than once"
)));
}
parse("query", name, first).map(Some)
}
pub(crate) fn required<T>(&self, name: &str) -> Result<T, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
self.optional(name)?.ok_or_else(|| missing("query", name))
}
pub(crate) fn members<M, V>(&self, name: &str, declared: &[&str]) -> Result<Option<M>, ApiError>
where
M: FromIterator<(String, V)>,
V: serde::de::DeserializeOwned + From<String>,
{
let mut seen: Vec<&str> = Vec::new();
let mut members: Vec<(String, V)> = Vec::new();
for (key, value) in &self.0 {
if declared.contains(&key.as_str()) {
continue;
}
if seen.contains(&key.as_str()) {
return Err(ApiError::BadRequest(format!(
"the member `{key}` of the query parameter `{name}` is given more than once"
)));
}
seen.push(key);
members.push((key.clone(), member_value(value)));
}
Ok((!members.is_empty()).then(|| members.into_iter().collect()))
}
}
fn member_value<V>(raw: &str) -> V
where
V: serde::de::DeserializeOwned + From<String>,
{
if raw.starts_with('"') {
return V::from(raw.to_owned());
}
serde_json::from_str::<V>(raw).unwrap_or_else(|_not_json| V::from(raw.to_owned()))
}
fn header_text(headers: &HeaderMap, name: &str) -> Result<Option<String>, ApiError> {
let mut joined: Option<String> = None;
for line in header_lines(headers, name)? {
match joined.as_mut() {
Some(text) => {
text.push_str(", ");
text.push_str(&line);
}
None => joined = Some(line),
}
}
Ok(joined)
}
fn header_lines(headers: &HeaderMap, name: &str) -> Result<Vec<String>, ApiError> {
headers
.get_all(name)
.iter()
.map(|value| {
value.to_str().map(str::to_owned).map_err(|_opaque| {
ApiError::BadRequest(format!("the header `{name}` is not visible ASCII text"))
})
})
.collect()
}
pub(crate) fn header_optional<T>(headers: &HeaderMap, name: &str) -> Result<Option<T>, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
header_text(headers, name)?
.map(|text| parse("header", name, &text))
.transpose()
}
pub(crate) fn header_required<T>(headers: &HeaderMap, name: &str) -> Result<T, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
header_optional(headers, name)?.ok_or_else(|| missing("header", name))
}
pub(crate) fn header_list<T>(headers: &HeaderMap, name: &str) -> Result<Option<Vec<T>>, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
let lines = header_lines(headers, name)?;
if lines.is_empty() {
return Ok(None);
}
lines
.iter()
.map(|line| parse("header", name, line))
.collect::<Result<Vec<T>, ApiError>>()
.map(Some)
}
fn admit_json(headers: &HeaderMap) -> Result<(), ApiError> {
let Some(value) = headers.get(CONTENT_TYPE) else {
return Ok(());
};
let refused = || {
ApiError::UnsupportedMediaType(format!(
"this operation reads canonical JSON, not `{}`",
String::from_utf8_lossy(value.as_bytes())
))
};
let text = value.to_str().map_err(|_opaque| refused())?;
let media = text.split(';').next().unwrap_or_default().trim();
if media.eq_ignore_ascii_case(CANONICAL_JSON) {
Ok(())
} else {
Err(refused())
}
}
fn is_blank(body: &[u8]) -> bool {
body.iter().all(u8::is_ascii_whitespace)
}
fn utf8(body: &[u8]) -> Result<&str, ApiError> {
std::str::from_utf8(body)
.map_err(|_not_utf8| ApiError::BadRequest("the request body is not UTF-8 text".to_owned()))
}
pub(crate) fn json_body<T: serde::de::DeserializeOwned>(
headers: &HeaderMap,
body: &[u8],
) -> Result<T, ApiError> {
admit_json(headers)?;
if is_blank(body) {
return Err(ApiError::BadRequest(
"this operation requires a request body".to_owned(),
));
}
crate::json::from_canonical_json(utf8(body)?).map_err(|error| {
ApiError::BadRequest(format!(
"the request body is not the documented shape: {error}"
))
})
}
pub(crate) fn json_body_optional<T: serde::de::DeserializeOwned>(
headers: &HeaderMap,
body: &[u8],
) -> Result<Option<T>, ApiError> {
if is_blank(body) {
return Ok(None);
}
json_body(headers, body).map(Some)
}
pub(crate) fn text_body(body: &[u8]) -> Result<String, ApiError> {
if body.is_empty() {
return Err(ApiError::BadRequest(
"this operation requires a request body".to_owned(),
));
}
utf8(body).map(str::to_owned)
}
pub(crate) struct Reply {
status: StatusCode,
headers: HeaderMap,
body: Vec<u8>,
}
impl Reply {
pub(crate) fn new(status: StatusCode) -> Self {
Self {
status,
headers: HeaderMap::new(),
body: Vec::new(),
}
}
pub(crate) fn json<B: serde::Serialize + ?Sized>(&mut self, body: &B) -> Result<(), ApiError> {
self.body = match serde_json::to_vec(body) {
Ok(bytes) => bytes,
Err(_) => {
return Err(ApiError::Internal(
"the answer body does not serialize".to_owned(),
));
}
};
self.headers
.insert(CONTENT_TYPE, HeaderValue::from_static(CANONICAL_JSON));
Ok(())
}
pub(crate) fn raw(&mut self, body: Vec<u8>) {
self.body = body;
}
pub(crate) fn headers(&mut self, headers: impl ResponseHeaders) -> Result<(), HeaderError> {
merge_headers(&mut self.headers, headers.into_header_map()?);
Ok(())
}
pub(crate) fn finish(self) -> axum::response::Response {
let mut response = axum::response::Response::new(axum::body::Body::from(self.body));
*response.status_mut() = self.status;
*response.headers_mut() = self.headers;
response
}
}
pub(crate) fn respond(
served: Result<axum::response::Response, Refusal>,
) -> axum::response::Response {
match served {
Ok(response) => response,
Err(refusal) => refusal.into_response(),
}
}
pub fn router<S>(api: Arc<S>) -> axum::Router
where
S: AdminApi + DefinitionApi + DemographicApi + EhrApi + QueryApi + SystemApi,
S: Send + Sync + 'static,
{
with_fallbacks(
admin::server::router(Arc::clone(&api))
.merge(definition::server::router(Arc::clone(&api)))
.merge(demographic::server::router(Arc::clone(&api)))
.merge(ehr::server::router(Arc::clone(&api)))
.merge(query::server::router(Arc::clone(&api)))
.merge(system::server::router(api)),
)
}
pub fn with_fallbacks(router: axum::Router) -> axum::Router {
router
.fallback(not_found)
.method_not_allowed_fallback(method_not_allowed)
}
async fn not_found(uri: http::Uri) -> axum::response::Response {
error_response(
StatusCode::NOT_FOUND,
format!("no ITS-REST operation has the path `{}`", uri.path()),
Vec::new(),
)
}
async fn method_not_allowed(method: http::Method, uri: http::Uri) -> axum::response::Response {
let mut response = error_response(
StatusCode::METHOD_NOT_ALLOWED,
format!("the operations at `{}` do not take {method}", uri.path()),
Vec::new(),
);
if let Lookup::MethodNotAllowed { allowed } = lookup(&method, uri.path())
&& let Ok(value) = HeaderValue::from_str(&allowed.join(", "))
{
response.headers_mut().insert(ALLOW, value);
}
response
}