use crate::Meta;
use std::fmt::{self, Display, Formatter};
use tanzim_value::{Location, ValueType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorKind {
Type {
expected: ValueType,
found: ValueType,
},
NotConvertible { target: ValueType, found: ValueType },
Format { expected: &'static str },
BelowMin { value: String, min: String },
AboveMax { value: String, max: String },
TooShort { len: usize, min: usize },
TooLong { len: usize, max: usize },
PatternMismatch { pattern: String },
Duplicate { index: usize },
MissingKey { key: String },
UnknownKey { key: String },
NotAllowed { value: String },
Either {
first: Box<Error>,
second: Box<Error>,
},
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Type { expected, found } => {
write!(f, "expected {expected}, found {found}")
}
Self::NotConvertible { target, found } => {
write!(f, "cannot convert {found} to {target}")
}
Self::Format { expected } => write!(f, "invalid {expected}"),
Self::BelowMin { value, min } => write!(f, "{value} is below the minimum {min}"),
Self::AboveMax { value, max } => write!(f, "{value} is above the maximum {max}"),
Self::TooShort { len, min } => {
write!(f, "length {len} is below the minimum {min}")
}
Self::TooLong { len, max } => write!(f, "length {len} is above the maximum {max}"),
Self::PatternMismatch { pattern } => {
write!(f, "does not match pattern `{pattern}`")
}
Self::Duplicate { index } => write!(f, "duplicate item at index {index}"),
Self::MissingKey { key } => write!(f, "missing required key `{key}`"),
Self::UnknownKey { key } => write!(f, "unknown key `{key}`"),
Self::NotAllowed { value } => write!(f, "`{value}` is not an allowed value"),
Self::Either { first, second } => {
write!(f, "no alternative matched: ({first}) or ({second})")
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Error {
pub kind: ErrorKind,
pub path: Vec<Segment>,
pub location: Option<Box<Location>>,
pub meta: Option<Box<Meta>>,
}
impl Error {
pub fn new(kind: ErrorKind) -> Self {
Self {
kind,
path: Vec::new(),
location: None,
meta: None,
}
}
pub fn with_location(mut self, location: &Location) -> Self {
if self.location.is_none() {
self.location = Some(Box::new(location.clone()));
}
self
}
pub fn with_meta(mut self, meta: &Meta) -> Self {
if self.meta.is_none() {
self.meta = Some(Box::new(meta.clone()));
}
self
}
pub fn name(&self) -> Option<&str> {
self.meta.as_ref().map(|meta| meta.name.as_str())
}
pub fn default_value(&self) -> Option<&tanzim_value::Value> {
self.meta.as_ref().and_then(|meta| meta.default.as_ref())
}
pub fn under_key(mut self, key: &str, location: &Location) -> Self {
self.path.insert(0, Segment::Key(key.to_string()));
self.with_location(location)
}
pub fn under_index(mut self, index: usize, location: &Location) -> Self {
self.path.insert(0, Segment::Index(index));
self.with_location(location)
}
fn write_path(&self, f: &mut Formatter<'_>) -> fmt::Result {
for (position, segment) in self.path.iter().enumerate() {
match segment {
Segment::Key(key) => {
if position > 0 {
write!(f, ".")?;
}
write!(f, "{key}")?;
}
Segment::Index(index) => write!(f, "[{index}]")?,
}
}
Ok(())
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(meta) = &self.meta
&& !meta.name.is_empty()
{
write!(f, "{}: ", meta.name)?;
}
if !self.path.is_empty() {
self.write_path(f)?;
write!(f, ": ")?;
}
write!(f, "{}", self.kind)?;
if let Some(location) = &self.location {
write!(f, " at {location}")?;
}
if f.alternate()
&& let Some(meta) = &self.meta
{
if let Some(description) = &meta.description {
write!(f, "\n {description}")?;
}
for (value, note) in &meta.examples {
match note {
Some(note) => write!(f, "\n example: {value} ({note})")?,
None => write!(f, "\n example: {value}")?,
}
}
}
Ok(())
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
use tanzim_value::Location;
#[test]
fn nested_error_renders_path_and_innermost_location() {
let leaf_loc = Location::at("file", "config.yaml", Some(3), Some(9), None);
let outer_loc = Location::at("file", "config.yaml", Some(2), Some(1), None);
let error = Error::new(ErrorKind::Type {
expected: ValueType::Int,
found: ValueType::String,
})
.under_key("port", &leaf_loc)
.under_index(0, &outer_loc)
.under_key("servers", &outer_loc);
let message = error.to_string();
assert!(message.starts_with("servers[0].port: expected integer, found string"));
assert!(message.contains("config.yaml:3:9"));
}
}