1use std::fmt;
4use std::str::FromStr;
5
6#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
8#[non_exhaustive]
9pub enum PathSeg {
10 Key(Box<str>),
12 Index(usize),
14}
15
16impl From<&str> for PathSeg {
17 fn from(key: &str) -> Self {
18 Self::Key(key.into())
19 }
20}
21
22impl From<String> for PathSeg {
23 fn from(key: String) -> Self {
24 Self::Key(key.into())
25 }
26}
27
28impl From<usize> for PathSeg {
29 fn from(index: usize) -> Self {
30 Self::Index(index)
31 }
32}
33
34#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct AuthoredPath(Vec<PathSeg>);
44
45impl AuthoredPath {
46 pub fn root() -> Self {
48 Self::default()
49 }
50
51 pub fn segs(&self) -> &[PathSeg] {
53 &self.0
54 }
55
56 pub fn is_root(&self) -> bool {
58 self.0.is_empty()
59 }
60
61 pub fn push(&mut self, seg: impl Into<PathSeg>) {
63 self.0.push(seg.into());
64 }
65
66 pub fn pop(&mut self) -> Option<PathSeg> {
68 self.0.pop()
69 }
70
71 pub fn join(&self, seg: impl Into<PathSeg>) -> Self {
73 let mut path = self.clone();
74 path.push(seg);
75 path
76 }
77
78 pub fn extend(&self, rest: &Self) -> Self {
80 let mut path = self.clone();
81 path.0.extend(rest.0.iter().cloned());
82 path
83 }
84}
85
86fn needs_quotes(key: &str) -> bool {
88 key.is_empty() || key.contains(['.', '[', ']', '"'])
89}
90
91impl fmt::Display for AuthoredPath {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 for (i, seg) in self.0.iter().enumerate() {
94 match seg {
95 PathSeg::Key(key) if needs_quotes(key) => {
96 let quoted = serde_json::to_string(&**key).map_err(|_| fmt::Error)?;
97 write!(f, "[{quoted}]")?;
98 }
99 PathSeg::Key(key) if i == 0 => f.write_str(key)?,
100 PathSeg::Key(key) => write!(f, ".{key}")?,
101 PathSeg::Index(index) => write!(f, "[{index}]")?,
102 }
103 }
104 Ok(())
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct ParsePathError(String);
111
112impl fmt::Display for ParsePathError {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(f, "not an authored path: {:?}", self.0)
115 }
116}
117
118impl std::error::Error for ParsePathError {}
119
120impl FromStr for AuthoredPath {
121 type Err = ParsePathError;
122
123 fn from_str(text: &str) -> Result<Self, Self::Err> {
126 let bad = || ParsePathError(text.to_string());
127 let mut path = Self::root();
128 let mut rest = text;
129 let mut first = true;
130 while !rest.is_empty() {
131 if let Some(after) = rest.strip_prefix("[\"") {
132 let mut escaped = false;
134 let end = after
135 .char_indices()
136 .find(|&(_, c)| {
137 let close = c == '"' && !escaped;
138 escaped = c == '\\' && !escaped;
139 close
140 })
141 .map(|(at, _)| at)
142 .ok_or_else(bad)?;
143 let literal = &rest[1..end + 3];
145 let key: String = serde_json::from_str(literal).map_err(|_| bad())?;
146 path.push(key);
147 rest = after[end + 1..].strip_prefix(']').ok_or_else(bad)?;
148 } else if let Some(after) = rest.strip_prefix('[') {
149 let end = after.find(']').ok_or_else(bad)?;
150 let index = after[..end].parse::<usize>().map_err(|_| bad())?;
151 path.push(index);
152 rest = &after[end + 1..];
153 } else {
154 let after = if first {
155 rest
156 } else {
157 rest.strip_prefix('.').ok_or_else(bad)?
158 };
159 let end = after.find(['.', '[']).unwrap_or(after.len());
160 let key = &after[..end];
161 if needs_quotes(key) {
162 return Err(bad());
163 }
164 path.push(key);
165 rest = &after[end..];
166 }
167 first = false;
168 }
169 Ok(path)
170 }
171}
172
173impl serde::Serialize for AuthoredPath {
174 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
175 serializer.collect_str(self)
176 }
177}
178
179macro_rules! issue_codes {
185 ($($(#[$doc:meta])* $code:ident = $text:literal, $severity:ident;)*) => {
186 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
190 #[non_exhaustive]
191 pub enum IssueCode {
192 $($(#[$doc])* $code,)*
193 }
194
195 impl IssueCode {
196 pub const ALL: &'static [Self] = &[$(Self::$code),*];
198
199 pub fn as_str(self) -> &'static str {
202 match self {
203 $(Self::$code => $text,)*
204 }
205 }
206
207 pub fn severity(self) -> Severity {
210 match self {
211 $(Self::$code => Severity::$severity,)*
212 }
213 }
214 }
215 };
216}
217
218issue_codes! {
219 EmptyWorkflowId = "EMPTY_WORKFLOW_ID", Rejected;
221 NoTasks = "NO_TASKS", Rejected;
223 MissingStepId = "MISSING_STEP_ID", Rejected;
225 DuplicateStepId = "DUPLICATE_STEP_ID", Rejected;
227 EmptyGroup = "EMPTY_GROUP", Rejected;
229 GroupTooDeep = "GROUP_TOO_DEEP", Rejected;
232 MissingFunction = "MISSING_FUNCTION", Rejected;
234 InvalidFunctionName = "INVALID_FUNCTION_NAME", Rejected;
236 InvalidTerminal = "INVALID_TERMINAL", Rejected;
238 UnguardedValidation = "UNGUARDED_VALIDATION", Advisory;
248 InvalidHaltOn = "INVALID_HALT_ON", Rejected;
252 GroupContinueOnError = "GROUP_CONTINUE_ON_ERROR", Advisory;
262 InvalidMapping = "INVALID_MAPPING", Rejected;
268 NullMapping = "NULL_MAPPING", Advisory;
279 LoopIncrementTooSmall = "LOOP_INCREMENT_TOO_SMALL", Rejected;
281 LoopBoundEmpty = "LOOP_BOUND_EMPTY", Rejected;
283 LoopCounterInvalid = "LOOP_COUNTER_INVALID", Rejected;
285 LoopSlotInvalid = "LOOP_SLOT_INVALID", Rejected;
287 LoopItemWithoutOver = "LOOP_ITEM_WITHOUT_OVER", Rejected;
290 LoopSlotCollision = "LOOP_SLOT_COLLISION", Rejected;
294 LoopOverInvalid = "LOOP_OVER_INVALID", Rejected;
298 InvalidForEach = "INVALID_FOR_EACH", Rejected;
305 UnknownFunction = "UNKNOWN_FUNCTION", Rejected;
308 InputParse = "INPUT_PARSE", Rejected;
311 TemplateCompile = "TEMPLATE_COMPILE", Rejected;
313 UnknownSecret = "UNKNOWN_SECRET", Rejected;
317 SecretInMessageWrite = "SECRET_IN_MESSAGE_WRITE", Rejected;
323 DuplicateTemplateKey = "DUPLICATE_TEMPLATE_KEY", Rejected;
329 EscapedTemplateKey = "ESCAPED_TEMPLATE_KEY", Advisory;
340 ParseFailed = "PARSE_FAILED", Rejected;
344 MixedSpelling = "MIXED_SPELLING", Rejected;
349 ExtCollision = "EXT_COLLISION", Rejected;
353 LikelyTypo = "LIKELY_TYPO", Defect;
357 UnknownKey = "UNKNOWN_KEY", Advisory;
361 V3Spelling = "V3_SPELLING", Advisory;
365 NullCondition = "NULL_CONDITION", Advisory;
371 PayloadVar = "PAYLOAD_VAR", Defect;
375 OperatorInDocument = "OPERATOR_IN_DOCUMENT", Advisory;
379 SecretInDocument = "SECRET_IN_DOCUMENT", Rejected;
383 InvalidPath = "INVALID_PATH", Rejected;
387 UnresolvedReference = "UNRESOLVED_REFERENCE", Defect;
396 InvalidRollout = "INVALID_ROLLOUT", Defect;
403}
404
405impl fmt::Display for IssueCode {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 f.write_str(self.as_str())
408 }
409}
410
411pub(crate) fn any_rejected(issues: &[Issue]) -> bool {
414 issues.iter().any(|i| i.severity == Severity::Rejected)
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
419pub struct ParseIssueCodeError(String);
420
421impl fmt::Display for ParseIssueCodeError {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 write!(f, "unknown issue code: {:?}", self.0)
424 }
425}
426
427impl std::error::Error for ParseIssueCodeError {}
428
429impl FromStr for IssueCode {
430 type Err = ParseIssueCodeError;
431
432 fn from_str(text: &str) -> Result<Self, Self::Err> {
434 Self::ALL
435 .iter()
436 .copied()
437 .find(|code| code.as_str() == text)
438 .ok_or_else(|| ParseIssueCodeError(text.to_string()))
439 }
440}
441
442#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
444#[non_exhaustive]
445pub enum Severity {
446 Rejected,
448 Defect,
450 Advisory,
452}
453
454impl Severity {
455 const ALL: [Self; 3] = [Self::Rejected, Self::Defect, Self::Advisory];
456
457 pub fn as_str(self) -> &'static str {
459 match self {
460 Self::Rejected => "REJECTED",
461 Self::Defect => "DEFECT",
462 Self::Advisory => "ADVISORY",
463 }
464 }
465}
466
467impl fmt::Display for Severity {
468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469 f.write_str(self.as_str())
470 }
471}
472
473#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
475#[non_exhaustive]
476pub enum StepKind {
477 Task,
479 Group,
481 Setup,
483}
484
485impl StepKind {
486 const ALL: [Self; 3] = [Self::Task, Self::Group, Self::Setup];
487
488 pub fn as_str(self) -> &'static str {
490 match self {
491 Self::Task => "task",
492 Self::Group => "group",
493 Self::Setup => "setup",
494 }
495 }
496}
497
498fn from_text<'de, D, T, E>(
500 deserializer: D,
501 read: impl FnOnce(&str) -> Result<T, E>,
502) -> Result<T, D::Error>
503where
504 D: serde::Deserializer<'de>,
505 E: fmt::Display,
506{
507 let text = <String as serde::Deserialize>::deserialize(deserializer)?;
508 read(&text).map_err(serde::de::Error::custom)
509}
510
511fn named<T: Copy>(
513 all: &[T],
514 as_str: fn(T) -> &'static str,
515 what: &str,
516 text: &str,
517) -> Result<T, String> {
518 all.iter()
519 .copied()
520 .find(|v| as_str(*v) == text)
521 .ok_or_else(|| format!("unknown {what}: {text:?}"))
522}
523
524impl<'de> serde::Deserialize<'de> for AuthoredPath {
525 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
526 from_text(deserializer, str::parse)
527 }
528}
529
530impl serde::Serialize for IssueCode {
531 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
532 serializer.serialize_str(self.as_str())
533 }
534}
535
536impl<'de> serde::Deserialize<'de> for IssueCode {
537 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
538 from_text(deserializer, str::parse)
539 }
540}
541
542impl serde::Serialize for Severity {
543 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
544 serializer.serialize_str(self.as_str())
545 }
546}
547
548impl<'de> serde::Deserialize<'de> for Severity {
549 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
550 from_text(deserializer, |text| {
551 named(&Self::ALL, Self::as_str, "severity", text)
552 })
553 }
554}
555
556impl serde::Serialize for StepKind {
557 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
558 serializer.serialize_str(self.as_str())
559 }
560}
561
562impl<'de> serde::Deserialize<'de> for StepKind {
563 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
564 from_text(deserializer, |text| {
565 named(&Self::ALL, Self::as_str, "step kind", text)
566 })
567 }
568}
569
570#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
576#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
577#[non_exhaustive]
578pub struct Issue {
579 pub code: IssueCode,
581 pub severity: Severity,
583 pub path: AuthoredPath,
585 pub step_kind: Option<StepKind>,
587 pub step_id: Option<String>,
589 pub field: Option<String>,
591 pub message: String,
593}
594
595impl Issue {
596 pub fn new(code: IssueCode, path: AuthoredPath, message: impl Into<String>) -> Self {
598 Self {
599 code,
600 severity: code.severity(),
601 path,
602 step_kind: None,
603 step_id: None,
604 field: None,
605 message: message.into(),
606 }
607 }
608
609 #[must_use]
611 pub fn with_severity(mut self, severity: Severity) -> Self {
612 self.severity = severity;
613 self
614 }
615
616 #[must_use]
618 pub fn with_step(mut self, kind: StepKind, id: Option<&str>) -> Self {
619 self.step_kind = Some(kind);
620 self.step_id = id.map(str::to_string);
621 self
622 }
623
624 #[must_use]
626 pub fn with_field(mut self, field: impl Into<String>) -> Self {
627 self.field = Some(field.into());
628 self
629 }
630}
631
632#[cfg(feature = "ts")]
633crate::base::ts::ts_decl!(
634 IssueCode,
635 "IssueCode",
636 [" What an issue is about: its SCREAMING_SNAKE code, such as `UNKNOWN_FUNCTION`."],
637 |_cfg| crate::base::ts::literals(IssueCode::ALL.iter().map(|c| c.as_str()))
638);
639
640#[cfg(feature = "ts")]
641crate::base::ts::ts_decl!(Severity, "Severity", [" When an issue bites."], |_cfg| {
642 crate::base::ts::literals(Severity::ALL.map(Severity::as_str))
643});
644
645#[cfg(feature = "ts")]
646crate::base::ts::ts_decl!(
647 StepKind,
648 "StepKind",
649 [" What kind of step an issue sits in."],
650 |_cfg| crate::base::ts::literals(StepKind::ALL.map(StepKind::as_str))
651);
652
653#[cfg(feature = "ts")]
654crate::base::ts::ts_decl!(
655 AuthoredPath,
656 "AuthoredPath",
657 [
658 " A path in the authored JSON, as v3 spelled it: `tasks[1].tasks[0].id`.",
659 " The document itself is the empty string.",
660 ],
661 |_cfg| "string".to_owned()
662);
663
664impl fmt::Display for Issue {
665 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666 if self.path.is_root() {
667 write!(f, "{} [{}]", self.message, self.code)
668 } else {
669 write!(f, "{}: {} [{}]", self.path, self.message, self.code)
670 }
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use serde_json::{Value, json};
678 use std::collections::HashSet;
679
680 #[test]
681 fn every_code_round_trips_through_its_string() {
682 for &code in IssueCode::ALL {
683 assert_eq!(code.as_str().parse::<IssueCode>(), Ok(code), "{code:?}");
684 }
685 assert!("NOT_A_CODE".parse::<IssueCode>().is_err());
686 }
687
688 #[test]
689 fn code_strings_are_distinct_and_screaming_snake() {
690 let mut seen = HashSet::new();
691 for &code in IssueCode::ALL {
692 assert!(seen.insert(code.as_str()), "duplicate string for {code:?}");
693 assert_eq!(code.to_string(), code.as_str());
694 assert!(
695 code.as_str()
696 .chars()
697 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'),
698 "{code:?} is not SCREAMING_SNAKE"
699 );
700 }
701 }
702
703 #[test]
704 fn the_advisory_and_defect_sets_are_exactly_these() {
705 let by = |want: Severity| -> HashSet<&'static str> {
706 IssueCode::ALL
707 .iter()
708 .filter(|c| c.severity() == want)
709 .map(|c| c.as_str())
710 .collect()
711 };
712 assert_eq!(
713 by(Severity::Advisory),
714 HashSet::from([
715 "UNGUARDED_VALIDATION",
716 "GROUP_CONTINUE_ON_ERROR",
717 "ESCAPED_TEMPLATE_KEY",
718 "NULL_MAPPING",
719 "UNKNOWN_KEY",
720 "V3_SPELLING",
721 "OPERATOR_IN_DOCUMENT",
722 "NULL_CONDITION",
723 ])
724 );
725 assert_eq!(
726 by(Severity::Defect),
727 HashSet::from([
728 "LIKELY_TYPO",
729 "PAYLOAD_VAR",
730 "UNRESOLVED_REFERENCE",
731 "INVALID_ROLLOUT",
732 ])
733 );
734 assert_eq!(by(Severity::Rejected).len(), IssueCode::ALL.len() - 12);
735 assert!(by(Severity::Rejected).contains("SECRET_IN_DOCUMENT"));
736 assert!(by(Severity::Rejected).contains("INVALID_PATH"));
737 }
738
739 #[test]
740 fn severities_have_distinct_strings() {
741 let all = [Severity::Rejected, Severity::Defect, Severity::Advisory];
742 let strings: HashSet<_> = all.iter().map(|s| s.as_str()).collect();
743 assert_eq!(strings.len(), 3);
744 assert_eq!(Severity::Defect.to_string(), "DEFECT");
745 }
746
747 #[test]
748 fn an_authored_path_round_trips() {
749 for text in [
750 "tasks[1].tasks[0].id",
751 "loop.setup[0].id",
752 "function.input.fields.a",
753 "[2]",
754 "",
755 "a[0][1].b",
756 ] {
757 let path: AuthoredPath = text.parse().unwrap();
758 assert_eq!(path.to_string(), text);
759 assert_eq!(path.is_root(), text.is_empty());
760 }
761 assert!("a..b".parse::<AuthoredPath>().is_err());
762 assert!("a[x]".parse::<AuthoredPath>().is_err());
763 assert!("a[1".parse::<AuthoredPath>().is_err());
764 }
765
766 #[test]
767 fn path_building_and_serialization() {
768 let mut p = AuthoredPath::root().join("tasks").join(1usize);
769 assert_eq!(p.to_string(), "tasks[1]");
770 let q = p.extend(&"function.name".parse().unwrap());
771 assert_eq!(q.to_string(), "tasks[1].function.name");
772 assert_eq!(p.pop(), Some(PathSeg::Index(1)));
773 assert_eq!(
774 serde_json::to_string(&q).unwrap(),
775 "\"tasks[1].function.name\""
776 );
777 }
778
779 #[test]
780 fn an_issue_takes_its_codes_severity_and_displays() {
781 let issue = Issue::new(IssueCode::UnknownKey, "tasks[0].x".parse().unwrap(), "m");
782 assert_eq!(issue.severity, Severity::Advisory);
783 assert_eq!(issue.to_string(), "tasks[0].x: m [UNKNOWN_KEY]");
784 let root = Issue::new(IssueCode::NoTasks, AuthoredPath::root(), "m")
785 .with_severity(Severity::Defect)
786 .with_step(StepKind::Setup, Some("a"))
787 .with_field("f");
788 assert_eq!(root.severity, Severity::Defect);
789 assert_eq!(root.step_kind, Some(StepKind::Setup));
790 assert_eq!(root.step_id.as_deref(), Some("a"));
791 assert_eq!(root.field.as_deref(), Some("f"));
792 assert_eq!(root.to_string(), "m [NO_TASKS]");
793 }
794
795 #[test]
796 fn a_key_it_could_not_otherwise_print_is_quoted() {
797 for (key, shown) in [
798 ("a.b", r#"["a.b"]"#),
799 ("", r#"[""]"#),
800 ("x[0]", r#"["x[0]"]"#),
801 ("q\"t", r#"["q\"t"]"#),
802 ] {
803 let path = AuthoredPath::root().join(key);
804 assert_eq!(path.to_string(), shown, "{key:?}");
805 assert_eq!(shown.parse::<AuthoredPath>(), Ok(path), "{shown}");
806 let deeper = AuthoredPath::root().join("log").join(key).join(0usize);
807 let shown = format!("log{shown}[0]");
808 assert_eq!(deeper.to_string(), shown, "{key:?}");
809 assert_eq!(shown.parse::<AuthoredPath>(), Ok(deeper), "{shown}");
810 }
811 }
812
813 #[test]
814 fn a_quoted_key_round_trips_anywhere_in_a_path() {
815 let text = r#"tasks[0].function.input.fields["a.b"]"#;
816 let path: AuthoredPath = text.parse().unwrap();
817 assert_eq!(path.segs().len(), 6);
818 assert_eq!(path.segs()[5], PathSeg::Key("a.b".into()));
819 assert_eq!(path.to_string(), text);
820
821 let text = r#"["a.b"].c["]"]["\"\\"][3]"#;
822 let path: AuthoredPath = text.parse().unwrap();
823 assert_eq!(
824 path.segs(),
825 [
826 PathSeg::Key("a.b".into()),
827 PathSeg::Key("c".into()),
828 PathSeg::Key("]".into()),
829 PathSeg::Key("\"\\".into()),
830 PathSeg::Index(3),
831 ]
832 );
833 assert_eq!(path.to_string(), text);
834
835 let path: AuthoredPath = r#"a["b"]["\\"]"#.parse().unwrap();
838 assert_eq!(
839 path.segs(),
840 [
841 PathSeg::Key("a".into()),
842 PathSeg::Key("b".into()),
843 PathSeg::Key("\\".into()),
844 ]
845 );
846 assert_eq!(path.to_string(), r"a.b.\");
847 assert_eq!(path.to_string().parse::<AuthoredPath>(), Ok(path));
848
849 let ordinary = AuthoredPath::root()
851 .join("tasks")
852 .join(1usize)
853 .join("function")
854 .join("input");
855 assert_eq!(ordinary.to_string(), "tasks[1].function.input");
856
857 for bad in [
858 r#"a.["b"]"#,
859 r#"a["b"#,
860 r#"a["b""#,
861 r#"a["b"x]"#,
862 r#"a[b"]"#,
863 r#"a"b"#,
864 r#"["\q"]"#,
865 ] {
866 assert!(bad.parse::<AuthoredPath>().is_err(), "{bad}");
867 }
868 }
869
870 #[test]
871 fn an_issue_serializes_with_its_parts_as_strings() {
872 let issue = Issue::new(
873 IssueCode::UnknownFunction,
874 r#"tasks[0].function.input["a.b"]"#.parse().unwrap(),
875 "no function named `x`",
876 )
877 .with_step(StepKind::Task, Some("t"))
878 .with_field("a.b");
879 let value = serde_json::to_value(&issue).unwrap();
880 let mut keys: Vec<&str> = value
881 .as_object()
882 .unwrap()
883 .keys()
884 .map(String::as_str)
885 .collect();
886 keys.sort_unstable();
887 let mut want = [
888 "code",
889 "severity",
890 "path",
891 "step_kind",
892 "step_id",
893 "field",
894 "message",
895 ];
896 want.sort_unstable();
897 assert_eq!(keys, want);
898 assert_eq!(value["code"], "UNKNOWN_FUNCTION");
899 assert_eq!(value["severity"], "REJECTED");
900 assert_eq!(value["path"], r#"tasks[0].function.input["a.b"]"#);
901 assert_eq!(value["step_kind"], "task");
902 assert_eq!(value["step_id"], "t");
903 assert_eq!(value["field"], "a.b");
904 assert_eq!(value["message"], "no function named `x`");
905 assert_eq!(serde_json::from_value::<Issue>(value).unwrap(), issue);
906
907 let bare = Issue::new(IssueCode::NoTasks, AuthoredPath::root(), "m");
909 let value = serde_json::to_value(&bare).unwrap();
910 assert_eq!(value["step_kind"], Value::Null);
911 assert_eq!(value["path"], "");
912 assert_eq!(serde_json::from_value::<Issue>(value).unwrap(), bare);
913 }
914
915 #[test]
916 fn the_parts_of_an_issue_read_back_from_their_strings() {
917 for kind in [StepKind::Task, StepKind::Group, StepKind::Setup] {
918 let value = serde_json::to_value(kind).unwrap();
919 assert_eq!(value, kind.as_str());
920 assert_eq!(serde_json::from_value::<StepKind>(value).unwrap(), kind);
921 }
922 for severity in [Severity::Rejected, Severity::Defect, Severity::Advisory] {
923 let value = serde_json::to_value(severity).unwrap();
924 assert_eq!(serde_json::from_value::<Severity>(value).unwrap(), severity);
925 }
926 for &code in IssueCode::ALL {
927 let value = serde_json::to_value(code).unwrap();
928 assert_eq!(value, code.as_str());
929 assert_eq!(serde_json::from_value::<IssueCode>(value).unwrap(), code);
930 }
931 assert!(serde_json::from_value::<StepKind>(json!("Task")).is_err());
932 assert!(serde_json::from_value::<Severity>(json!("rejected")).is_err());
933 assert!(serde_json::from_value::<IssueCode>(json!("NOT_A_CODE")).is_err());
934 assert!(serde_json::from_value::<AuthoredPath>(json!("a..b")).is_err());
935 }
936}