oapi_codegen/lower/validate.rs
1//! Collection of semantic problems into one report.
2//!
3//! The generator stops on an ambiguity and does not guess. See the "Design
4//! differences" section of the README. A spec with several independent problems
5//! therefore costs one run for each problem. [`Diagnostics`] collects independent
6//! problems, so one run reports all of them.
7//!
8//! Two rules keep this report useful.
9//!
10//! * The collector holds independent problems only. Some checks produce a result
11//! that the rest of the lowering pass needs. An unresolved `$ref` is one
12//! example. Such a check must still return at once. A run that continues past
13//! it reports later problems that are only effects of the first one.
14//! * One problem is reported as itself. [`Diagnostics::into_result`] returns the
15//! single [`Error`] unchanged. It builds an [`Error::Validation`] for two or
16//! more problems only. A caller that matches one variant still works, and the
17//! message for one problem carries no count.
18
19use crate::error::Error;
20use crate::error::Result;
21
22/// A collector of independent semantic problems. One run reports every problem
23/// that it finds and does not stop at the first one.
24///
25/// Add a problem with [`Diagnostics::push`]. Add the result of a fallible check
26/// with [`Diagnostics::check`]. End with [`Diagnostics::into_result`].
27#[derive(Debug, Default)]
28pub struct Diagnostics {
29 problems: Vec<Error>,
30}
31
32impl Diagnostics {
33 /// A collector with no problems recorded.
34 pub fn new() -> Self {
35 return Self { problems: Vec::new() };
36 }
37
38 /// Record a problem and continue.
39 ///
40 /// A problem that is itself a report opens and adds its problems one by one.
41 /// A check can collect on its own and give a report back, and a caller that
42 /// collects again would otherwise nest one report inside another. The reader
43 /// then gets a count that hides most of the list. [`Error::Validation`]
44 /// therefore holds leaf problems only, at one level.
45 pub fn push(&mut self, problem: Error) {
46 match problem {
47 Error::Validation { problems } => self.problems.extend(problems),
48 leaf => self.problems.push(leaf),
49 }
50 }
51
52 /// Record the error from a failed check and continue.
53 ///
54 /// Use this method for a check that returns a `Result`. That check keeps one
55 /// signature. A caller can then run it alone, or as part of this collection
56 /// pass.
57 pub fn check(&mut self, outcome: Result<()>) {
58 if let Err(problem) = outcome {
59 self.push(problem);
60 }
61 }
62
63 /// Whether no problems have been recorded.
64 pub fn is_empty(&self) -> bool {
65 return self.problems.is_empty();
66 }
67
68 /// How many problems have been recorded.
69 pub fn len(&self) -> usize {
70 return self.problems.len();
71 }
72
73 /// Convert the collected problems into a result.
74 ///
75 /// No problems give `Ok(())`. One problem returns as itself, so the caller
76 /// sees the error that a stop-at-first check gives. Two or more problems go
77 /// into an [`Error::Validation`] in discovery order.
78 pub fn into_result(mut self) -> Result<()> {
79 // `pop` and not an index, because the workspace denies
80 // `indexing_slicing`. This path also makes no new allocation.
81 if self.problems.len() == 1 {
82 match self.problems.pop() {
83 Some(single) => return Err(single),
84 None => return Ok(()),
85 }
86 }
87 if self.problems.is_empty() {
88 return Ok(());
89 }
90 return Err(Error::Validation {
91 problems: self.problems,
92 });
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 fn problem(name: &str) -> Error {
101 return Error::TypeNameCollision {
102 name: name.to_owned(),
103 artifact: "response enum".to_owned(),
104 hint: "rename it".to_owned(),
105 };
106 }
107
108 #[test]
109 fn empty_collector_is_ok() {
110 let diagnostics = Diagnostics::new();
111 assert!(diagnostics.is_empty());
112 assert!(diagnostics.into_result().is_ok());
113 }
114
115 #[test]
116 fn single_problem_is_reported_as_itself() {
117 // One problem must stay unwrapped. A caller matches one variant, and the
118 // message must carry no count.
119 let mut diagnostics = Diagnostics::new();
120 diagnostics.push(problem("Widget"));
121 let err = diagnostics.into_result().expect_err("one problem must fail");
122 assert!(
123 matches!(&err, Error::TypeNameCollision { name, .. } if name == "Widget"),
124 "expected the original variant unwrapped, got: {err:?}",
125 );
126 }
127
128 #[test]
129 fn multiple_problems_aggregate_in_order() {
130 let mut diagnostics = Diagnostics::new();
131 diagnostics.push(problem("Widget"));
132 diagnostics.check(Err(problem("Gadget")));
133 diagnostics.check(Ok(()));
134 assert_eq!(diagnostics.len(), 2, "a passing check must not be recorded");
135 let err = diagnostics.into_result().expect_err("two problems must fail");
136 let Error::Validation { problems } = &err else {
137 panic!("expected Validation, got: {err:?}");
138 };
139 assert_eq!(problems.len(), 2);
140 let message = err.to_string();
141 let widget = message.find("Widget").expect("message should list the first problem");
142 let gadget = message.find("Gadget").expect("message should list the second problem");
143 assert!(
144 widget < gadget,
145 "problems should be listed in discovery order: {message}"
146 );
147 assert!(
148 message.contains('2'),
149 "message should say how many problems were found: {message}",
150 );
151 }
152
153 #[test]
154 fn a_report_pushed_into_a_report_does_not_nest() {
155 // A schema-level check collects on its own and gives a report back. The
156 // loop over schemas collects again. Without opening the inner report the
157 // reader sees "found 2 problems", and one of the two hides the rest.
158 let mut inner = Diagnostics::new();
159 inner.push(problem("Widget"));
160 inner.push(problem("Gadget"));
161 let report = inner.into_result().expect_err("two problems must fail");
162
163 let mut outer = Diagnostics::new();
164 outer.push(report);
165 outer.push(problem("Doohickey"));
166 assert_eq!(outer.len(), 3, "the inner problems should be counted one by one");
167
168 let err = outer.into_result().expect_err("three problems must fail");
169 let Error::Validation { problems } = &err else {
170 panic!("expected Validation, got: {err:?}");
171 };
172 assert!(
173 problems
174 .iter()
175 .all(|entry| return !matches!(*entry, Error::Validation { .. })),
176 "a report must hold leaf problems only, got: {problems:?}",
177 );
178 }
179}