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            schemas::check_unchecked_schemas,
32        },
33        violation::{Severity, SpecError, Violation},
34    },
35};
36
37/// Checks a document against the rules of a specification version.
38#[derive(Clone, Copy, Debug)]
39pub struct Validator {
40    version: SpecVersion,
41}
42
43impl Validator {
44    /// Creates a validator for `version`.
45    #[must_use]
46    pub fn new(version: SpecVersion) -> Self {
47        Self { version }
48    }
49
50    /// Collects every violation in `document`, most structural first.
51    #[must_use]
52    pub fn validate(&self, document: &Document) -> Vec<Violation> {
53        let mut violations = Vec::new();
54
55        // A document is checked against the version it claims to be. 3.2-only
56        // constructs are `#[cfg]`-gated, so a 3.1-only build cannot hold one --
57        // but a 3.2-capable build can, and being asked to validate such a
58        // document as 3.1 has to say so rather than pass a description 3.1
59        // cannot express. This is the same walk `Document::emit` refuses on,
60        // read here so that validating and emitting agree.
61        //
62        // `EmptyDocument` used to be the only rule this version was consulted
63        // for, and it is raised nowhere now: every version requires a document
64        // to carry at least one of `paths`, `components` or `webhooks`, and
65        // `Document::paths` is always serialized, so the condition is prevented
66        // by construction rather than reported after the fact.
67        if !self.version.supports_3_2() {
68            let blockers = crate::emit::downgrade::three_two_only_constructs(document);
69            if !blockers.is_empty() {
70                violations.push(Violation::error("#", SpecError::RequiresV3_2 { blockers }));
71            }
72        }
73
74        // A License Object setting both `identifier` and `url` used to be
75        // checked here. `License` now holds at most one of the two, so a
76        // document carrying both cannot reach this function: it fails to
77        // deserialize, and there is no way to build one.
78
79        check_servers(document, &mut violations);
80        check_tags(document, &mut violations);
81        check_component_names(document, &mut violations);
82        self.check_paths(document, &mut violations);
83        check_unchecked_schemas(document, &mut violations);
84        check_opaque(document, &mut violations);
85        check_extensions("#", &document.extensions, &mut violations);
86
87        violations
88    }
89}
90
91impl Document {
92    /// Validates this document against the rules of `version`.
93    ///
94    /// # Errors
95    ///
96    /// Returns every [`Severity::Error`] violation found. Warnings are
97    /// discarded; use [`Validator::validate`] to see them.
98    pub fn validate(&self, version: SpecVersion) -> Result<(), Vec<Violation>> {
99        let errors: Vec<Violation> = Validator::new(version)
100            .validate(self)
101            .into_iter()
102            .filter(|violation| violation.severity == Severity::Error)
103            .collect();
104
105        if errors.is_empty() {
106            Ok(())
107        } else {
108            Err(errors)
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests;