Skip to main content

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 &parameters {
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, &parameters, &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: &parameter::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}