1use std::fmt;
4
5use super::scan::Syntax;
6use crate::{Seg, render_path};
7
8#[derive(Debug)]
10pub enum InterpError {
11 Problems(Vec<Problem>),
12 Cycle(Cycle),
13}
14
15impl std::error::Error for InterpError {}
16
17impl fmt::Display for InterpError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 Self::Cycle(cycle) => write!(f, "{cycle}"),
21 Self::Problems(problems) => {
22 let mut lines: Vec<String> = Vec::new();
24 for group in Group::ALL {
25 let members = problems.iter().filter(|p| p.group() == group);
26 let mut any = false;
27 for problem in members {
28 if !any {
29 lines.push(group.header().to_string());
30 any = true;
31 }
32 lines.push(format!(
33 " --> {}: {}",
34 render_path(problem.path()),
35 problem.detail()
36 ));
37 }
38 }
39 f.write_str(&lines.join("\n"))
40 }
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Problem {
48 Syntax { path: Vec<Seg>, error: Syntax },
50 Unresolved { path: Vec<Seg>, reference: String },
52 NotStringifiable {
54 path: Vec<Seg>,
55 reference: String,
56 kind: &'static str,
57 },
58}
59
60impl Problem {
61 pub fn path(&self) -> &[Seg] {
63 match self {
64 Self::Syntax { path, .. }
65 | Self::Unresolved { path, .. }
66 | Self::NotStringifiable { path, .. } => path,
67 }
68 }
69
70 fn group(&self) -> Group {
71 match self {
72 Self::Syntax { .. } => Group::Syntax,
73 Self::Unresolved { .. } => Group::Unresolved,
74 Self::NotStringifiable { .. } => Group::NotStringifiable,
75 }
76 }
77
78 fn detail(&self) -> String {
79 match self {
80 Self::Syntax { error, .. } => error.to_string(),
81 Self::Unresolved { reference, .. } => format!("`{reference}`"),
82 Self::NotStringifiable {
83 reference, kind, ..
84 } => format!("`{reference}` is {} {kind}", article(kind)),
85 }
86 }
87}
88
89fn article(kind: &str) -> &'static str {
91 if kind.starts_with(['a', 'e', 'i', 'o', 'u']) {
92 "an"
93 } else {
94 "a"
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99enum Group {
100 Syntax,
101 Unresolved,
102 NotStringifiable,
103}
104
105impl Group {
106 const ALL: [Self; 3] = [Self::Syntax, Self::Unresolved, Self::NotStringifiable];
108
109 fn header(self) -> &'static str {
110 match self {
111 Self::Syntax => "invalid reference",
112 Self::Unresolved => "unresolved reference",
113 Self::NotStringifiable => "reference cannot be rendered into a string",
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct Cycle {
121 chain: Vec<Vec<Seg>>,
122}
123
124impl Cycle {
125 pub(crate) fn new(chain: Vec<Vec<Seg>>) -> Self {
126 Self { chain }
127 }
128}
129
130impl fmt::Display for Cycle {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 let hops: Vec<String> = self
133 .chain
134 .iter()
135 .map(|p| format!("`{}`", render_path(p)))
136 .collect();
137 write!(f, "reference cycle: {}", hops.join(" -> "))
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn key(dotted: &str) -> Vec<Seg> {
146 dotted.split('.').map(|s| Seg::Key(s.to_string())).collect()
147 }
148
149 #[test]
150 fn one_kind_renders_as_a_header_and_a_list() {
151 let err = InterpError::Problems(vec![
152 Problem::Unresolved {
153 path: key("server.url"),
154 reference: "db.hostname".into(),
155 },
156 Problem::Unresolved {
157 path: vec![Seg::Key("tags".into()), Seg::Index(0)],
158 reference: "env:REGION".into(),
159 },
160 ]);
161 assert_eq!(
162 err.to_string(),
163 "unresolved reference\n\
164 \x20 --> server.url: `db.hostname`\n\
165 \x20 --> tags[0]: `env:REGION`"
166 );
167 }
168
169 #[test]
170 fn kinds_group_in_a_fixed_order() {
171 let err = InterpError::Problems(vec![
172 Problem::NotStringifiable {
173 path: key("url"),
174 reference: "db".into(),
175 kind: "object",
176 },
177 Problem::Unresolved {
178 path: key("a"),
179 reference: "nope".into(),
180 },
181 Problem::Syntax {
182 path: key("b"),
183 error: Syntax::EmptyRef,
184 },
185 ]);
186 assert_eq!(
187 err.to_string(),
188 "invalid reference\n\
189 \x20 --> b: empty reference `${}`\n\
190 unresolved reference\n\
191 \x20 --> a: `nope`\n\
192 reference cannot be rendered into a string\n\
193 \x20 --> url: `db` is an object"
194 );
195 }
196
197 #[test]
198 fn a_cycle_reads_as_a_chain() {
199 let err = InterpError::Cycle(Cycle::new(vec![key("a"), key("b"), key("a")]));
200 assert_eq!(err.to_string(), "reference cycle: `a` -> `b` -> `a`");
201 }
202
203 #[test]
204 fn articles_match_the_three_possible_kinds() {
205 assert_eq!(article("object"), "an");
206 assert_eq!(article("array"), "an");
207 assert_eq!(article("null"), "a");
208 }
209}