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;