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;