roas_http_validator/validator.rs
1//! The validator itself: a description, prepared once, judging many
2//! requests.
3
4use std::collections::BTreeMap;
5use std::fmt;
6
7use roas::v3_2::operation::Operation;
8use roas::v3_2::parameter::Parameter;
9use roas::v3_2::path_item::PathItem;
10use roas::v3_2::spec::Spec;
11
12use crate::body;
13use crate::decoder::Decoders;
14use crate::parameter;
15use crate::paths;
16use crate::report::{ErrorKind, Location, RoutingError, ValidationError, ValidationReport};
17use crate::request::{RequestView, decode_path_segment};
18use crate::router::Router;
19
20/// What to check, and where the description's paths start.
21///
22/// ```
23/// use roas_http_validator::Options;
24///
25/// let options = Options::new().base_path("/api/v1").reject_undescribed_query_parameters();
26/// ```
27#[derive(Clone, Default)]
28#[non_exhaustive]
29pub struct Options {
30 base_path: Option<String>,
31 skip_body: bool,
32 reject_undescribed_query_parameters: bool,
33 pub(crate) decoders: Decoders,
34}
35
36/// Written out by hand because a decoder is a function, and a function
37/// has nothing useful to print. The media types it was registered for
38/// do.
39impl fmt::Debug for Options {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.debug_struct("Options")
42 .field("base_path", &self.base_path)
43 .field("skip_body", &self.skip_body)
44 .field(
45 "reject_undescribed_query_parameters",
46 &self.reject_undescribed_query_parameters,
47 )
48 .field("decoders", &self.decoders.media_types().collect::<Vec<_>>())
49 .finish()
50 }
51}
52
53impl Options {
54 /// Everything checked, base path taken from the Server Objects.
55 #[must_use]
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 /// The prefix a request path carries before the description's own
61 /// paths begin, overriding whatever `servers` implies.
62 #[must_use]
63 pub fn base_path(mut self, base_path: impl Into<String>) -> Self {
64 self.base_path = Some(base_path.into());
65 self
66 }
67
68 /// Leave the body alone. Useful in a middleware that would rather
69 /// not buffer one, and in a client-side check of a request that has
70 /// not been serialized yet.
71 #[must_use]
72 pub fn skip_body(mut self) -> Self {
73 self.skip_body = true;
74 self
75 }
76
77 /// Read a media type this crate does not know how to read.
78 ///
79 /// The built-in decoders cover JSON,
80 /// `application/x-www-form-urlencoded` and `text/*`; anything else
81 /// is reported as unchecked rather than guessed at. Register a
82 /// decoder and its media type joins them — the bytes become a
83 /// value, and the Schema Object judges it like any other.
84 ///
85 /// This is how `multipart/form-data` and XML are meant to be
86 /// handled: see [`crate::Decoder`] for why they are a hook rather
87 /// than more built-ins.
88 ///
89 /// Looked up the way a Media Type Object is — exact match, then a
90 /// `type/*` range, then `*/*` — and a registration takes precedence
91 /// over the built-in for the same media type, so a caller who wants
92 /// their own JSON reader can have one.
93 ///
94 /// ```
95 /// use roas_http_validator::Options;
96 ///
97 /// let options = Options::new().decoder("text/csv", |bytes, _media_type| {
98 /// let text = std::str::from_utf8(bytes).map_err(|error| error.to_string())?;
99 /// Ok(serde_json::Value::Array(
100 /// text.lines().map(|line| line.into()).collect(),
101 /// ))
102 /// });
103 /// ```
104 #[must_use]
105 pub fn decoder<F>(mut self, media_type: &str, decoder: F) -> Self
106 where
107 F: Fn(&[u8], &str) -> Result<serde_json::Value, String> + Send + Sync + 'static,
108 {
109 self.decoders
110 .insert(media_type, std::sync::Arc::new(decoder));
111 self
112 }
113
114 /// Report a query parameter the operation does not describe.
115 ///
116 /// Off by default: OpenAPI does not forbid undescribed query
117 /// parameters, and plenty of real clients send tracking parameters
118 /// that no description mentions. On, it catches the typo in
119 /// `?limti=10` that would otherwise silently do nothing.
120 #[must_use]
121 pub fn reject_undescribed_query_parameters(mut self) -> Self {
122 self.reject_undescribed_query_parameters = true;
123 self
124 }
125}
126
127/// One OpenAPI description, ready to judge requests against.
128///
129/// Building one walks the description's paths once; validating is then
130/// a match and a handful of schema checks, so a server builds this at
131/// startup and keeps it.
132///
133/// ```
134/// use roas_http_validator::{RequestView, Validator};
135///
136/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
137/// let spec = serde_json::from_str(r#"{
138/// "openapi": "3.2.0",
139/// "info": { "title": "Pets", "version": "1.0.0" },
140/// "paths": {
141/// "/pets/{petId}": {
142/// "get": {
143/// "operationId": "getPet",
144/// "parameters": [
145/// { "name": "petId", "in": "path", "required": true,
146/// "schema": { "type": "integer" } }
147/// ]
148/// }
149/// }
150/// }
151/// }"#)?;
152///
153/// let validator = Validator::new(spec);
154/// assert!(validator.validate(&RequestView::new("GET", "/pets/7"))?.is_valid());
155/// assert!(!validator.validate(&RequestView::new("GET", "/pets/rex"))?.is_valid());
156/// # Ok(()) }
157/// ```
158#[derive(Clone, Debug)]
159pub struct Validator {
160 spec: Spec,
161 /// Every Path Item Object with its `$ref` followed and merged,
162 /// resolved once here rather than on every request.
163 path_items: BTreeMap<String, PathItem>,
164 router: Router,
165 options: Options,
166}
167
168impl Validator {
169 /// Prepare a v3.2 description with the default [`Options`].
170 #[must_use]
171 pub fn new(spec: Spec) -> Self {
172 Self::with_options(spec, Options::new())
173 }
174
175 /// Prepare a v3.2 description.
176 #[must_use]
177 pub fn with_options(spec: Spec, options: Options) -> Self {
178 let path_items = paths::resolve(&spec);
179 let router = Router::new(
180 &path_items,
181 spec.servers.as_deref(),
182 options.base_path.as_deref(),
183 );
184 Self {
185 spec,
186 path_items,
187 router,
188 options,
189 }
190 }
191
192 /// The description being validated against.
193 #[must_use]
194 pub fn spec(&self) -> &Spec {
195 &self.spec
196 }
197
198 /// Judge one request.
199 ///
200 /// # Errors
201 ///
202 /// [`RoutingError`] when the request cannot be judged at all, which
203 /// is a different answer from "the request is invalid" and usually a
204 /// different response code:
205 ///
206 /// - [`RoutingError::PathNotFound`] — no template matches the path.
207 /// - [`RoutingError::MethodNotAllowed`] — a template matches and
208 /// describes other methods, but not this one.
209 /// - [`RoutingError::Unresolved`] — a template matches but its Path
210 /// Item Object could not be read, so neither of the above can be
211 /// said honestly.
212 pub fn validate(&self, request: &RequestView<'_>) -> Result<ValidationReport, RoutingError> {
213 let matched = self
214 .router
215 .route(&request.path, &request.method)
216 .ok_or_else(|| RoutingError::PathNotFound {
217 path: request.path.clone().into_owned(),
218 })?;
219 let template = matched.template.to_owned();
220 let path_parameters = matched.parameters;
221
222 let path_item = self.path_item(&template);
223 // A `$ref` chain that could not be followed leaves part of this
224 // Path Item Object unread.
225 let unresolved = path_item.and_then(|item| item.reference.clone());
226 let found = path_item.and_then(|item| self.operation(item, request));
227
228 let Some((method, operation)) = found else {
229 // With half the Path Item Object unread, "no such method"
230 // is not something that can be said: the half that did not
231 // arrive may well have described it.
232 if let Some(reference) = unresolved {
233 return Err(RoutingError::Unresolved {
234 template,
235 reference,
236 });
237 }
238 return Err(RoutingError::MethodNotAllowed {
239 template,
240 // The token the request actually carried, not a
241 // normalization of it: `get` was refused *because* it is
242 // not `GET`, and saying "no GET here" beside an `Allow`
243 // naming `GET` would be nonsense.
244 method: request.method.clone().into_owned(),
245 allowed: path_item.map(allowed_methods).unwrap_or_default(),
246 });
247 };
248
249 let mut errors = Vec::new();
250 // An operation was found, so the request can still be judged —
251 // but whatever the unread half held went unapplied, and saying
252 // so is the difference between "valid" and "not checked".
253 if let Some(reference) = unresolved {
254 errors.push(ValidationError {
255 location: Location::Description,
256 name: String::new(),
257 pointer: String::new(),
258 kind: ErrorKind::UnresolvedReference(reference),
259 });
260 }
261 let parameters = self.parameters(path_item, operation, &mut errors);
262 // Decoded once for the whole operation rather than per parameter.
263 let extracted = parameter::Extracted::new(request, &path_parameters);
264
265 for parameter in ¶meters {
266 parameter::validate(
267 parameter,
268 request,
269 &extracted,
270 &self.spec,
271 &self.options.decoders,
272 &mut errors,
273 );
274 }
275
276 if self.options.reject_undescribed_query_parameters {
277 check_for_strays(&extracted, ¶meters, &self.spec, &mut errors);
278 }
279
280 if !self.options.skip_body
281 && let Some(request_body) = &operation.request_body
282 {
283 match request_body.get_item(&self.spec) {
284 Ok(request_body) => {
285 body::validate(
286 request_body,
287 request,
288 &self.spec,
289 &self.options.decoders,
290 &mut errors,
291 );
292 }
293 Err(error) => errors.push(ValidationError {
294 location: Location::Body,
295 name: String::new(),
296 pointer: String::new(),
297 kind: ErrorKind::UnresolvedReference(error.to_string()),
298 }),
299 }
300 }
301
302 Ok(ValidationReport {
303 template,
304 method,
305 operation_id: operation.operation_id.clone(),
306 // Decoded here and only here: validation splits before it
307 // decodes, but a report is for a reader.
308 path_parameters: path_parameters
309 .iter()
310 .map(|(name, raw)| (name.clone(), decode_path_segment(raw)))
311 .collect(),
312 errors,
313 })
314 }
315
316 /// The operation a request's method names, and the key the Path
317 /// Item Object files it under.
318 ///
319 /// See [`crate::method`] for why `get` does not find `get`.
320 fn operation<'i>(
321 &self,
322 path_item: &'i PathItem,
323 request: &RequestView<'_>,
324 ) -> Option<(String, &'i Operation)> {
325 // Each map is searched with its own key and never the other's.
326 if let Some(key) = crate::method::standard(&request.method)
327 && let Some((key, operation)) = path_item
328 .operations
329 .as_ref()
330 .and_then(|operations| operations.get_key_value(&key))
331 {
332 return Some((crate::method::from_standard_key(key), operation));
333 }
334 path_item
335 .additional_operations
336 .as_ref()?
337 .get_key_value(request.method.as_ref())
338 // Already a method token: `additionalOperations` is keyed by
339 // the method itself.
340 .map(|(key, operation)| (key.clone(), operation))
341 }
342
343 /// The Path Item Object for a template, already resolved.
344 fn path_item(&self, template: &str) -> Option<&PathItem> {
345 self.path_items.get(template)
346 }
347
348 /// The parameters that apply to one operation: the Path Item
349 /// Object's, overridden by the Operation Object's where both name
350 /// the same `name` and `in`.
351 fn parameters(
352 &self,
353 path_item: Option<&PathItem>,
354 operation: &Operation,
355 errors: &mut Vec<ValidationError>,
356 ) -> Vec<Parameter> {
357 let mut merged: BTreeMap<(String, Location), Parameter> = BTreeMap::new();
358 let inherited = path_item.and_then(|item| item.parameters.as_deref());
359 let declared = operation.parameters.as_deref();
360
361 for source in [inherited, declared].into_iter().flatten() {
362 for parameter in source {
363 match parameter.get_item(&self.spec) {
364 Ok(parameter) => {
365 merged.insert(identity(parameter), parameter.clone());
366 }
367 // The parameter cannot be read, so it cannot be
368 // checked — which is the description's fault, not
369 // the request's, and says so.
370 Err(error) => errors.push(ValidationError {
371 location: Location::Description,
372 name: String::new(),
373 pointer: String::new(),
374 kind: ErrorKind::UnresolvedReference(error.to_string()),
375 }),
376 }
377 }
378 }
379 merged.into_values().collect()
380 }
381}
382
383/// Report query parameters the operation says nothing about.
384fn check_for_strays(
385 extracted: ¶meter::Extracted<'_>,
386 parameters: &[Parameter],
387 spec: &Spec,
388 errors: &mut Vec<ValidationError>,
389) {
390 // `in: querystring` describes the query string whole, so there is no
391 // such thing as a stray parameter alongside one.
392 if parameters
393 .iter()
394 .any(|parameter| matches!(parameter, Parameter::Querystring(_)))
395 {
396 return;
397 }
398 for (name, _) in &extracted.query {
399 if !parameters
400 .iter()
401 .any(|parameter| parameter::accounts_for(parameter, name, spec))
402 {
403 errors.push(ValidationError {
404 location: Location::Query,
405 name: name.clone(),
406 pointer: String::new(),
407 kind: ErrorKind::Undescribed,
408 });
409 }
410 }
411}
412
413/// Every method a Path Item Object describes, as method tokens — which
414/// is what an `Allow` header wants, and what `operations`' lowercase
415/// keys are not.
416fn allowed_methods(path_item: &PathItem) -> Vec<String> {
417 let standard = path_item
418 .operations
419 .iter()
420 .flatten()
421 .map(|(key, _)| crate::method::from_standard_key(key));
422 let additional = path_item
423 .additional_operations
424 .iter()
425 .flatten()
426 .map(|(key, _)| key.clone());
427 standard.chain(additional).collect()
428}
429
430/// What makes a parameter unique: its name and its location.
431fn identity(parameter: &Parameter) -> (String, Location) {
432 match parameter {
433 Parameter::Path(path) => (path.name.clone(), Location::Path),
434 Parameter::Query(query) => (query.name.clone(), Location::Query),
435 Parameter::Querystring(querystring) => (querystring.name.clone(), Location::Querystring),
436 Parameter::Header(header) => (header.name.clone(), Location::Header),
437 Parameter::Cookie(cookie) => (cookie.name.clone(), Location::Cookie),
438 }
439}