1use serde_json::Value;
6
7#[derive(Clone)]
35pub(crate) struct ErrorPath {
36 start_index: Option<usize>,
37 literals: Vec<String>,
38 pub(crate) path: String,
39}
40
41impl ErrorPath {
43 fn new(path: String, literals: Vec<String>) -> Self {
44 Self {
45 path,
46 literals,
47 start_index: None,
48 }
49 }
50
51 pub(crate) fn feature(name: &str) -> Self {
52 Self::new(format!("features/{name}"), Default::default())
53 }
54
55 pub(crate) fn object(name: &str) -> Self {
56 Self::new(format!("objects/{name}"), Default::default())
57 }
58
59 pub(crate) fn example(&self, name: &str) -> Self {
60 Self::new(
61 format!("{}#examples[\"{name}\"]", &self.path),
62 self.literals.clone(),
63 )
64 }
65
66 pub(crate) fn property(&self, prop_key: &str) -> Self {
67 Self::new(
68 format!("{}.{prop_key}", &self.path),
69 append_quoted(&self.literals, prop_key),
70 )
71 }
72
73 pub(crate) fn enum_map_key(&self, enum_: &str, key: &str) -> Self {
74 Self::new(
75 format!("{}[{enum_}#{key}]", &self.path),
76 append(&self.literals, &["{".to_string(), format!("\"{key}\"")]),
77 )
78 }
79
80 pub(crate) fn map_key(&self, key: &str) -> Self {
81 Self::new(
82 format!("{}['{key}']", &self.path),
83 append(&self.literals, &["{".to_string(), format!("\"{key}\"")]),
84 )
85 }
86
87 pub(crate) fn array_index(&self, index: usize) -> Self {
88 let mut literals = append1(&self.literals, "[");
89 if index > 0 {
90 literals.extend_from_slice(&[",".repeat(index)]);
91 }
92 Self::new(format!("{}[{index}]", &self.path), literals)
93 }
94
95 pub(crate) fn object_value(&self, name: &str) -> Self {
96 Self::new(
97 format!("{}#{name}", &self.path),
98 append1(&self.literals, "{"),
99 )
100 }
101
102 pub(crate) fn open_brace(&self) -> Self {
103 Self::new(self.path.clone(), append1(&self.literals, "{"))
104 }
105
106 pub(crate) fn final_error_quoted(&self, highlight: &str) -> Self {
107 Self::new(self.path.clone(), append_quoted(&self.literals, highlight))
108 }
109
110 pub(crate) fn final_error_value(&self, value: &Value) -> Self {
111 let len = self.literals.len();
112 let mut literals = Vec::with_capacity(len * 2);
113 literals.extend_from_slice(self.literals.as_slice());
114 collect_path(&mut literals, value);
115
116 Self {
117 path: self.path.clone(),
118 literals,
119 start_index: Some(len),
120 }
121 }
122}
123
124fn collect_path(literals: &mut Vec<String>, value: &Value) {
125 match value {
126 Value::Bool(_) | Value::Number(_) | Value::Null => literals.push(value.to_string()),
127 Value::String(s) => literals.push(format!("\"{s}\"")),
128
129 Value::Array(array) => {
130 literals.push(String::from("["));
131 for v in array {
132 collect_path(literals, v);
133 }
134 literals.push(String::from("]"));
135 }
136
137 Value::Object(map) => {
138 literals.push(String::from("{"));
139 if let Some((k, v)) = map.iter().next_back() {
140 literals.push(format!("\"{k}\""));
141 collect_path(literals, v);
142 }
143 literals.push(String::from("}"));
144 }
145 }
146}
147
148impl ErrorPath {
150 pub(crate) fn error_token_abbr(&self) -> String {
151 match self.start_index {
152 Some(index) if index < self.literals.len() - 1 => {
153 let start = self
154 .literals
155 .get(index)
156 .map(String::as_str)
157 .unwrap_or_default();
158 let end = self.last_error_token().unwrap();
159 format!("{start}…{end}")
160 }
161 _ => self.last_error_token().unwrap().to_owned(),
162 }
163 }
164
165 pub(crate) fn last_error_token(&self) -> Option<&str> {
166 self.literals.last().map(String::as_str)
167 }
168}
169
170#[cfg(feature = "client-lib")]
171impl ErrorPath {
172 pub(crate) fn first_error_token(&self) -> Option<&str> {
173 if let Some(index) = self.start_index {
174 self.literals.get(index).map(String::as_str)
175 } else {
176 self.last_error_token()
177 }
178 }
179
180 pub(crate) fn error_span(&self, src: &str) -> crate::editing::CursorSpan {
185 use crate::editing::CursorPosition;
186 let mut lines = src.lines().peekable();
187 let last_token = self.last_error_token().unwrap();
188 if let Some(index) = self.start_index {
189 let path_to_first = self.literals[..index + 1].iter().map(String::as_str);
190 let rest = self.literals[index + 1..].iter().map(String::as_str);
191
192 let pos = line_col_from_lines(&mut lines, (0, 0), path_to_first);
193 let from: CursorPosition = pos.into();
194
195 let to: CursorPosition = line_col_from_lines(&mut lines, pos, rest).into();
196
197 from + (to + last_token)
198 } else {
199 let from: CursorPosition =
200 line_col_from_lines(&mut lines, (0, 0), self.literals.iter().map(String::as_str))
201 .into();
202 from + last_token
203 }
204 }
205}
206
207fn append(original: &[String], new: &[String]) -> Vec<String> {
208 let mut clone = Vec::with_capacity(original.len() + new.len());
209 clone.extend_from_slice(original);
210 clone.extend_from_slice(new);
211 clone
212}
213
214fn append1(original: &[String], new: &str) -> Vec<String> {
215 let mut clone = Vec::with_capacity(original.len() + 1);
216 clone.extend_from_slice(original);
217 clone.push(new.to_string());
218 clone
219}
220
221fn append_quoted(original: &[String], new: &str) -> Vec<String> {
222 append1(original, &format!("\"{new}\""))
223}
224
225#[cfg(feature = "client-lib")]
226fn line_col_from_lines<'a>(
227 lines: &mut std::iter::Peekable<impl Iterator<Item = &'a str>>,
228 start: (usize, usize),
229 path: impl Iterator<Item = &'a str>,
230) -> (usize, usize) {
231 let (mut line_no, mut col_no) = start;
232
233 let mut first_match = col_no == 0;
235
236 for p in path {
237 loop {
238 if let Some(line) = lines.peek() {
239 let start = if first_match { 0 } else { col_no + 1 };
245
246 if let Some(i) = find_index(line, p, start) {
247 col_no = i;
248 first_match = false;
249 break;
250 } else if lines.next().is_some() {
251 line_no += 1;
253 first_match = true;
254 col_no = 0;
255 }
256 } else {
257 return (0, 0);
259 }
260 }
261 }
262
263 (line_no, col_no)
264}
265
266#[cfg(feature = "client-lib")]
269fn find_index(line: &str, pattern: &str, start: usize) -> Option<usize> {
270 use unicode_segmentation::UnicodeSegmentation;
271 let line: Vec<&str> = UnicodeSegmentation::graphemes(line, true).collect();
272 let line_from_start = &line[start..];
273
274 let pattern: Vec<&str> = UnicodeSegmentation::graphemes(pattern, true).collect();
275 let pattern = pattern.as_slice();
276
277 line_from_start
278 .windows(pattern.len())
279 .position(|window| window == pattern)
280 .map(|i| i + start)
281}
282
283#[cfg(feature = "client-lib")]
284#[cfg(test)]
285mod construction_tests {
286 use serde_json::json;
287
288 use super::ErrorPath;
289
290 #[test]
291 fn test_property() {
292 let path = ErrorPath::feature("my-feature").property("my-property");
293 assert_eq!("features/my-feature.my-property", &path.path);
294 assert_eq!(&["\"my-property\""], path.literals.as_slice());
295
296 let path = ErrorPath::object("MyObject").property("my-property");
297 assert_eq!("objects/MyObject.my-property", &path.path);
298 assert_eq!(&["\"my-property\""], path.literals.as_slice());
299 }
300
301 #[test]
302 fn test_map_key() {
303 let path = ErrorPath::feature("my-feature")
304 .property("my-map")
305 .map_key("my-key");
306 assert_eq!("features/my-feature.my-map['my-key']", &path.path);
307 assert_eq!(&["\"my-map\"", "{", "\"my-key\""], path.literals.as_slice());
308 }
309
310 #[test]
311 fn test_enum_map_key() {
312 let path = ErrorPath::feature("my-feature")
313 .property("my-map")
314 .enum_map_key("MyEnum", "my-variant");
315 assert_eq!("features/my-feature.my-map[MyEnum#my-variant]", &path.path);
316 assert_eq!(
317 &["\"my-map\"", "{", "\"my-variant\""],
318 path.literals.as_slice()
319 );
320 }
321
322 #[test]
323 fn test_array_index() {
324 let path = ErrorPath::feature("my-feature")
325 .property("my-array")
326 .array_index(1);
327 assert_eq!("features/my-feature.my-array[1]", &path.path);
328 assert_eq!(&["\"my-array\"", "[", ","], path.literals.as_slice());
329
330 let path = ErrorPath::feature("my-feature")
331 .property("my-array")
332 .array_index(0);
333 assert_eq!("features/my-feature.my-array[0]", &path.path);
334 assert_eq!(&["\"my-array\"", "["], path.literals.as_slice());
335 }
336
337 #[test]
338 fn test_object_value() {
339 let path = ErrorPath::feature("my-feature")
340 .property("my-object")
341 .object_value("MyObject");
342 assert_eq!("features/my-feature.my-object#MyObject", &path.path);
343 assert_eq!(&["\"my-object\"", "{"], path.literals.as_slice());
344 }
345
346 #[test]
347 fn test_final_error() {
348 let path = ErrorPath::feature("messaging")
350 .property("messages")
351 .map_key("my-message")
352 .object_value("MessageData")
353 .property("is-control")
354 .final_error_value(&json!(1));
355 assert_eq!(
356 "features/messaging.messages['my-message']#MessageData.is-control",
357 &path.path
358 );
359 assert_eq!(
360 &[
361 "\"messages\"",
362 "{",
363 "\"my-message\"",
364 "{",
365 "\"is-control\"",
366 "1"
367 ],
368 path.literals.as_slice()
369 );
370
371 let path = ErrorPath::feature("homescreen")
373 .property("sections-enabled")
374 .enum_map_key("HomeScreenSection", "pocket")
375 .final_error_value(&json!(1));
376 assert_eq!(
377 "features/homescreen.sections-enabled[HomeScreenSection#pocket]",
378 &path.path
379 );
380
381 assert_eq!(
382 &["\"sections-enabled\"", "{", "\"pocket\"", "1"],
383 path.literals.as_slice()
384 );
385 }
386
387 #[test]
388 fn test_final_error_value_scalars() {
389 let path = ErrorPath::feature("my-feature").property("is-enabled");
390
391 let observed = {
392 let value = json!(true);
393 path.final_error_value(&value)
394 };
395 assert_eq!(observed.literals.as_slice(), &["\"is-enabled\"", "true"]);
396
397 let observed = {
398 let value = json!(13);
399 path.final_error_value(&value)
400 };
401 assert_eq!(observed.literals.as_slice(), &["\"is-enabled\"", "13"]);
402
403 let observed = {
404 let value = json!("string");
405 path.final_error_value(&value)
406 };
407 assert_eq!(
408 observed.literals.as_slice(),
409 &["\"is-enabled\"", "\"string\""]
410 );
411 }
412
413 #[test]
414 fn test_final_error_value_arrays() {
415 let path = ErrorPath::feature("my-feature").property("is-enabled");
416
417 let observed = {
418 let value = json!([]);
419 let o = path.final_error_value(&value);
420 assert_eq!(o.first_error_token(), Some("["));
421 o
422 };
423 assert_eq!(observed.literals.as_slice(), &["\"is-enabled\"", "[", "]"]);
424
425 let observed = {
426 let value = json!([1, 2]);
427 let o = path.final_error_value(&value);
428 assert_eq!(o.first_error_token(), Some("["));
429 o
430 };
431 assert_eq!(
432 observed.literals.as_slice(),
433 &["\"is-enabled\"", "[", "1", "2", "]"]
434 );
435 }
436
437 #[test]
438 fn test_final_error_value_objects() {
439 let path = ErrorPath::feature("my-feature").property("is-enabled");
440
441 let observed = {
442 let value = json!({});
443 let o = path.final_error_value(&value);
444 assert_eq!(o.first_error_token(), Some("{"));
445 o
446 };
447 assert_eq!(observed.literals.as_slice(), &["\"is-enabled\"", "{", "}"]);
448
449 let observed = {
450 let value = json!({"last": true});
451 let o = path.final_error_value(&value);
452 assert_eq!(o.first_error_token(), Some("{"));
453 o
454 };
455 assert_eq!(
456 observed.literals.as_slice(),
457 &["\"is-enabled\"", "{", "\"last\"", "true", "}"]
458 );
459
460 let observed = {
461 let value = json!({"first": true, "last": true});
462 let o = path.final_error_value(&value);
463 assert_eq!(o.first_error_token(), Some("{"));
464 o
465 };
466 assert_eq!(
467 observed.literals.as_slice(),
468 &["\"is-enabled\"", "{", "\"last\"", "true", "}"]
469 );
470 }
471}
472
473#[cfg(feature = "client-lib")]
474#[cfg(test)]
475mod line_col_tests {
476
477 use super::*;
478 use crate::error::Result;
479
480 fn line_col<'a>(src: &'a str, path: impl Iterator<Item = &'a str>) -> (usize, usize) {
481 let mut lines = src.lines().peekable();
482 line_col_from_lines(&mut lines, (0, 0), path)
483 }
484
485 #[test]
486 fn test_find_err() -> Result<()> {
487 fn do_test(s: &str, path: &[&str], expected: (usize, usize)) {
488 let p = path.last().unwrap();
489 let path = path.iter().cloned();
490 let from = line_col(s, path);
491 assert_eq!(from, expected, "Can't find \"{p}\" at {expected:?} in {s}");
492 }
493
494 fn do_multi(s: &[&str], path: &[&str], expected: (usize, usize)) {
495 let s = s.join("\n");
496 do_test(&s, path, expected);
497 }
498
499 do_test("ab cd", &["cd"], (0, 3));
500 do_test("ab cd", &["ab", "cd"], (0, 3));
501 do_test("áط ¢đ εƒ gի", &["áط", "¢đ"], (0, 3));
502
503 do_test("ab ab", &["ab"], (0, 0));
504 do_test("ab ab", &["ab", "ab"], (0, 3));
505
506 do_multi(
507 &["ab xx cd", "xx ef xx gh", "ij xx"],
508 &["ab", "cd", "gh", "xx"],
509 (2, 3),
510 );
511
512 do_multi(
513 &[
514 "{", " boolean: true,", " object: {", " integer: \"string\"", " }", "}", ],
521 &["object", "integer", "\"string\""],
522 (3, 13),
523 );
524
525 do_multi(
527 &[
528 "{", " boolean: true,", " object: {", " integer: 1,", " astring: \"string\"", " },", " integer: \"string\"", "}", ],
537 &["integer", "\"string\""],
538 (4, 13),
539 );
540
541 do_multi(&["áط ab", "¢đ cd", "εƒ ef", "gh gի"], &["áط", "cd"], (1, 3));
543
544 do_multi(
546 &[
547 "Wàłţż, Waltz,",
548 "bâđ bad",
549 "ņÿmƥĥ, nymph,",
550 "ƒőŕ for",
551 "qüíĉķ quick",
552 "ĵíğş jigs",
553 "vęx vex",
554 ],
555 &["bad", "nymph"],
556 (2, 7),
557 );
558
559 Ok(())
560 }
561
562 #[test]
563 fn test_find_index_from() -> Result<()> {
564 assert_eq!(find_index("012345601", "01", 0), Some(0));
565 assert_eq!(find_index("012345601", "01", 1), Some(7));
566 assert_eq!(find_index("012345602", "01", 1), None);
567 assert_eq!(find_index("åéîø token", "token", 0), Some(5));
568 Ok(())
569 }
570}
571
572#[cfg(feature = "client-lib")]
573#[cfg(test)]
574mod integration_tests {
575
576 use serde_json::json;
577
578 use super::*;
579
580 fn test_error_span(src: &[&str], path: &ErrorPath, from: (usize, usize), to: (usize, usize)) {
581 test_error_span_string(src.join("\n"), path, from, to);
582 }
583
584 fn test_error_span_oneline(
585 src: &[&str],
586 path: &ErrorPath,
587 from: (usize, usize),
588 to: (usize, usize),
589 ) {
590 test_error_span_string(src.join(""), path, from, to);
591 }
592
593 fn test_error_span_string(
594 src: String,
595 path: &ErrorPath,
596 from: (usize, usize),
597 to: (usize, usize),
598 ) {
599 let observed = path.error_span(src.as_str());
600
601 assert_eq!(
602 observed.from,
603 from.into(),
604 "Incorrectly found first error token \"{p}\" starts at {from:?} in {src}",
605 from = observed.from,
606 p = path.first_error_token().unwrap()
607 );
608 assert_eq!(
609 observed.to,
610 to.into(),
611 "Incorrectly found last error token \"{p}\" ends at {to:?} in {src}",
612 p = path.last_error_token().unwrap(),
613 to = observed.to,
614 );
615 }
616
617 #[test]
618 fn test_last_token() {
619 let path = ErrorPath::feature("test-feature")
620 .property("integer")
621 .final_error_quoted("string");
622 let src = &[
623 r#"{"#, r#" "boolean": true,"#, r#" "integer": "string""#, r#"}"#, ];
629
630 test_error_span(src, &path, (2, 13), (2, 21));
631 test_error_span_oneline(src, &path, (0, 32), (0, 32 + "string".len() + 2))
632 }
633
634 #[test]
635 fn test_type_mismatch_scalar() {
636 let path = ErrorPath::feature("test-feature")
637 .property("boolean")
638 .final_error_value(&json!(13));
639
640 let src = &[
641 r#"{"#, r#" "boolean": 13,"#, r#" "integer": 1"#, r#"}"#, ];
647 test_error_span(src, &path, (1, 13), (1, 13 + 2));
648 }
649
650 #[test]
651 fn test_type_mismatch_error_on_one_line() {
652 let path = ErrorPath::feature("test-feature")
653 .property("integer")
654 .final_error_value(&json!({
655 "string": "string"
656 }));
657
658 let src = &[
659 r#"{"#, r#" "integer": { "string": "string" },"#, r#" "short": 1,"#, r#" "boolean": true,"#, r#"}"#, ];
666 test_error_span(
667 src,
668 &path,
669 (1, 13),
670 (1, 13 + r#"{ "string": "string" }"#.len()),
671 );
672
673 test_error_span_oneline(
674 src,
675 &path,
676 (0, 14),
677 (0, 14 + r#"{ "string": "string" }"#.len()),
678 );
679 }
680
681 #[test]
682 fn test_type_mismatch_error_on_multiple_lines() {
683 let path = ErrorPath::feature("test-feature").final_error_value(&json!({}));
684 let src = &[
685 r#"{ "#, r#" "#, r#" "#, r#" "#, r#"} "#, ];
692 test_error_span(src, &path, (0, 0), (4, 1));
693 }
694
695 #[test]
696 fn test_error_abbr() {
697 let path = ErrorPath::feature("test_feature").final_error_value(&json!(true));
698 assert_eq!(path.error_token_abbr().as_str(), "true");
699
700 let path = ErrorPath::feature("test_feature").final_error_value(&json!(42));
701 assert_eq!(path.error_token_abbr().as_str(), "42");
702
703 let path = ErrorPath::feature("test_feature").final_error_value(&json!("string"));
704 assert_eq!(path.error_token_abbr().as_str(), "\"string\"");
705
706 let path = ErrorPath::feature("test_feature").final_error_value(&json!([]));
707 assert_eq!(path.error_token_abbr().as_str(), "[…]");
708
709 let path = ErrorPath::feature("test_feature").final_error_value(&json!({}));
710 assert_eq!(path.error_token_abbr().as_str(), "{…}");
711
712 let path = ErrorPath::feature("test_feature").final_error_quoted("foo");
713 assert_eq!(path.error_token_abbr().as_str(), "\"foo\"");
714 }
715}