1use std::fmt;
10
11use super::scan::Syntax;
12use crate::{Seg, render_path};
13
14#[derive(Debug)]
22pub enum InterpError {
23 Problems(Vec<Problem>),
24 Cycle(Cycle),
25}
26
27impl std::error::Error for InterpError {}
28
29impl fmt::Display for InterpError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Cycle(cycle) => write!(f, "{cycle}"),
33 Self::Problems(problems) => {
34 let mut lines: Vec<String> = Vec::new();
42 for group in Group::ALL {
43 let members = problems.iter().filter(|p| p.group() == group);
44 let mut any = false;
45 for problem in members {
46 if !any {
47 lines.push(group.header().to_string());
48 any = true;
49 }
50 lines.push(format!(
51 " --> {}: {}",
52 render_path(problem.path()),
53 problem.detail()
54 ));
55 }
56 }
57 f.write_str(&lines.join("\n"))
58 }
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum Problem {
66 Syntax { path: Vec<Seg>, error: Syntax },
68 Unresolved { path: Vec<Seg>, reference: String },
75 NotStringifiable {
80 path: Vec<Seg>,
81 reference: String,
82 kind: &'static str,
83 },
84}
85
86impl Problem {
87 pub fn path(&self) -> &[Seg] {
89 match self {
90 Self::Syntax { path, .. }
91 | Self::Unresolved { path, .. }
92 | Self::NotStringifiable { path, .. } => path,
93 }
94 }
95
96 fn group(&self) -> Group {
97 match self {
98 Self::Syntax { .. } => Group::Syntax,
99 Self::Unresolved { .. } => Group::Unresolved,
100 Self::NotStringifiable { .. } => Group::NotStringifiable,
101 }
102 }
103
104 fn detail(&self) -> String {
105 match self {
106 Self::Syntax { error, .. } => error.to_string(),
107 Self::Unresolved { reference, .. } => format!("`{reference}`"),
108 Self::NotStringifiable {
109 reference, kind, ..
110 } => format!("`{reference}` is {} {kind}", article(kind)),
111 }
112 }
113}
114
115fn article(kind: &str) -> &'static str {
118 if kind.starts_with(['a', 'e', 'i', 'o', 'u']) {
119 "an"
120 } else {
121 "a"
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126enum Group {
127 Syntax,
128 Unresolved,
129 NotStringifiable,
130}
131
132impl Group {
133 const ALL: [Self; 3] = [Self::Syntax, Self::Unresolved, Self::NotStringifiable];
137
138 fn header(self) -> &'static str {
139 match self {
140 Self::Syntax => "invalid reference",
141 Self::Unresolved => "unresolved reference",
142 Self::NotStringifiable => "reference cannot be rendered into a string",
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Cycle {
153 chain: Vec<Vec<Seg>>,
154}
155
156impl Cycle {
157 pub(crate) fn new(chain: Vec<Vec<Seg>>) -> Self {
158 Self { chain }
159 }
160}
161
162impl fmt::Display for Cycle {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 let hops: Vec<String> = self
165 .chain
166 .iter()
167 .map(|p| format!("`{}`", render_path(p)))
168 .collect();
169 write!(f, "reference cycle: {}", hops.join(" -> "))
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 fn key(dotted: &str) -> Vec<Seg> {
178 dotted.split('.').map(|s| Seg::Key(s.to_string())).collect()
179 }
180
181 #[test]
182 fn one_kind_renders_as_a_header_and_a_list() {
183 let err = InterpError::Problems(vec![
184 Problem::Unresolved {
185 path: key("server.url"),
186 reference: "db.hostname".into(),
187 },
188 Problem::Unresolved {
189 path: vec![Seg::Key("tags".into()), Seg::Index(0)],
190 reference: "env:REGION".into(),
191 },
192 ]);
193 assert_eq!(
194 err.to_string(),
195 "unresolved reference\n\
196 \x20 --> server.url: `db.hostname`\n\
197 \x20 --> tags[0]: `env:REGION`"
198 );
199 }
200
201 #[test]
204 fn kinds_group_in_a_fixed_order() {
205 let err = InterpError::Problems(vec![
206 Problem::NotStringifiable {
207 path: key("url"),
208 reference: "db".into(),
209 kind: "object",
210 },
211 Problem::Unresolved {
212 path: key("a"),
213 reference: "nope".into(),
214 },
215 Problem::Syntax {
216 path: key("b"),
217 error: Syntax::EmptyRef,
218 },
219 ]);
220 assert_eq!(
221 err.to_string(),
222 "invalid reference\n\
223 \x20 --> b: empty reference `${}`\n\
224 unresolved reference\n\
225 \x20 --> a: `nope`\n\
226 reference cannot be rendered into a string\n\
227 \x20 --> url: `db` is an object"
228 );
229 }
230
231 #[test]
232 fn a_cycle_reads_as_a_chain() {
233 let err = InterpError::Cycle(Cycle::new(vec![key("a"), key("b"), key("a")]));
234 assert_eq!(err.to_string(), "reference cycle: `a` -> `b` -> `a`");
235 }
236
237 #[test]
238 fn articles_match_the_three_possible_kinds() {
239 assert_eq!(article("object"), "an");
240 assert_eq!(article("array"), "an");
241 assert_eq!(article("null"), "a");
242 }
243}