use crate::parse::PathSegment;
use std::fmt;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
pub line: usize,
pub column: usize,
pub offset: usize,
}
impl Position {
pub fn new() -> Self {
Self {
line: 1,
column: 1,
offset: 0,
}
}
}
impl Default for Position {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[derive(Debug, Error)]
pub enum UclError {
#[error("Serde error: {0}")]
Serde(#[from] SerdeError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Syntax error: {0}")]
Syntax(#[source] crate::parse::Error),
#[error("Parsing stopped: {0}")]
Stopped(#[source] crate::parse::Error),
#[error("Deserialization error: {0}")]
Deserialize(DeserializeError),
}
impl UclError {
pub fn parse_error(&self) -> Option<&crate::parse::Error> {
match self {
UclError::Syntax(e) | UclError::Stopped(e) => Some(e),
_ => None,
}
}
pub fn position(&self) -> Option<Position> {
match self {
UclError::Deserialize(e) => e.position(),
_ => self.parse_error().map(crate::parse::Error::position),
}
}
pub fn file(&self) -> Option<&Path> {
match self {
UclError::Deserialize(e) => e.file(),
_ => self.parse_error().and_then(crate::parse::Error::file),
}
}
#[cold]
#[inline(never)]
pub(crate) fn in_document(self) -> Self {
match self {
UclError::Serde(error) => UclError::Deserialize(DeserializeError::new(error)),
other => other,
}
}
pub(crate) fn inside(self, segment: PathSegment) -> Self {
self.map_deserialize(|e| e.path.insert(0, segment))
}
pub(crate) fn inside_entry(self, key: &str) -> Self {
self.inside(PathSegment::Key {
key: key.to_owned(),
index: 0,
})
}
pub(crate) fn inside_values_of(self, key: &str) -> Self {
self.map_deserialize(|e| match e.path.first() {
Some(&PathSegment::Index(index)) => {
e.path[0] = PathSegment::Key {
key: key.to_owned(),
index,
};
}
_ => e.path.insert(
0,
PathSegment::Key {
key: key.to_owned(),
index: 0,
},
),
})
}
#[cold]
#[inline(never)]
pub(crate) fn at_key(self, key: &str) -> Self {
self.inside_entry(key).map_deserialize(|e| {
if e.path.len() == 1 {
e.key = true;
}
})
}
#[cold]
#[inline(never)]
pub(crate) fn add_step(&mut self, step: Step<'_>) {
let placeholder = UclError::Serde(SerdeError::Custom(String::new()));
let error = std::mem::replace(self, placeholder);
*self = match step {
Step::Index(index) => error.inside(PathSegment::Index(index)),
Step::Entry(key) => error.inside_entry(key),
Step::Values(key) => error.inside_values_of(key),
Step::Key(key) => error.at_key(key),
};
}
fn map_deserialize(self, change: impl FnOnce(&mut Located)) -> Self {
match self.in_document() {
UclError::Deserialize(mut e) => {
change(&mut e.0);
UclError::Deserialize(e)
}
other => other,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Step<'a> {
Index(usize),
Entry(&'a str),
Values(&'a str),
Key(&'a str),
}
#[derive(Debug)]
pub struct DeserializeError(Box<Located>);
#[derive(Debug)]
struct Located {
error: SerdeError,
path: Vec<PathSegment>,
key: bool,
position: Option<Position>,
file: Option<PathBuf>,
}
impl DeserializeError {
fn new(error: SerdeError) -> Self {
Self(Box::new(Located {
error,
path: Vec::new(),
key: false,
position: None,
file: None,
}))
}
pub fn error(&self) -> &SerdeError {
&self.0.error
}
pub fn into_error(self) -> SerdeError {
self.0.error
}
pub fn path(&self) -> &[PathSegment] {
&self.0.path
}
pub fn at_key(&self) -> bool {
self.0.key
}
pub fn position(&self) -> Option<Position> {
self.0.position
}
pub fn file(&self) -> Option<&Path> {
self.0.file.as_deref()
}
pub(crate) fn locate_request(&self) -> (&[PathSegment], bool) {
(&self.0.path, self.0.key)
}
pub(crate) fn set_position(&mut self, position: Position, file: Option<PathBuf>) {
self.0.position = Some(position);
self.0.file = file;
}
}
impl fmt::Display for DeserializeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.error)?;
if !self.0.path.is_empty() {
let what = if self.0.key { "at the key of" } else { "at" };
write!(f, " {what} {}", PathText(&self.0.path))?;
}
if let Some(position) = self.0.position {
write!(f, " (line {}, column {}", position.line, position.column)?;
match &self.0.file {
Some(file) => write!(f, " of {})", file.display())?,
None => f.write_str(")")?,
}
}
Ok(())
}
}
impl std::error::Error for DeserializeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0.error)
}
}
struct PathText<'a>(&'a [PathSegment]);
impl fmt::Display for PathText<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (n, segment) in self.0.iter().enumerate() {
match segment {
PathSegment::Key { key, index } => {
if n > 0 {
f.write_str(".")?;
}
let plain = !key.is_empty()
&& key
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '/'));
if plain {
f.write_str(key)?;
} else {
write!(f, "{key:?}")?;
}
if *index > 0 {
write!(f, "[{index}]")?;
}
}
PathSegment::Index(index) => write!(f, "[{index}]")?,
}
}
Ok(())
}
}
impl From<crate::parse::Error> for UclError {
fn from(error: crate::parse::Error) -> Self {
if error.is_stopped() {
UclError::Stopped(error)
} else {
UclError::Syntax(error)
}
}
}
#[derive(Debug, Error)]
pub enum SerdeError {
#[error("{0}")]
Custom(String),
#[error("cannot serialize {0}")]
Unrepresentable(String),
#[error("more than {limit} maps and sequences nested inside one another")]
TooDeep { limit: usize },
}
impl serde::de::Error for UclError {
fn custom<T: fmt::Display>(msg: T) -> Self {
UclError::Serde(SerdeError::Custom(msg.to_string()))
}
}
impl serde::ser::Error for UclError {
fn custom<T: fmt::Display>(msg: T) -> Self {
UclError::Serde(SerdeError::Custom(msg.to_string()))
}
}
impl serde::de::Error for SerdeError {
fn custom<T: fmt::Display>(msg: T) -> Self {
SerdeError::Custom(msg.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_new() {
let pos = Position::new();
assert_eq!(pos.line, 1);
assert_eq!(pos.column, 1);
assert_eq!(pos.offset, 0);
assert_eq!(Position::default(), pos);
}
#[test]
fn test_position_display() {
let pos = Position {
line: 42,
column: 13,
offset: 100,
};
assert_eq!(format!("{pos}"), "42:13");
}
}