use roas_http_validator::{RoutingError, ToRequestView, 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:
post:
operationId: createPet
parameters:
- name: X-Request-Id
in: header
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name: { type: string, minLength: 1 }
age: { type: integer, minimum: 0 }
# `roas` models `multipleOf` as an `f64`, so the step
# stands for any decimal that rounds to it — enough to
# disprove divisibility, never enough to prove it.
weight: { type: number, multipleOf: 0.5 }
"#;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let validator = Validator::new(serde_yaml_ng::from_str(PETSTORE)?);
let client = reqwest::blocking::Client::new();
let calls = [
(
"a call that matches the description",
r#"{"name":"Rex","age":4}"#,
true,
),
("a body missing a required field", r#"{"age":4}"#, true),
(
"a call that forgot the required header",
r#"{"name":"Rex"}"#,
false,
),
(
"a call carrying a value nothing here can decide",
r#"{"name":"Rex","weight":2.5}"#,
true,
),
];
for (what, body, with_header) in calls {
let mut request = client
.post("https://api.example.com/v1/pets")
.header("content-type", "application/json");
if with_header {
request = request.header("x-request-id", "abc-123");
}
let request = request.body(body).build()?;
println!("\n{what}");
match check(&validator, &request) {
Verdict::Matches => {
println!(" ✓ matches the description — safe to send");
}
Verdict::Undetermined(notes) => {
println!(" ? nothing found wrong, but not everything could be checked");
for note in notes {
println!(" {note}");
}
}
Verdict::Violates(problems) => {
for problem in problems {
println!(" ✗ {problem}");
}
}
}
}
Ok(())
}
enum Verdict {
Matches,
Undetermined(Vec<String>),
Violates(Vec<String>),
}
fn check(validator: &Validator, request: &reqwest::blocking::Request) -> Verdict {
let report = match validator.validate(&request.request_view()) {
Ok(report) => report,
Err(
error @ (RoutingError::PathNotFound { .. } | RoutingError::MethodNotAllowed { .. }),
) => {
return Verdict::Violates(vec![error.to_string()]);
}
Err(error) => return Verdict::Undetermined(vec![error.to_string()]),
};
let problems: Vec<String> = report.violations().map(ToString::to_string).collect();
if !problems.is_empty() {
return Verdict::Violates(problems);
}
let notes: Vec<String> = report.unchecked().map(ToString::to_string).collect();
if notes.is_empty() {
Verdict::Matches
} else {
Verdict::Undetermined(notes)
}
}