1use crate::Meta;
2use std::fmt::{self, Display, Formatter};
3use tanzim_value::{Location, ValueType};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum Segment {
8 Key(String),
10 Index(usize),
12}
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum ErrorKind {
17 Type {
19 expected: ValueType,
20 found: ValueType,
21 },
22 NotConvertible { target: ValueType, found: ValueType },
24 Format { expected: &'static str },
26 BelowMin { value: String, min: String },
28 AboveMax { value: String, max: String },
30 TooShort { len: usize, min: usize },
32 TooLong { len: usize, max: usize },
34 PatternMismatch { pattern: String },
36 Duplicate { index: usize },
38 MissingKey { key: String },
40 UnknownKey { key: String },
42 NotAllowed { value: String },
44 Either {
46 first: Box<Error>,
47 second: Box<Error>,
48 },
49}
50
51impl Display for ErrorKind {
52 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Type { expected, found } => {
55 write!(f, "expected {expected}, found {found}")
56 }
57 Self::NotConvertible { target, found } => {
58 write!(f, "cannot convert {found} to {target}")
59 }
60 Self::Format { expected } => write!(f, "invalid {expected}"),
61 Self::BelowMin { value, min } => write!(f, "{value} is below the minimum {min}"),
62 Self::AboveMax { value, max } => write!(f, "{value} is above the maximum {max}"),
63 Self::TooShort { len, min } => {
64 write!(f, "length {len} is below the minimum {min}")
65 }
66 Self::TooLong { len, max } => write!(f, "length {len} is above the maximum {max}"),
67 Self::PatternMismatch { pattern } => {
68 write!(f, "does not match pattern `{pattern}`")
69 }
70 Self::Duplicate { index } => write!(f, "duplicate item at index {index}"),
71 Self::MissingKey { key } => write!(f, "missing required key `{key}`"),
72 Self::UnknownKey { key } => write!(f, "unknown key `{key}`"),
73 Self::NotAllowed { value } => write!(f, "`{value}` is not an allowed value"),
74 Self::Either { first, second } => {
75 write!(f, "no alternative matched: ({first}) or ({second})")
76 }
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq)]
86pub struct Error {
87 pub kind: ErrorKind,
88 pub path: Vec<Segment>,
90 pub location: Option<Box<Location>>,
94 pub meta: Option<Box<Meta>>,
99}
100
101impl Error {
102 pub fn new(kind: ErrorKind) -> Self {
104 Self {
105 kind,
106 path: Vec::new(),
107 location: None,
108 meta: None,
109 }
110 }
111
112 pub fn with_location(mut self, location: &Location) -> Self {
114 if self.location.is_none() {
115 self.location = Some(Box::new(location.clone()));
116 }
117 self
118 }
119
120 pub fn with_meta(mut self, meta: &Meta) -> Self {
122 if self.meta.is_none() {
123 self.meta = Some(Box::new(meta.clone()));
124 }
125 self
126 }
127
128 pub fn name(&self) -> Option<&str> {
130 self.meta.as_ref().map(|meta| meta.name.as_str())
131 }
132
133 pub fn default_value(&self) -> Option<&tanzim_value::Value> {
135 self.meta.as_ref().and_then(|meta| meta.default.as_ref())
136 }
137
138 pub fn under_key(mut self, key: &str, location: &Location) -> Self {
140 self.path.insert(0, Segment::Key(key.to_string()));
141 self.with_location(location)
142 }
143
144 pub fn under_index(mut self, index: usize, location: &Location) -> Self {
146 self.path.insert(0, Segment::Index(index));
147 self.with_location(location)
148 }
149
150 fn write_path(&self, f: &mut Formatter<'_>) -> fmt::Result {
151 for (position, segment) in self.path.iter().enumerate() {
152 match segment {
153 Segment::Key(key) => {
154 if position > 0 {
155 write!(f, ".")?;
156 }
157 write!(f, "{key}")?;
158 }
159 Segment::Index(index) => write!(f, "[{index}]")?,
160 }
161 }
162 Ok(())
163 }
164}
165
166impl Display for Error {
167 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
168 if let Some(meta) = &self.meta
169 && !meta.name.is_empty()
170 {
171 write!(f, "{}: ", meta.name)?;
172 }
173 if !self.path.is_empty() {
174 self.write_path(f)?;
175 write!(f, ": ")?;
176 }
177 write!(f, "{}", self.kind)?;
178 if let Some(location) = &self.location {
179 write!(f, " at {location}")?;
180 }
181 if f.alternate()
182 && let Some(meta) = &self.meta
183 {
184 if let Some(description) = &meta.description {
185 write!(f, "\n {description}")?;
186 }
187 for (value, note) in &meta.examples {
188 match note {
189 Some(note) => write!(f, "\n example: {value} ({note})")?,
190 None => write!(f, "\n example: {value}")?,
191 }
192 }
193 }
194 Ok(())
195 }
196}
197
198impl std::error::Error for Error {}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use tanzim_value::Location;
204
205 #[test]
206 fn nested_error_renders_path_and_innermost_location() {
207 let leaf_loc = Location::at("file", "config.yaml", Some(3), Some(9), None);
208 let outer_loc = Location::at("file", "config.yaml", Some(2), Some(1), None);
209 let error = Error::new(ErrorKind::Type {
210 expected: ValueType::Int,
211 found: ValueType::String,
212 })
213 .under_key("port", &leaf_loc)
214 .under_index(0, &outer_loc)
215 .under_key("servers", &outer_loc);
216
217 let message = error.to_string();
218 assert!(message.starts_with("servers[0].port: expected integer, found string"));
219 assert!(message.contains("config.yaml:3:9"));
221 }
222}