1use std::collections::BTreeMap;
10
11use serde_json::Value;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
15pub enum ResultsLevel {
16 Basic,
18 Detailed,
20 Verbose,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct SchemaResult {
27 pub is_match: bool,
28 pub message: String,
30 pub evaluation_location: String,
32 pub schema_evaluation_location: String,
34 pub document_evaluation_location: String,
36}
37
38pub(crate) enum Message<'a> {
40 None,
41 Static(&'static str),
42 Lazy(&'a dyn Fn() -> String),
43}
44
45struct Frame {
46 eval_len: usize,
47 schema_path: String,
48 doc_len: usize,
49 commit_index: usize,
50 rows_start: usize,
51}
52
53pub struct JsonSchemaResultsCollector {
55 level: ResultsLevel,
56 committed: Vec<SchemaResult>,
57 frames: Vec<Frame>,
58 pending: Vec<SchemaResult>,
60 eval_path: String,
61 schema_path: String,
62 doc_path: String,
63}
64
65pub fn encode_pointer_segment(segment: &str) -> std::borrow::Cow<'_, str> {
67 crate::uri::escape_pointer_token(segment)
68}
69
70impl JsonSchemaResultsCollector {
71 pub fn new(level: ResultsLevel) -> Self {
73 JsonSchemaResultsCollector {
74 level,
75 committed: Vec::new(),
76 frames: Vec::new(),
77 pending: Vec::new(),
78 eval_path: String::new(),
79 schema_path: String::new(),
80 doc_path: String::new(),
81 }
82 }
83
84 pub fn level(&self) -> ResultsLevel {
85 self.level
86 }
87
88 pub fn results(&self) -> &[SchemaResult] {
90 &self.committed
91 }
92
93 pub fn take_results(&mut self) -> Vec<SchemaResult> {
95 std::mem::take(&mut self.committed)
96 }
97
98 pub(crate) fn begin_child_context(
104 &mut self,
105 eval_segment: Option<&str>,
106 schema_location: Option<&str>,
107 doc_segment: Option<&str>,
108 ) {
109 self.frames.push(Frame {
110 eval_len: self.eval_path.len(),
111 schema_path: self.schema_path.clone(),
112 doc_len: self.doc_path.len(),
113 commit_index: self.committed.len(),
114 rows_start: self.pending.len(),
115 });
116 if let Some(s) = eval_segment {
117 self.eval_path.push('/');
118 self.eval_path.push_str(s);
119 }
120 if let Some(s) = schema_location {
121 self.schema_path.clear();
122 self.schema_path.push_str(s);
123 }
124 if let Some(s) = doc_segment {
125 self.doc_path.push('/');
126 self.doc_path.push_str(s);
127 }
128 }
129
130 pub(crate) fn commit_child_context(&mut self, parent_is_match: bool, child_is_match: bool, message: Message<'_>) {
133 if parent_is_match && self.level != ResultsLevel::Verbose {
134 self.pop_child_context();
135 return;
136 }
137 let row =
138 self.row(child_is_match, message, self.eval_path.clone(), self.schema_path.clone(), self.doc_path.clone());
139 self.pending.push(row);
140 let frame = self.frames.pop().unwrap();
141 let rows = self.pending.split_off(frame.rows_start);
142 self.committed.extend(rows.into_iter().rev());
143 self.restore(frame);
144 }
145
146 pub(crate) fn pop_child_context(&mut self) {
148 let frame = self.frames.pop().unwrap();
149 self.committed.truncate(frame.commit_index);
150 self.pending.truncate(frame.rows_start);
151 self.restore(frame);
152 }
153
154 pub(crate) fn evaluated_keyword(&mut self, is_match: bool, message: Message<'_>, keyword: &str) {
155 if !is_match || self.level == ResultsLevel::Verbose {
156 let k = encode_pointer_segment(keyword);
157 let row = self.row(
158 is_match,
159 message,
160 format!("{}/{k}", self.eval_path),
161 format!("{}/{k}", self.schema_path),
162 self.doc_path.clone(),
163 );
164 self.pending.push(row);
165 }
166 }
167
168 pub(crate) fn evaluated_keyword_for_property(
169 &mut self,
170 is_match: bool,
171 message: Message<'_>,
172 property_name: &str,
173 keyword: &str,
174 ) {
175 if !is_match || self.level == ResultsLevel::Verbose {
176 let k = encode_pointer_segment(keyword);
177 let row = self.row(
178 is_match,
179 message,
180 format!("{}/{k}", self.eval_path),
181 format!("{}/{k}", self.schema_path),
182 format!("{}/{}", self.doc_path, encode_pointer_segment(property_name)),
183 );
184 self.pending.push(row);
185 }
186 }
187
188 pub(crate) fn ignored_keyword(&mut self, message: Message<'_>, keyword: &str) {
190 if self.level == ResultsLevel::Verbose {
191 let row = self.row(
192 true,
193 message,
194 format!("{}/{}", self.eval_path, encode_pointer_segment(keyword)),
195 self.schema_path.clone(),
196 self.doc_path.clone(),
197 );
198 self.pending.push(row);
199 }
200 }
201
202 pub(crate) fn evaluated_boolean_schema(&mut self, is_match: bool) {
203 if !is_match || self.level == ResultsLevel::Verbose {
204 let row = self.row(
205 is_match,
206 Message::None,
207 self.eval_path.clone(),
208 self.schema_path.clone(),
209 self.doc_path.clone(),
210 );
211 self.pending.push(row);
212 }
213 }
214
215 fn row(
216 &self,
217 is_match: bool,
218 message: Message<'_>,
219 evaluation_location: String,
220 schema_evaluation_location: String,
221 document_evaluation_location: String,
222 ) -> SchemaResult {
223 let with_text = self.level == ResultsLevel::Verbose || (!is_match && self.level >= ResultsLevel::Detailed);
224 let message = if with_text {
225 match message {
226 Message::None => String::new(),
227 Message::Static(s) => s.to_string(),
228 Message::Lazy(f) => f(),
229 }
230 } else {
231 String::new()
232 };
233 SchemaResult {
234 is_match,
235 message,
236 evaluation_location,
237 schema_evaluation_location,
238 document_evaluation_location,
239 }
240 }
241
242 fn restore(&mut self, frame: Frame) {
243 self.eval_path.truncate(frame.eval_len);
244 self.schema_path = frame.schema_path;
245 self.doc_path.truncate(frame.doc_len);
246 }
247}
248
249#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct Annotation {
252 pub instance_location: String,
254 pub keyword: String,
255 pub schema_location: String,
257 pub value: String,
259}
260
261pub fn enumerate_annotations(collector: &JsonSchemaResultsCollector) -> impl Iterator<Item = Annotation> + '_ {
263 collector.results().iter().filter_map(|r| {
264 if !r.is_match || r.message.is_empty() {
265 return None;
266 }
267 let slash = r.evaluation_location.rfind('/')?;
268 if r.evaluation_location == r.schema_evaluation_location {
269 return None;
270 }
271 let keyword = &r.evaluation_location[slash + 1..];
272 let first = r.message.as_bytes()[0];
273 if keyword.is_empty()
274 || !(matches!(first, b'"' | b'{' | b'[' | b't' | b'f' | b'n' | b'-') || first.is_ascii_digit())
275 {
276 return None;
277 }
278 Some(Annotation {
279 instance_location: r.document_evaluation_location.clone(),
280 keyword: keyword.to_string(),
281 schema_location: r.schema_evaluation_location.clone(),
282 value: r.message.clone(),
283 })
284 })
285}
286
287pub fn schema_location_fragment(schema_location: &str) -> String {
289 let mut out = String::from("#");
290 for &byte in schema_location.as_bytes() {
291 let c = byte as char;
292 if byte < 128 && (c.is_ascii_alphanumeric() || "-._~!$&'()*+,;=:@/?".contains(c)) {
293 out.push(c);
294 } else {
295 out.push_str(&format!("%{byte:02X}"));
296 }
297 }
298 out
299}
300
301pub fn collect_annotations(
304 collector: &JsonSchemaResultsCollector,
305) -> BTreeMap<String, BTreeMap<String, BTreeMap<String, Value>>> {
306 let mut out: BTreeMap<String, BTreeMap<String, BTreeMap<String, Value>>> = BTreeMap::new();
307 for a in enumerate_annotations(collector) {
308 let value = serde_json::from_str(&a.value).unwrap_or(Value::String(a.value.clone()));
309 out.entry(a.instance_location)
310 .or_default()
311 .entry(a.keyword)
312 .or_default()
313 .insert(schema_location_fragment(&a.schema_location), value);
314 }
315 out
316}