use std::collections::BTreeMap;
use roas::common::bool_or::BoolOr;
use roas::common::formats::SchemaType;
use roas::common::reference::RefOr;
use roas::v3_2::media_type::{Encoding, MediaType};
use roas::v3_2::parameter::{InCookieStyle, InHeaderStyle, InPathStyle, InQueryStyle, Parameter};
use roas::v3_2::schema::{Schema, SingleSchema};
use roas::v3_2::spec::Spec;
use serde_json::Value;
use crate::body;
use crate::report::{ErrorKind, Location, ValidationError};
use crate::request::{RequestView, decode_form, decode_path_segment, split_query};
use crate::schema;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Style {
Matrix,
Label,
Simple,
Form,
SpaceDelimited,
PipeDelimited,
DeepObject,
}
struct Described<'p> {
name: &'p str,
location: Location,
required: bool,
style: Style,
explode: bool,
schema: Option<&'p RefOr<Schema>>,
content: Option<&'p BTreeMap<String, RefOr<MediaType>>>,
}
enum Shape<'s> {
Primitive(Primitive),
Array(Option<&'s RefOr<Schema>>),
Object(Option<&'s BTreeMap<String, RefOr<Schema>>>),
Opaque,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Primitive {
String,
Integer,
Number,
Boolean,
Null,
}
pub(crate) struct Extracted<'r> {
pub(crate) path: &'r BTreeMap<String, String>,
pub(crate) query: Vec<(String, String)>,
pub(crate) cookies: Vec<(String, String)>,
}
impl<'r> Extracted<'r> {
pub(crate) fn new(request: &RequestView<'_>, path: &'r BTreeMap<String, String>) -> Self {
Self {
path,
query: request.query_pairs_raw(),
cookies: request.cookies(),
}
}
}
pub(crate) fn read_form_body(
text: &str,
properties: Option<&BTreeMap<String, RefOr<Schema>>>,
encoding: Option<&BTreeMap<String, Encoding>>,
spec: &Spec,
) -> Result<Value, String> {
let pairs = split_query(text);
let mut object = serde_json::Map::new();
let mut fields = Vec::new();
if let Some(properties) = properties {
for (name, schema) in properties {
let field =
Described::form_field(name, encoding.and_then(|e| e.get(name)), Some(schema));
if let Some(value) = field.read_form_field(&pairs, spec)? {
object.insert(name.clone(), value);
}
fields.push(field);
}
}
for (name, value) in &pairs {
if object.contains_key(name) || fields.iter().any(|field| field.accounts_for(name, spec)) {
continue;
}
object.insert(name.clone(), Value::String(decode_form(value)));
}
Ok(Value::Object(object))
}
pub(crate) fn accounts_for(parameter: &Parameter, name: &str, spec: &Spec) -> bool {
Described::of(parameter).accounts_for(name, spec)
}
pub(crate) fn validate(
parameter: &Parameter,
request: &RequestView<'_>,
extracted: &Extracted<'_>,
spec: &Spec,
errors: &mut Vec<ValidationError>,
) {
let described = Described::of(parameter);
let mut push = |pointer: String, kind: ErrorKind| {
errors.push(ValidationError {
location: described.location,
name: described.name.to_owned(),
pointer,
kind,
});
};
if let Some(content) = described.content {
let Some(raw) = described.raw_text(request, extracted) else {
if described.required {
push(String::new(), ErrorKind::Missing);
}
return;
};
validate_as_content(&raw, content, spec, &mut push);
return;
}
let shape = described
.schema
.map_or(Shape::Opaque, |schema| Shape::of(schema, spec));
let value = match described.read(request, extracted, &shape, spec) {
Ok(Some(value)) => value,
Ok(None) => {
if described.required {
push(String::new(), ErrorKind::Missing);
}
return;
}
Err(why) => {
push(String::new(), ErrorKind::Malformed(why));
return;
}
};
let Some(declared) = described.schema else {
return;
};
report_failures(&value, declared, spec, &mut push);
}
fn validate_as_content(
raw: &str,
content: &BTreeMap<String, RefOr<MediaType>>,
spec: &Spec,
push: &mut impl FnMut(String, ErrorKind),
) {
let Some((media_type, entry)) = content.iter().next() else {
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 value = match body::decode(
raw.as_bytes(),
media_type,
declared,
entry.encoding.as_ref(),
spec,
) {
Ok(value) => value,
Err(body::Decoded::Malformed(why)) => {
push(String::new(), ErrorKind::Malformed(why));
return;
}
Err(body::Decoded::Unsupported(what)) => {
push(String::new(), ErrorKind::Unsupported(what));
return;
}
};
report_failures(&value, declared, spec, push);
}
fn report_failures(
value: &Value,
declared: &RefOr<Schema>,
spec: &Spec,
push: &mut impl FnMut(String, ErrorKind),
) {
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) fn is_json(media_type: &str) -> bool {
let media_type = media_type.trim().to_ascii_lowercase();
media_type == "application/json"
|| media_type.ends_with("+json")
|| media_type.starts_with("application/json;")
}
impl<'p> Described<'p> {
fn form_field(
name: &'p str,
encoding: Option<&Encoding>,
schema: Option<&'p RefOr<Schema>>,
) -> Self {
let style = match encoding.and_then(|encoding| encoding.style.as_ref()) {
Some(InQueryStyle::SpaceDelimited) => Style::SpaceDelimited,
Some(InQueryStyle::PipeDelimited) => Style::PipeDelimited,
Some(InQueryStyle::DeepObject) => Style::DeepObject,
Some(InQueryStyle::Form) | None => Style::Form,
};
Self {
name,
location: Location::Query,
required: false,
style,
explode: encoding
.and_then(|encoding| encoding.explode)
.unwrap_or(style == Style::Form),
schema,
content: None,
}
}
fn deep_object_key<'n>(&self, name: &'n str) -> Option<&'n str> {
name.strip_prefix(self.name)?
.strip_prefix('[')?
.strip_suffix(']')
}
fn accounts_for(&self, name: &str, spec: &Spec) -> bool {
if self.location != Location::Query {
return false;
}
if self.style == Style::DeepObject {
return self.deep_object_key(name).is_some();
}
if self.explode
&& self.style == Style::Form
&& let Some(schema) = self.schema
&& let Shape::Object(Some(properties)) = Shape::of(schema, spec)
{
return properties.contains_key(name);
}
name == self.name
}
fn read_form_field(
&self,
pairs: &[(String, String)],
spec: &Spec,
) -> Result<Option<Value>, String> {
let shape = self
.schema
.map_or(Shape::Opaque, |schema| Shape::of(schema, spec));
self.read_query(pairs, &shape, spec)
}
fn of(parameter: &'p Parameter) -> Self {
match parameter {
Parameter::Path(path) => Self {
name: &path.name,
location: Location::Path,
required: true,
style: match path.style {
Some(InPathStyle::Matrix) => Style::Matrix,
Some(InPathStyle::Label) => Style::Label,
Some(InPathStyle::Simple) | None => Style::Simple,
},
explode: path.explode.unwrap_or(false),
schema: path.schema.as_ref(),
content: path.content.as_ref(),
},
Parameter::Querystring(querystring) => Self {
name: &querystring.name,
location: Location::Querystring,
required: querystring.required.unwrap_or(false),
style: Style::Simple,
explode: false,
schema: None,
content: Some(&querystring.content),
},
Parameter::Query(query) => Self {
name: &query.name,
location: Location::Query,
required: query.required.unwrap_or(false),
style: match query.style {
Some(InQueryStyle::SpaceDelimited) => Style::SpaceDelimited,
Some(InQueryStyle::PipeDelimited) => Style::PipeDelimited,
Some(InQueryStyle::DeepObject) => Style::DeepObject,
Some(InQueryStyle::Form) | None => Style::Form,
},
explode: query
.explode
.unwrap_or(matches!(query.style, Some(InQueryStyle::Form) | None)),
schema: query.schema.as_ref(),
content: query.content.as_ref(),
},
Parameter::Header(header) => Self {
name: &header.name,
location: Location::Header,
required: header.required.unwrap_or(false),
style: match header.style {
Some(InHeaderStyle::Simple) | None => Style::Simple,
},
explode: header.explode.unwrap_or(false),
schema: header.schema.as_ref(),
content: header.content.as_ref(),
},
Parameter::Cookie(cookie) => Self {
name: &cookie.name,
location: Location::Cookie,
required: cookie.required.unwrap_or(false),
style: match cookie.style {
Some(InCookieStyle::Form) | None => Style::Form,
},
explode: cookie.explode.unwrap_or(true),
schema: cookie.schema.as_ref(),
content: cookie.content.as_ref(),
},
}
}
fn raw_text(&self, request: &RequestView<'_>, extracted: &Extracted<'_>) -> Option<String> {
match self.location {
Location::Path => extracted
.path
.get(self.name)
.map(|raw| self.decode_value(raw)),
Location::Query => extracted
.query
.iter()
.find(|(name, _)| name == self.name)
.map(|(_, value)| self.decode_value(value)),
Location::Querystring => Some(request.query.as_deref().unwrap_or_default().to_owned()),
Location::Header => request.header(self.name).map(str::to_owned),
Location::Cookie => extracted
.cookies
.iter()
.find(|(name, _)| name == self.name)
.map(|(_, value)| value.clone()),
Location::Body | Location::Description => None,
}
}
fn read(
&self,
request: &RequestView<'_>,
extracted: &Extracted<'_>,
shape: &Shape<'_>,
spec: &Spec,
) -> Result<Option<Value>, String> {
match self.location {
Location::Path => {
let Some(raw) = extracted.path.get(self.name) else {
return Ok(None);
};
self.read_single(self.undecorate(raw), shape, spec)
.map(Some)
}
Location::Query => self.read_query(&extracted.query, shape, spec),
Location::Header => {
let values: Vec<&str> = request.header_values(self.name).collect();
if values.is_empty() {
return Ok(None);
}
self.read_single(&values.join(","), shape, spec).map(Some)
}
Location::Cookie => {
let found = extracted.cookies.iter().find(|(name, _)| name == self.name);
let Some((_, raw)) = found else {
return Ok(None);
};
self.read_single(raw, shape, spec).map(Some)
}
Location::Querystring | Location::Body | Location::Description => Ok(None),
}
}
fn decode_value(&self, raw: &str) -> String {
match self.location {
Location::Path => decode_path_segment(raw),
Location::Query | Location::Querystring => decode_form(raw),
Location::Header | Location::Cookie | Location::Body | Location::Description => {
raw.to_owned()
}
}
}
fn undecorate<'v>(&self, raw: &'v str) -> &'v str {
match self.style {
Style::Label => raw.strip_prefix('.').unwrap_or(raw),
Style::Matrix => {
let raw = raw.strip_prefix(';').unwrap_or(raw);
raw.strip_prefix(&format!("{}=", self.name)).unwrap_or(raw)
}
_ => raw,
}
}
fn read_single(&self, raw: &str, shape: &Shape<'_>, spec: &Spec) -> Result<Value, String> {
let separator = self.separator();
match shape {
Shape::Array(items) => {
let values = self
.split_list(raw, separator)
.iter()
.map(|part| coerce(&self.decode_value(part), *items, spec))
.collect::<Result<Vec<_>, _>>()?;
Ok(Value::Array(values))
}
Shape::Object(properties) => {
let pairs = if self.explode {
self.split_list(raw, separator)
.iter()
.filter_map(|part| {
part.split_once('=').map(|(name, value)| {
(self.decode_value(name), self.decode_value(value))
})
})
.collect()
} else {
let flat: Vec<String> = self
.split_list(raw, separator)
.iter()
.map(|part| self.decode_value(part))
.collect();
flat.chunks(2)
.filter(|chunk| chunk.len() == 2)
.map(|chunk| (chunk[0].clone(), chunk[1].clone()))
.collect()
};
object_from(pairs, *properties, spec)
}
Shape::Primitive(primitive) => coerce_primitive(&self.decode_value(raw), *primitive),
Shape::Opaque => Ok(Value::String(self.decode_value(raw))),
}
}
fn separator(&self) -> char {
match self.style {
Style::SpaceDelimited => ' ',
Style::PipeDelimited => '|',
Style::Label => '.',
Style::Matrix if self.explode => ';',
_ => ',',
}
}
fn split_list(&self, raw: &str, separator: char) -> Vec<String> {
if raw.is_empty() {
return Vec::new();
}
let normalized;
let raw = if separator == ' ' {
normalized = raw.replace("%20", " ").replace('+', " ");
normalized.as_str()
} else {
raw
};
raw.split(separator)
.map(|part| {
if self.style == Style::Matrix && self.explode {
part.strip_prefix(&format!("{}=", self.name))
.unwrap_or(part)
.to_owned()
} else {
part.to_owned()
}
})
.collect()
}
fn read_query(
&self,
pairs: &[(String, String)],
shape: &Shape<'_>,
spec: &Spec,
) -> Result<Option<Value>, String> {
if self.style == Style::DeepObject {
let members: Vec<(String, String)> = pairs
.iter()
.filter_map(|(name, value)| {
let key = self.deep_object_key(name)?;
Some((key.to_owned(), self.decode_value(value)))
})
.collect();
if members.is_empty() {
return Ok(None);
}
let properties = match shape {
Shape::Object(properties) => *properties,
_ => None,
};
return object_from(members, properties, spec).map(Some);
}
if self.explode
&& let Shape::Object(Some(properties)) = shape
{
let members: Vec<(String, String)> = pairs
.iter()
.filter(|(name, _)| properties.contains_key(name))
.map(|(name, value)| (name.clone(), self.decode_value(value)))
.collect();
if members.is_empty() {
return Ok(None);
}
return object_from(members, Some(*properties), spec).map(Some);
}
let mine: Vec<&String> = pairs
.iter()
.filter(|(name, _)| name == self.name)
.map(|(_, value)| value)
.collect();
if mine.is_empty() {
return Ok(None);
}
if self.explode
&& let Shape::Array(items) = shape
{
let values = mine
.into_iter()
.map(|value| coerce(&self.decode_value(value), *items, spec))
.collect::<Result<Vec<_>, _>>()?;
return Ok(Some(Value::Array(values)));
}
self.read_single(mine[0], shape, spec).map(Some)
}
}
fn object_from(
pairs: Vec<(String, String)>,
properties: Option<&BTreeMap<String, RefOr<Schema>>>,
spec: &Spec,
) -> Result<Value, String> {
let mut object = serde_json::Map::new();
for (name, value) in pairs {
let property = properties.and_then(|properties| properties.get(&name));
object.insert(name, coerce(&value, property, spec)?);
}
Ok(Value::Object(object))
}
pub(crate) fn coerce(
raw: &str,
schema: Option<&RefOr<Schema>>,
spec: &Spec,
) -> Result<Value, String> {
match schema.map(|schema| Shape::of(schema, spec)) {
Some(Shape::Primitive(primitive)) => coerce_primitive(raw, primitive),
_ => Ok(Value::String(raw.to_owned())),
}
}
fn coerce_primitive(raw: &str, primitive: Primitive) -> Result<Value, String> {
match primitive {
Primitive::String => Ok(Value::String(raw.to_owned())),
Primitive::Integer => raw
.parse::<i64>()
.map(Value::from)
.map_err(|_| format!("{raw:?} is not an integer")),
Primitive::Number => raw
.parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(Value::Number)
.ok_or_else(|| format!("{raw:?} is not a number")),
Primitive::Boolean => match raw {
"true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)),
_ => Err(format!("{raw:?} is not `true` or `false`")),
},
Primitive::Null => match raw {
"" | "null" => Ok(Value::Null),
_ => Err(format!("{raw:?} is not null")),
},
}
}
impl<'s> Shape<'s> {
fn of(schema: &'s RefOr<Schema>, spec: &'s Spec) -> Self {
let Ok(resolved) = schema.get_item(spec) else {
return Shape::Opaque;
};
match resolved {
Schema::Single(single) => match single.as_ref() {
SingleSchema::String(_) => Shape::Primitive(Primitive::String),
SingleSchema::Integer(_) => Shape::Primitive(Primitive::Integer),
SingleSchema::Number(_) => Shape::Primitive(Primitive::Number),
SingleSchema::Boolean(_) => Shape::Primitive(Primitive::Boolean),
SingleSchema::Null(_) => Shape::Primitive(Primitive::Null),
SingleSchema::Array(array) => Shape::Array(match &array.items {
Some(BoolOr::Item(items)) => Some(items),
_ => None,
}),
SingleSchema::Object(object) => Shape::Object(object.properties.as_ref()),
},
Schema::Multi(multi) => multi
.schema_types
.iter()
.find_map(|schema_type| match schema_type {
SchemaType::String => Some(Shape::Primitive(Primitive::String)),
SchemaType::Integer => Some(Shape::Primitive(Primitive::Integer)),
SchemaType::Number => Some(Shape::Primitive(Primitive::Number)),
SchemaType::Boolean => Some(Shape::Primitive(Primitive::Boolean)),
SchemaType::Array => Some(Shape::Array(None)),
SchemaType::Object => Some(Shape::Object(None)),
SchemaType::Null | SchemaType::Custom(_) => None,
})
.unwrap_or(Shape::Opaque),
_ => Shape::Opaque,
}
}
}