use std::collections::BTreeMap;
use roas::v3_2::operation::Operation;
use roas::v3_2::parameter::Parameter;
use roas::v3_2::path_item::PathItem;
use roas::v3_2::spec::Spec;
use crate::body;
use crate::parameter;
use crate::paths;
use crate::report::{ErrorKind, Location, RoutingError, ValidationError, ValidationReport};
use crate::request::{RequestView, decode_path_segment};
use crate::router::Router;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Options {
base_path: Option<String>,
skip_body: bool,
reject_undescribed_query_parameters: bool,
}
impl Options {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn base_path(mut self, base_path: impl Into<String>) -> Self {
self.base_path = Some(base_path.into());
self
}
#[must_use]
pub fn skip_body(mut self) -> Self {
self.skip_body = true;
self
}
#[must_use]
pub fn reject_undescribed_query_parameters(mut self) -> Self {
self.reject_undescribed_query_parameters = true;
self
}
}
#[derive(Clone, Debug)]
pub struct Validator {
spec: Spec,
path_items: BTreeMap<String, PathItem>,
router: Router,
options: Options,
}
impl Validator {
#[must_use]
pub fn new(spec: Spec) -> Self {
Self::with_options(spec, Options::new())
}
#[must_use]
pub fn with_options(spec: Spec, options: Options) -> Self {
let path_items = paths::resolve(&spec);
let router = Router::new(
&path_items,
spec.servers.as_deref(),
options.base_path.as_deref(),
);
Self {
spec,
path_items,
router,
options,
}
}
#[must_use]
pub fn spec(&self) -> &Spec {
&self.spec
}
pub fn validate(&self, request: &RequestView<'_>) -> Result<ValidationReport, RoutingError> {
let matched = self
.router
.route(&request.path, &request.method)
.ok_or_else(|| RoutingError::PathNotFound {
path: request.path.clone().into_owned(),
})?;
let template = matched.template.to_owned();
let path_parameters = matched.parameters;
let path_item = self.path_item(&template);
let unresolved = path_item.and_then(|item| item.reference.clone());
let found = path_item.and_then(|item| self.operation(item, request));
let Some((method, operation)) = found else {
if let Some(reference) = unresolved {
return Err(RoutingError::Unresolved {
template,
reference,
});
}
return Err(RoutingError::MethodNotAllowed {
template,
method: request.method.clone().into_owned(),
allowed: path_item.map(allowed_methods).unwrap_or_default(),
});
};
let mut errors = Vec::new();
if let Some(reference) = unresolved {
errors.push(ValidationError {
location: Location::Description,
name: String::new(),
pointer: String::new(),
kind: ErrorKind::UnresolvedReference(reference),
});
}
let parameters = self.parameters(path_item, operation, &mut errors);
let extracted = parameter::Extracted::new(request, &path_parameters);
for parameter in ¶meters {
parameter::validate(parameter, request, &extracted, &self.spec, &mut errors);
}
if self.options.reject_undescribed_query_parameters {
check_for_strays(&extracted, ¶meters, &self.spec, &mut errors);
}
if !self.options.skip_body
&& let Some(request_body) = &operation.request_body
{
match request_body.get_item(&self.spec) {
Ok(request_body) => {
body::validate(request_body, request, &self.spec, &mut errors);
}
Err(error) => errors.push(ValidationError {
location: Location::Body,
name: String::new(),
pointer: String::new(),
kind: ErrorKind::UnresolvedReference(error.to_string()),
}),
}
}
Ok(ValidationReport {
template,
method,
operation_id: operation.operation_id.clone(),
path_parameters: path_parameters
.iter()
.map(|(name, raw)| (name.clone(), decode_path_segment(raw)))
.collect(),
errors,
})
}
fn operation<'i>(
&self,
path_item: &'i PathItem,
request: &RequestView<'_>,
) -> Option<(String, &'i Operation)> {
if let Some(key) = crate::method::standard(&request.method)
&& let Some((key, operation)) = path_item
.operations
.as_ref()
.and_then(|operations| operations.get_key_value(&key))
{
return Some((crate::method::from_standard_key(key), operation));
}
path_item
.additional_operations
.as_ref()?
.get_key_value(request.method.as_ref())
.map(|(key, operation)| (key.clone(), operation))
}
fn path_item(&self, template: &str) -> Option<&PathItem> {
self.path_items.get(template)
}
fn parameters(
&self,
path_item: Option<&PathItem>,
operation: &Operation,
errors: &mut Vec<ValidationError>,
) -> Vec<Parameter> {
let mut merged: BTreeMap<(String, Location), Parameter> = BTreeMap::new();
let inherited = path_item.and_then(|item| item.parameters.as_deref());
let declared = operation.parameters.as_deref();
for source in [inherited, declared].into_iter().flatten() {
for parameter in source {
match parameter.get_item(&self.spec) {
Ok(parameter) => {
merged.insert(identity(parameter), parameter.clone());
}
Err(error) => errors.push(ValidationError {
location: Location::Description,
name: String::new(),
pointer: String::new(),
kind: ErrorKind::UnresolvedReference(error.to_string()),
}),
}
}
}
merged.into_values().collect()
}
}
fn check_for_strays(
extracted: ¶meter::Extracted<'_>,
parameters: &[Parameter],
spec: &Spec,
errors: &mut Vec<ValidationError>,
) {
if parameters
.iter()
.any(|parameter| matches!(parameter, Parameter::Querystring(_)))
{
return;
}
for (name, _) in &extracted.query {
if !parameters
.iter()
.any(|parameter| parameter::accounts_for(parameter, name, spec))
{
errors.push(ValidationError {
location: Location::Query,
name: name.clone(),
pointer: String::new(),
kind: ErrorKind::Undescribed,
});
}
}
}
fn allowed_methods(path_item: &PathItem) -> Vec<String> {
let standard = path_item
.operations
.iter()
.flatten()
.map(|(key, _)| crate::method::from_standard_key(key));
let additional = path_item
.additional_operations
.iter()
.flatten()
.map(|(key, _)| key.clone());
standard.chain(additional).collect()
}
fn identity(parameter: &Parameter) -> (String, Location) {
match parameter {
Parameter::Path(path) => (path.name.clone(), Location::Path),
Parameter::Query(query) => (query.name.clone(), Location::Query),
Parameter::Querystring(querystring) => (querystring.name.clone(), Location::Querystring),
Parameter::Header(header) => (header.name.clone(), Location::Header),
Parameter::Cookie(cookie) => (cookie.name.clone(), Location::Cookie),
}
}