use roas_http_validator::{Options, RequestView, RoutingError, Validator};
const PETSTORE: &str = r#"
openapi: 3.2.0
info:
title: Pets
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/pets:
get:
operationId: listPets
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
- name: tags
in: query
style: form
explode: true
schema:
type: array
items:
type: string
post:
operationId: createPet
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name:
type: string
minLength: 1
age:
type: integer
minimum: 0
/pets/{petId}:
get:
operationId: getPet
parameters:
- name: petId
in: path
required: true
schema:
type: integer
minimum: 1
"#;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let spec = serde_yaml_ng::from_str(PETSTORE)?;
let validator = Validator::with_options(
spec,
Options::new().reject_undescribed_query_parameters(),
);
let requests = [
RequestView::new("GET", "/pets/7"),
RequestView::new("GET", "/v1/pets").with_query("limit=10&tags=cute&tags=small"),
RequestView::new("GET", "/pets").with_query("limit=1000"),
RequestView::new("GET", "/pets").with_query("limit=lots"),
RequestView::new("GET", "/pets").with_query("limti=10"),
RequestView::new("GET", "/pets/rex"),
RequestView::new("POST", "/pets")
.with_header("content-type", "application/json")
.with_body(br#"{"name":"Rex","age":4}"#.as_slice()),
RequestView::new("POST", "/pets")
.with_header("content-type", "application/json")
.with_body(br#"{"age":-1}"#.as_slice()),
RequestView::new("DELETE", "/pets"),
RequestView::new("GET", "/unicorns"),
];
for request in &requests {
let query = request.query.as_deref().unwrap_or("");
let separator = if query.is_empty() { "" } else { "?" };
println!("\n{} {}{separator}{query}", request.method, request.path);
println!(" → {}", answer(&validator, request));
}
Ok(())
}
fn answer(validator: &Validator, request: &RequestView<'_>) -> String {
let report = match validator.validate(request) {
Err(RoutingError::PathNotFound { .. }) => return "404 (no such path)".to_owned(),
Err(RoutingError::MethodNotAllowed { allowed, .. }) => {
return format!("405, Allow: {}", allowed.join(", "));
}
Err(RoutingError::Unresolved { reference, .. }) => {
return format!("500 (description references {reference}, which is missing)");
}
Ok(report) => report,
Err(other) => return format!("500 ({other})"),
};
if report.is_valid() {
return format!("200 ({})", report.operation_id.as_deref().unwrap_or("ok"));
}
let violations: Vec<String> = report.violations().map(ToString::to_string).collect();
let unchecked: Vec<String> = report.unchecked().map(ToString::to_string).collect();
let mut answer = if violations.is_empty() {
"200, but not everything could be checked".to_owned()
} else {
format!("400 ({} problem(s))", violations.len())
};
for violation in &violations {
answer.push_str(&format!("\n {violation}"));
}
for note in &unchecked {
answer.push_str(&format!("\n (not checked) {note}"));
}
answer
}