use std::collections::BTreeMap;
use roas::common::reference::RefOr;
use roas::v3_2::media_type::{Encoding, MediaType};
use roas::v3_2::request_body::RequestBody;
use roas::v3_2::schema::{Schema, SingleSchema};
use roas::v3_2::spec::Spec;
use serde_json::Value;
use crate::decoder::Decoders;
use crate::parameter::is_json;
use crate::report::{ErrorKind, Location, ValidationError};
use crate::request::RequestView;
use crate::schema;
pub(crate) fn validate(
request_body: &RequestBody,
request: &RequestView<'_>,
spec: &Spec,
decoders: &Decoders,
errors: &mut Vec<ValidationError>,
) {
let mut push = |pointer: String, kind: ErrorKind| {
errors.push(ValidationError {
location: Location::Body,
name: String::new(),
pointer,
kind,
});
};
let sent = request.content_type();
let Some(bytes) = request.body.as_deref() else {
if request_body.required == Some(true) {
push(String::new(), ErrorKind::Missing);
}
return;
};
let Some((media_type, entry)) = select(&request_body.content, sent.as_deref()) else {
push(
String::new(),
ErrorKind::UnexpectedMediaType {
got: sent,
expected: request_body.content.keys().cloned().collect(),
},
);
return;
};
let entry = match entry.get_item(spec) {
Ok(entry) => entry,
Err(error) => {
push(
String::new(),
ErrorKind::UnresolvedReference(error.to_string()),
);
return;
}
};
let Some(declared) = &entry.schema else {
return;
};
let sent_header = request.header("content-type").unwrap_or(&media_type);
let value = match decode(
bytes,
&media_type,
sent_header,
declared,
entry.encoding.as_ref(),
spec,
decoders,
) {
Ok(value) => value,
Err(Decoded::Malformed(why)) => {
push(String::new(), ErrorKind::Malformed(why));
return;
}
Err(Decoded::Unsupported(what)) => {
push(String::new(), ErrorKind::Unsupported(what));
return;
}
};
for failure in schema::check(&value, declared, spec) {
push(
failure.pointer,
match failure.kind {
schema::FailureKind::Unresolved => ErrorKind::UnresolvedReference(failure.message),
schema::FailureKind::Unchecked => ErrorKind::Unchecked(failure.message),
schema::FailureKind::Violated => ErrorKind::Schema(failure.message),
},
);
}
}
pub(crate) enum Decoded {
Malformed(String),
Unsupported(String),
}
fn select<'c>(
content: &'c BTreeMap<String, RefOr<MediaType>>,
sent: Option<&str>,
) -> Option<(String, &'c RefOr<MediaType>)> {
let sent = sent?;
let mut ranges: Vec<(String, &RefOr<MediaType>)> = Vec::new();
for (key, entry) in content {
let key_media_type = key
.split(';')
.next()
.unwrap_or(key)
.trim()
.to_ascii_lowercase();
if key_media_type == sent {
return Some((sent.to_owned(), entry));
}
if key_media_type.ends_with("/*") || key_media_type == "*/*" {
ranges.push((key_media_type, entry));
}
}
ranges.sort_by_key(|(key, _)| std::cmp::Reverse(key.len()));
ranges.into_iter().find_map(|(key, entry)| {
let matches = key == "*/*"
|| sent
.split_once('/')
.is_some_and(|(kind, _)| key == format!("{kind}/*"));
matches.then_some((sent.to_owned(), entry))
})
}
pub(crate) fn decode(
bytes: &[u8],
media_type: &str,
content_type: &str,
declared: &RefOr<Schema>,
encoding: Option<&BTreeMap<String, Encoding>>,
spec: &Spec,
decoders: &Decoders,
) -> Result<Value, Decoded> {
if let Some(decoder) = decoders.find(media_type) {
return decoder(bytes, content_type).map_err(Decoded::Malformed);
}
if is_json(media_type) {
return serde_json::from_slice(bytes)
.map_err(|error| Decoded::Malformed(format!("invalid JSON: {error}")));
}
if media_type == "application/x-www-form-urlencoded" {
let text = as_text(bytes)?;
return crate::parameter::read_form_body(
&text,
object_properties(declared, spec),
encoding,
spec,
)
.map_err(Decoded::Malformed);
}
if media_type.starts_with("text/") {
return Ok(Value::String(as_text(bytes)?));
}
Err(Decoded::Unsupported(format!("a {media_type} body")))
}
fn as_text(bytes: &[u8]) -> Result<String, Decoded> {
String::from_utf8(bytes.to_vec())
.map_err(|error| Decoded::Malformed(format!("is not UTF-8: {error}")))
}
fn object_properties<'s>(
schema: &'s RefOr<Schema>,
spec: &'s Spec,
) -> Option<&'s BTreeMap<String, RefOr<Schema>>> {
match schema.get_item(spec).ok()? {
Schema::Single(single) => match single.as_ref() {
SingleSchema::Object(object) => object.properties.as_ref(),
_ => None,
},
_ => None,
}
}