use std::fmt;
use std::str::FromStr;
use http::{HeaderMap, HeaderValue};
use super::routes::RouteMatch;
use super::runtime::{ApiError, Payload};
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 PathValues(Vec<(&'static str, String)>);
impl PathValues {
#[cfg(feature = "rest-server")]
pub(crate) fn new(pairs: Vec<(&'static str, String)>) -> Self {
Self(pairs)
}
pub(crate) fn from_route(matched: &RouteMatch) -> Result<Self, ApiError> {
matched
.path_params
.iter()
.map(|param| {
param
.decoded()
.map(|value| (param.name, value))
.map_err(|_not_utf8| {
ApiError::BadRequest(format!(
"the path parameter `{}` does not percent-decode to UTF-8 text",
param.name
))
})
})
.collect::<Result<Vec<_>, ApiError>>()
.map(Self)
}
pub(crate) fn value<T>(&self, name: &str) -> Result<T, ApiError>
where
T: FromStr,
T::Err: fmt::Display,
{
let raw = self
.0
.iter()
.find_map(|(key, value)| (*key == name).then_some(value))
.ok_or_else(|| missing("path", name))?;
parse("path", name, raw)
}
}
#[must_use]
pub fn query_component_octets(text: &str) -> Vec<u8> {
urlencoding::decode_binary(text.as_bytes()).into_owned()
}
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| {
String::from_utf8(query_component_octets(text)).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(
serde_json::from_str::<String>(raw).unwrap_or_else(|_not_json| 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(content_type: Option<&HeaderValue>) -> Result<(), ApiError> {
let Some(value) = 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>(
content_type: Option<&HeaderValue>,
body: &[u8],
) -> Result<T, ApiError> {
admit_json(content_type)?;
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}"
))
})
}
const SIMPLIFIED_FLAT: &str = "application/openehr.wt.flat+json";
const SIMPLIFIED_STRUCTURED: &str = "application/openehr.wt.structured+json";
pub(crate) fn payload_body<C, S>(
content_type: Option<&HeaderValue>,
body: &[u8],
) -> Result<Payload<C, S>, ApiError>
where
C: serde::de::DeserializeOwned,
S: serde::de::DeserializeOwned,
{
let media = content_type
.and_then(|value| value.to_str().ok())
.map(|text| {
text.split(';')
.next()
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
});
let simplified = |body: &[u8]| -> Result<S, ApiError> {
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}"
))
})
};
match media.as_deref() {
Some(SIMPLIFIED_FLAT) => simplified(body).map(Payload::Flat),
Some(SIMPLIFIED_STRUCTURED) => simplified(body).map(Payload::Structured),
_ => json_body(content_type, body).map(Payload::Canonical),
}
}
pub(crate) fn payload_body_optional<C, S>(
content_type: Option<&HeaderValue>,
body: &[u8],
) -> Result<Option<Payload<C, S>>, ApiError>
where
C: serde::de::DeserializeOwned,
S: serde::de::DeserializeOwned,
{
if is_blank(body) {
return Ok(None);
}
payload_body(content_type, 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)
}
#[cfg(test)]
mod tests {
use super::member_value;
#[test]
fn a_quoted_member_is_the_json_string_it_denotes() {
assert_eq!(member_value::<String>(r#""123""#), "123");
assert_eq!(member_value::<String>(r#""a\"b""#), "a\"b");
assert_eq!(member_value::<String>(r#""""#), "");
}
#[test]
fn an_unquoted_member_is_its_text() {
assert_eq!(member_value::<String>("abc::x::1"), "abc::x::1");
assert_eq!(member_value::<String>("123"), "123");
}
#[test]
fn a_malformed_quoted_member_keeps_its_text() {
assert_eq!(member_value::<String>(r#""abc"#), "\"abc");
assert_eq!(member_value::<String>(r#""a" b"#), "\"a\" b");
}
}