roas_http_validator/lib.rs
1//! Validates HTTP requests against an OpenAPI description.
2//!
3//! [`roas`](https://crates.io/crates/roas) parses a description and
4//! checks that the *description* is well formed. This checks that a
5//! *request* is what the description says it should be: the path is one
6//! the description names, the method is one that path offers, every
7//! required parameter arrived, each one is the type its Schema Object
8//! declares, and the body is what the Request Body Object describes.
9//!
10//! ```
11//! use roas_http_validator::{RequestView, Validator};
12//!
13//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! # let spec = serde_json::from_str(r#"{
15//! # "openapi": "3.2.0",
16//! # "info": { "title": "Pets", "version": "1.0.0" },
17//! # "paths": { "/pets": { "get": { "operationId": "listPets", "parameters": [
18//! # { "name": "limit", "in": "query", "schema": { "type": "integer", "maximum": 100 } }
19//! # ] } } }
20//! # }"#)?;
21//! let validator = Validator::new(spec);
22//!
23//! let request = RequestView::new("GET", "/pets").with_query("limit=1000");
24//! let report = validator.validate(&request)?;
25//!
26//! assert!(!report.is_valid());
27//! assert_eq!(
28//! report.errors[0].to_string(),
29//! "query parameter \"limit\": 1000 is above maximum 100",
30//! );
31//! # Ok(()) }
32//! ```
33//!
34//! ## Examples
35//!
36//! The repository carries three runnable ones: `validate` for the shape
37//! of the whole crate, `axum_layer` for the same thing as middleware
38//! (and for what buffering a body actually looks like), and
39//! `client_check` for asking whether a call you are about to *make*
40//! matches the description.
41//!
42//! ## Which request type
43//!
44//! None of them, and all of them. Rust has no single HTTP request type
45//! to validate: `http::Request` comes closest, but it is generic over a
46//! body that is usually a stream, and it is version-split — actix-web 4
47//! is on `http` 0.2 while hyper 1, axum 0.8 and reqwest are on 1.x, so
48//! their `HeaderMap`s are different types. Taking either one would shut
49//! out half the ecosystem.
50//!
51//! So this crate takes [`RequestView`], the small set of things an
52//! OpenAPI description actually talks about, and each framework gets a
53//! [`ToRequestView`] impl behind its own feature:
54//!
55//! | Feature | Covers |
56//! | --- | --- |
57//! | `http` | `http::Request`, `http::request::Parts` — and so axum, warp, tonic, hyper |
58//! | `actix-web` | `actix_web::HttpRequest` |
59//! | `poem` | `poem::Request` |
60//! | `salvo` | `salvo_core::http::Request` |
61//! | `rocket` | `rocket::Request` |
62//! | `reqwest` | `reqwest::Request` and its blocking twin — the client's side, for checking an outgoing call |
63//!
64//! The body is not part of that conversion. A framework body is a
65//! stream, and validating one means buffering it — how much, and
66//! whether at all, is the caller's decision, so the adapters convert
67//! the head and [`RequestView::with_body`] takes the bytes. The one
68//! exception is `reqwest`, where a non-streaming body is already bytes
69//! in memory and there is nothing to buffer.
70//!
71//! ## Versions
72//!
73//! The interpreter is v3.2. Enable `v3_1`, `v3_0` or `v2` to accept a
74//! description written to an older version: it is upconverted through
75//! `roas`'s own migrations first, so there is one interpreter rather
76//! than four.
77//!
78//! Numbers are compared as the decimals they were written as, on both
79//! sides. The one limit is the format a
80//! *description* is parsed from: JSON is exact throughout, while YAML
81//! reads scalars through an `f64` before `serde_json` is involved, so a
82//! fractional bound carrying more precision than a double is already
83//! rounded when it arrives. Every integer survives either way.
84//!
85//! ## Media types it does not read itself
86//!
87//! JSON, `application/x-www-form-urlencoded` and `text/*` are built in.
88//! Anything else — `multipart/form-data`, XML — is reported rather than
89//! guessed at, and [`Options::decoder`] is the way in: the bytes become
90//! a value and the Schema Object judges it like any other.
91//!
92//! Those two are a hook rather than more built-ins on purpose.
93//! Multipart would mean owning a boundary parser and buffering file
94//! uploads, which is exactly where this crate leaves buffering to the
95//! caller. XML has no specified mapping onto a schema instance at all —
96//! OpenAPI's XML Object is serialization metadata for code generators —
97//! so any translation is a choice, and taking the caller's beats
98//! inventing one.
99//!
100//! ## What it does not check yet
101//!
102//! Response validation and security requirements.
103//!
104//! Everything a check could not judge is reported rather than passed
105//! over, so a request never looks valid because nothing looked at it:
106//! [`ErrorKind::Unsupported`] for what is not implemented,
107//! [`ErrorKind::Unchecked`] for a description this crate can read but
108//! cannot apply faithfully. [`ValidationReport::unchecked`] separates
109//! both from what the request definitely got wrong.
110
111mod body;
112mod decimal;
113mod decoder;
114mod method;
115mod parameter;
116mod paths;
117mod report;
118mod request;
119mod router;
120mod schema;
121mod validator;
122
123mod adapters;
124
125pub use decoder::Decoder;
126pub use report::{ErrorKind, Location, RoutingError, ValidationError, ValidationReport};
127pub use request::{RequestView, ToRequestView};
128pub use validator::{Options, Validator};
129
130impl Validator {
131 /// Prepare a v3.1 description, upconverting it to v3.2 first.
132 #[cfg(feature = "v3_1")]
133 #[must_use]
134 pub fn from_v3_1(spec: roas::v3_1::spec::Spec, options: Options) -> Self {
135 Self::with_options(spec.into(), options)
136 }
137
138 /// Prepare a v3.0 description, upconverting it to v3.2 first.
139 #[cfg(feature = "v3_0")]
140 #[must_use]
141 pub fn from_v3_0(spec: roas::v3_0::spec::Spec, options: Options) -> Self {
142 let v3_1: roas::v3_1::spec::Spec = spec.into();
143 Self::from_v3_1(v3_1, options)
144 }
145
146 /// Prepare a v2.0 (Swagger) description, upconverting it to v3.2
147 /// first.
148 #[cfg(feature = "v2")]
149 #[must_use]
150 pub fn from_v2(spec: roas::v2::spec::Spec, options: Options) -> Self {
151 let v3_0: roas::v3_0::spec::Spec = spec.into();
152 Self::from_v3_0(v3_0, options)
153 }
154}