use quick_xml::Error as XmlError;
use std::{fmt, io, path::PathBuf, process::ExitCode, str::Utf8Error};
use strum::IntoEnumIterator;
use thiserror::Error;
use zip::result::ZipError;
use crate::fibroblast::tags::{any_child_tag::AnyChildTagDiscriminants, Extras};
pub type ClgnDecodingResult<T> = Result<T, ClgnDecodingError>;
#[derive(Debug, Error)]
pub enum ClgnDecodingError {
#[error("missing manifest file; must provide either collagen.jsonnet or collagen.json")]
MissingManifest,
#[error("invalid schema: {}", .0)]
InvalidSchema(#[from] InvalidSchemaErrorList),
#[error("IO error reading from {path:?} ({source})")]
IoRead { source: io::Error, path: PathBuf },
#[error("IO error writing to {path:?} ({source})")]
IoWrite { source: io::Error, path: PathBuf },
#[error("IO error (neither reading nor writing) handling {path:?} ({source})")]
IoOther { source: io::Error, path: PathBuf },
#[error("parent folder of {path:?} does not exist ({source})")]
FolderDoesNotExist { source: io::Error, path: PathBuf },
#[error("paths may not begin with a '/'; got {:?}", .0)]
InvalidPath(PathBuf),
#[error("error reading {path:?} ({source})")]
Zip { source: ZipError, path: PathBuf },
#[error("DEBUG: missing jsonnet file. this is not supposed to appear to end users; please file a bug!")]
MissingJsonnetFile,
#[error("error reading {path:?} as jsonnet ({msg})")]
JsonnetRead { msg: String, path: PathBuf },
#[error("failed to convert json at {path:?} to a tag ({source})")]
JsonDecodeFile {
source: serde_json::Error,
path: PathBuf,
},
#[error(
"after expanding jsonnet at {path:?} to json, failed to convert json to a tag ({source})"
)]
JsonDecodeJsonnet {
source: serde_json::Error,
path: PathBuf,
},
#[error("error writing {path:?} as json ({source})")]
JsonEncode {
source: serde_json::Error,
path: Option<PathBuf>,
},
#[error("XML error: {}", .0)]
Xml(#[from] XmlError),
#[error("error encoding XML as UTF-8: {}", .0)]
ToSvgString(#[from] Utf8Error),
#[error("error reading image: {msg}")]
Image { msg: String },
#[error("could not find bundled font {font_name:?}")]
BundledFontNotFound { font_name: String },
#[error("error watching folder: {:?}", .0)]
FolderWatch(Vec<notify::Error>),
#[error(
"Refusing to run in --watch mode. \
out_file {out_file:?} is a descendent of in_folder \
{in_folder:?}, which would lead to an infinite loop. \
To fix this, set out_file to a location outside \
of {in_folder:?}."
)]
RecursiveWatch {
in_folder: PathBuf,
out_file: PathBuf,
},
}
impl ClgnDecodingError {
#[must_use]
pub fn exit_code(&self) -> ExitCode {
use ClgnDecodingError::*;
ExitCode::from(match self {
InvalidSchema { .. } => 1,
JsonnetRead { .. } => 3,
JsonDecodeFile { .. } => 4,
JsonDecodeJsonnet { .. } => 5,
JsonEncode { .. } => 6,
MissingManifest => 9,
InvalidPath { .. } => 10,
IoRead { .. } => 11,
IoWrite { .. } => 12,
IoOther { .. } => 13,
FolderDoesNotExist { .. } => 17,
Image { .. } => 20,
Xml { .. } => 30,
ToSvgString { .. } => 40,
BundledFontNotFound { .. } => 50,
Zip { .. } => 101,
FolderWatch { .. } => 102,
RecursiveWatch { .. } => 103,
MissingJsonnetFile => {
eprintln!("DEBUG: we should not have gotten here. please file a bug!");
199
}
})
}
}
impl From<notify::Error> for ClgnDecodingError {
fn from(value: notify::Error) -> Self {
Self::FolderWatch(vec![value])
}
}
impl From<Vec<notify::Error>> for ClgnDecodingError {
fn from(value: Vec<notify::Error>) -> Self {
Self::FolderWatch(value)
}
}
#[derive(Debug)]
pub enum InvalidSchemaError {
InvalidType(serde_json::Value),
UnexpectedKeys {
tag_name: &'static str,
keys: Vec<String>,
},
UnrecognizedObject(Extras),
}
impl InvalidSchemaError {
pub(crate) fn unexpected_keys(tag_name: &'static str, keys: Vec<String>) -> Self {
Self::UnexpectedKeys { tag_name, keys }
}
}
impl std::error::Error for InvalidSchemaError {}
#[derive(Debug, Default)]
pub struct InvalidSchemaErrorList(pub(crate) Vec<InvalidSchemaError>);
impl InvalidSchemaErrorList {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, err: InvalidSchemaError) {
self.0.push(err);
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Display for InvalidSchemaErrorList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, error) in self.0.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(f, "{error}")?;
}
Ok(())
}
}
impl std::error::Error for InvalidSchemaErrorList {}
impl AnyChildTagDiscriminants {
pub(crate) fn primary_key(self) -> &'static str {
use AnyChildTagDiscriminants::*;
match self {
Generic => "tag",
Image => "image_path",
Container => "clgn_path",
NestedSvg => "svg_path",
Font => "fonts",
Text => "text",
}
}
pub(crate) fn name(self) -> &'static str {
self.into()
}
fn article(self) -> &'static str {
use AnyChildTagDiscriminants::*;
match self {
Generic | Container | NestedSvg | Font | Text => "a",
Image => "an",
}
}
fn additional_required_keys(self) -> &'static [&'static str] {
use AnyChildTagDiscriminants::*;
match self {
Generic | Image | Container | NestedSvg | Font | Text => &[],
}
}
fn optional_keys(self) -> &'static [&'static str] {
use AnyChildTagDiscriminants::*;
match self {
Generic => &["vars", "attrs", "children"],
Image => &["vars", "attrs", "kind", "children"],
Text => &["vars", "is_preescaped"],
Container | NestedSvg | Font => &[],
}
}
}
impl fmt::Display for InvalidSchemaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidSchemaError::InvalidType(v) => {
write!(f, "Each tag must be an object; got: {v:?}")
}
InvalidSchemaError::UnexpectedKeys { tag_name, keys } => {
write!(f, "unexpected keys for tag {tag_name:?}: {keys:?}")
}
InvalidSchemaError::UnrecognizedObject(o) => {
writeln!(
f,
"The following object did not match any known schema: {}",
serde_json::to_string(&o).unwrap()
)?;
let known_tags_ids_seen = AnyChildTagDiscriminants::iter()
.filter(|k| o.map().contains_key(k.primary_key()))
.collect::<Vec<_>>();
if known_tags_ids_seen.len() == 1 {
let kt = known_tags_ids_seen[0];
let key = kt.primary_key();
let name = kt.name();
let required_keys = kt.additional_required_keys();
let optional_keys = kt.optional_keys();
let a = kt.article();
write!(
f,
"The presence of key {key:?} implies that this is {a} `{name}` tag. "
)?;
let unexpected_keys = o
.map()
.keys()
.filter(|k| {
let k = k.as_str();
!(k == key || required_keys.contains(&k) || optional_keys.contains(&k))
})
.collect::<Vec<_>>();
let missing_keys = required_keys
.iter()
.copied()
.filter(|&k| !o.map().contains_key(k))
.collect::<Vec<_>>();
if unexpected_keys.is_empty() && missing_keys.is_empty() {
write!(
f,
"Since you provided all of the other required keys, \
{required_keys:?}, check that the values were all of \
the right type. "
)?;
} else {
if missing_keys.is_empty() {
write!(f, "`{name}` has no other required keys. ")?;
} else {
write!(
f,
"In addition to {key:?}, keys {required_keys:?} \
are required, but keys {missing_keys:?} were missing. "
)?;
}
if !unexpected_keys.is_empty() {
write!(
f,
"The only other permitted keys for `{name}` are {optional_keys:?}, \
but keys {unexpected_keys:?} were passed. "
)?;
}
}
} else if known_tags_ids_seen.len() >= 2 {
write!(
f,
"Could not infer the tag's type because multiple matching \
primary keys were found: {:?}. At most one \
may be provided. ",
known_tags_ids_seen
.iter()
.map(|kt| kt.primary_key())
.collect::<Vec<_>>()
)?;
} else {
write!(
f,
"Could not infer the tag's type because no recognized \
primary key was found. All tags except the root must have \
exactly one of the following keys: {:?}. ",
AnyChildTagDiscriminants::iter()
.map(|kt| kt.primary_key())
.collect::<Vec<_>>()
)?;
}
write!(
f,
"\nFor an in-depth description of the schema, visit \
https://docs.rs/collagen/{}/\
collagen/fibroblast/tags/enum.AnyChildTag.html",
env!("CARGO_PKG_VERSION")
)?;
Ok(())
}
}
}
}