use anyhow::anyhow;
use knf::fs::{AccumulateError, AccumulateTargetError};
use knf::{
IntegerOutOfRange, InterpError, LoadError, MergeError, NonFiniteFloat, NullInToml, PathError,
Problem, TomlError,
};
pub fn explain_accumulate_target(err: AccumulateTargetError) -> String {
match err {
AccumulateTargetError::Stdin => "--accumulate does not accept stdin",
AccumulateTargetError::InvalidPath => {
"--accumulate requires a relative target path without .. components"
}
AccumulateTargetError::UnknownExtension => {
"--accumulate requires a target with a JSON or TOML extension"
}
}
.to_owned()
}
pub fn explain_accumulate(err: AccumulateError) -> anyhow::Error {
match err {
AccumulateError::Inspect { path, source } => {
anyhow::Error::new(source).context(format!("inspecting `{}`", path.display()))
}
AccumulateError::List { path, source } => {
anyhow::Error::new(source).context(format!("listing `{}`", path.display()))
}
AccumulateError::Directory { path } | AccumulateError::NonRegular { path } => {
anyhow!("`{}` is not a regular file", path.display())
}
AccumulateError::CurrentDirectory(source) => {
anyhow::Error::new(source).context("determining the working directory")
}
}
}
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}")
}