Skip to main content

kynos_openapi/validate/
mod.rs

1//! Structural validation of a [`Document`].
2//!
3//! Everything checked here is a rule the OpenAPI specification states but that
4//! the type system cannot enforce on its own — uniqueness across a whole
5//! document, correspondence between a path template and its parameters, names
6//! that must resolve against what the document declares elsewhere.
7//!
8//! Mutual exclusions between fields are deliberately not among them. The model
9//! spells those as types, so a document that violates one can be neither built
10//! nor parsed and never reaches a rule here.
11//!
12//! Kynos runs this when a router is built, so a description that would mislead
13//! a client generator fails at startup rather than being published.
14//!
15//! [`Validator::validate`] is the orchestrator; the rules themselves are
16//! internal, one module per family of specification rule.
17
18pub mod violation;
19
20// The rules are implementation, not surface: a caller consumes
21// [`Violation`]s, never a checking function.
22mod rules;
23
24use crate::{
25    model::document::{Document, SpecVersion},
26    validate::{
27        rules::{
28            document::{check_component_names, check_servers, check_tags},
29            extensions::check_extensions,
30            opaque::check_opaque,
31        },
32        violation::{Severity, SpecError, Violation},
33    },
34};
35
36/// Checks a document against the rules of a specification version.
37#[derive(Clone, Copy, Debug)]
38pub struct Validator {
39    version: SpecVersion,
40}
41
42impl Validator {
43    /// Creates a validator for `version`.
44    #[must_use]
45    pub fn new(version: SpecVersion) -> Self {
46        Self { version }
47    }
48
49    /// Collects every violation in `document`, most structural first.
50    #[must_use]
51    pub fn validate(&self, document: &Document) -> Vec<Violation> {
52        let mut violations = Vec::new();
53
54        // A document is checked against the version it claims to be. 3.2-only
55        // constructs are `#[cfg]`-gated, so a 3.1-only build cannot hold one --
56        // but a 3.2-capable build can, and being asked to validate such a
57        // document as 3.1 has to say so rather than pass a description 3.1
58        // cannot express. This is the same walk `Document::emit` refuses on,
59        // read here so that validating and emitting agree.
60        //
61        // `EmptyDocument` used to be the only rule this version was consulted
62        // for, and it is raised nowhere now: every version requires a document
63        // to carry at least one of `paths`, `components` or `webhooks`, and
64        // `Document::paths` is always serialized, so the condition is prevented
65        // by construction rather than reported after the fact.
66        if !self.version.supports_3_2() {
67            let blockers = crate::emit::downgrade::three_two_only_constructs(document);
68            if !blockers.is_empty() {
69                violations.push(Violation::error("#", SpecError::RequiresV3_2 { blockers }));
70            }
71        }
72
73        // A License Object setting both `identifier` and `url` used to be
74        // checked here. `License` now holds at most one of the two, so a
75        // document carrying both cannot reach this function: it fails to
76        // deserialize, and there is no way to build one.
77
78        check_servers(document, &mut violations);
79        check_tags(document, &mut violations);
80        check_component_names(document, &mut violations);
81        self.check_paths(document, &mut violations);
82        check_opaque(document, &mut violations);
83        check_extensions("#", &document.extensions, &mut violations);
84
85        violations
86    }
87}
88
89impl Document {
90    /// Validates this document against the rules of `version`.
91    ///
92    /// # Errors
93    ///
94    /// Returns every [`Severity::Error`] violation found. Warnings are
95    /// discarded; use [`Validator::validate`] to see them.
96    pub fn validate(&self, version: SpecVersion) -> Result<(), Vec<Violation>> {
97        let errors: Vec<Violation> = Validator::new(version)
98            .validate(self)
99            .into_iter()
100            .filter(|violation| violation.severity == Severity::Error)
101            .collect();
102
103        if errors.is_empty() {
104            Ok(())
105        } else {
106            Err(errors)
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests;