use anyhow::anyhow;
use knf::{
IntegerOutOfRange, InterpError, LoadError, MergeError, NonFiniteFloat, NullInToml, PathError,
Problem, TomlError,
};
pub fn explain_pipeline(err: impl Into<anyhow::Error>) -> anyhow::Error {
let err = err.into();
let err = match err.downcast::<LoadError>() {
Ok(err) => return explain_load(err),
Err(err) => err,
};
let err = match err.downcast::<MergeError>() {
Ok(err @ MergeError::TypeConflict { .. }) => return err.into(),
Err(err) => err,
};
let err = match err.downcast::<InterpError>() {
Ok(err) => return explain_interp(err),
Err(err) => err,
};
let err = match err.downcast::<NonFiniteFloat>() {
Ok(report) => return explain_non_finite(report),
Err(err) => err,
};
match err.downcast::<TomlError>() {
Ok(TomlError::Null(report)) => explain_null(report),
Ok(TomlError::Integer(report)) => explain_integer(report),
Ok(err @ TomlError::Datetime(_)) => err.into(),
Err(err) => err,
}
}
fn explain_null(err: NullInToml) -> anyhow::Error {
anyhow!("{err}\nhelp: emit JSON with -f json, substitute with --null-as, or remove the null")
}
fn explain_integer(err: IntegerOutOfRange) -> anyhow::Error {
anyhow!("{err}\nhelp: TOML integers are signed 64-bit; emit JSON with -f json")
}
fn explain_non_finite(err: NonFiniteFloat) -> anyhow::Error {
anyhow!("{err}\nhelp: emit TOML with -f toml, which can represent inf and nan")
}
fn explain_load(err: LoadError) -> anyhow::Error {
match &err {
LoadError::StdinNeedsFormat | LoadError::UnknownExtension { .. } => {
anyhow!("{err}: pass --input-format json or --input-format toml")
}
LoadError::Directory { path } => anyhow!(
"{err}\nhelp: `knf {}/*.toml` merges its files as layers",
path.display()
),
}
}
pub fn name_the_set_flag(err: PathError) -> anyhow::Error {
match err {
PathError::IndexInKeyPath { .. } => anyhow!(
"{err}\nhelp: --set takes KEY.PATH=VALUE; an index like servers[0] can be read\n \
by a ${{...}} reference but never written — put the value in a file instead"
),
other => other.into(),
}
}
fn explain_interp(err: InterpError) -> anyhow::Error {
let mut help = String::new();
match &err {
InterpError::Cycle(_) => {
help.push_str("\nhelp: a reference may not resolve, directly or indirectly, to itself")
}
InterpError::Problems(problems) => {
let has = |f: fn(&Problem) -> bool| problems.iter().any(f);
let syntax = has(|p| matches!(p, Problem::Syntax { .. }));
let unresolved = has(|p| matches!(p, Problem::Unresolved { .. }));
if syntax {
help.push_str(
"\nhelp: a reference is `${key.path}` (with `[n]` for array elements) or `${env:NAME}`; write `$$` for a literal `$`",
);
}
if unresolved {
help.push_str(
"\nhelp: `${key.path}` names a key in the merged document, `${env:NAME}` an environment variable",
);
}
if has(|p| matches!(p, Problem::NotStringifiable { .. })) {
help.push_str(
"\nhelp: an object or array reference must be the whole string, not embedded in one",
);
}
if syntax || unresolved {
help.push_str("\nhelp: drop --interpolate to pass `${...}` through untouched");
}
}
}
anyhow!("{err}{help}")
}