Skip to main content

knf/interp/
error.rs

1//! Interpolation errors. They name key paths only, never files or flags.
2
3use std::fmt;
4
5use super::scan::Syntax;
6use crate::{Seg, render_path};
7
8/// Why interpolation failed: every problem found, or a single cycle.
9#[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                // Grouped by kind in a fixed order. No trailing newline.
23                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/// One thing wrong with one reference.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Problem {
48    /// A reference that is not spelled like one.
49    Syntax { path: Vec<Seg>, error: Syntax },
50    /// A missing key or unset environment variable.
51    Unresolved { path: Vec<Seg>, reference: String },
52    /// A container or null in embedded position: `url = "http://${db}/"`.
53    NotStringifiable {
54        path: Vec<Seg>,
55        reference: String,
56        kind: &'static str,
57    },
58}
59
60impl Problem {
61    /// Where in the merged document the offending string lives.
62    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
89/// `an` for `object`/`array`, `a` for `null`.
90fn 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    /// Report order, most fundamental first.
107    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/// A reference that resolves to itself, reported as the full chain.
119#[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}