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//! ## What it does not do yet
79//!
80//! Response validation, security requirements, `multipart/form-data`
81//! bodies, and XML — and exact decimal arithmetic, which would need
82//! `serde_json`'s `arbitrary_precision`: numbers are compared as the
83//! IEEE-754 doubles they arrive as, and anything that would over-claim
84//! on top of one is reported rather than assumed. Anything a check
85//! could not judge is reported — split out by
86//! [`ValidationReport::unchecked`] from what the request definitely got
87//! wrong —
88//! [`ErrorKind::Unsupported`] for what is not implemented yet,
89//! [`ErrorKind::Unchecked`] for a description this crate can read but
90//! cannot apply faithfully — rather than passed over, so a request
91//! never looks valid because nothing looked at it.
92
93mod body;
94mod method;
95mod parameter;
96mod paths;
97mod report;
98mod request;
99mod router;
100mod schema;
101mod validator;
102
103mod adapters;
104
105pub use report::{ErrorKind, Location, RoutingError, ValidationError, ValidationReport};
106pub use request::{RequestView, ToRequestView};
107pub use validator::{Options, Validator};
108
109impl Validator {
110 /// Prepare a v3.1 description, upconverting it to v3.2 first.
111 #[cfg(feature = "v3_1")]
112 #[must_use]
113 pub fn from_v3_1(spec: roas::v3_1::spec::Spec, options: Options) -> Self {
114 Self::with_options(spec.into(), options)
115 }
116
117 /// Prepare a v3.0 description, upconverting it to v3.2 first.
118 #[cfg(feature = "v3_0")]
119 #[must_use]
120 pub fn from_v3_0(spec: roas::v3_0::spec::Spec, options: Options) -> Self {
121 let v3_1: roas::v3_1::spec::Spec = spec.into();
122 Self::from_v3_1(v3_1, options)
123 }
124
125 /// Prepare a v2.0 (Swagger) description, upconverting it to v3.2
126 /// first.
127 #[cfg(feature = "v2")]
128 #[must_use]
129 pub fn from_v2(spec: roas::v2::spec::Spec, options: Options) -> Self {
130 let v3_0: roas::v3_0::spec::Spec = spec.into();
131 Self::from_v3_0(v3_0, options)
132 }
133}