use std::str::FromStr;
use knf::{
BadDatetime, Cycle, Env, EnvValue, Format, IntegerOutOfRange, InterpError, LoadError, Map,
MergeError, MergeOptions, NonFiniteFloat, NullInToml, Number, PathError, PathLeaf, Problem,
RefPath, STDIN, Seg, Syntax, TomlError, Value, interpolate, json_or_string, load_layers, merge,
merge_into, render_path,
};
mod named {
use super::*;
pub fn number(n: &Number) -> String {
format!("{n:?}")
}
pub fn cycle(c: &Cycle) -> String {
c.to_string()
}
pub fn syntax(s: &Syntax) -> String {
s.to_string()
}
pub fn path(segs: &[Seg]) -> String {
render_path(segs)
}
pub fn knobs(_: &Map, _: &MergeOptions, _: Format, _: &dyn Env) {}
}
#[allow(dead_code)]
struct EveryError {
merge: MergeError,
path: PathError,
load: LoadError,
null: NullInToml,
integer: IntegerOutOfRange,
non_finite: NonFiniteFloat,
interp: InterpError,
problem: Problem,
}
fn dir(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("a temp dir");
for (name, text) in files {
std::fs::write(dir.path().join(name), text).expect("writing a fixture");
}
dir
}
#[test]
fn the_public_surface_is_nameable_from_outside_the_crate() {
let refpath = RefPath::from_str("servers[0].host").expect("a well-formed path");
assert_eq!(named::path(refpath.segs()), "servers[0].host");
let keys = RefPath::from_keys(vec!["db".into(), "host".into()]).expect("keys are a path");
assert_eq!(keys.try_into_keys().expect("no indices"), ["db", "host"]);
assert!(matches!(
refpath.try_into_keys(),
Err(PathError::IndexInKeyPath { .. })
));
let leaf = PathLeaf::<String>::from_str("port=8080").expect("a well-formed expression");
assert_eq!(leaf.path().len(), 1);
assert_eq!(json_or_string("8080".to_string()), serde_json::json!(8080));
let opts = MergeOptions {
shallow: true,
..MergeOptions::default()
};
named::knobs(&Map::new(), &opts, Format::Json, &knf::ProcessEnv);
let tree = dir(&[
("a.json", r#"{"xs":[1],"port":8080}"#),
("b.json", r#"{"xs":[2]}"#),
]);
let (layers, _) = load_layers(
&[tree.path().join("a.json"), tree.path().join("b.json")],
None,
)
.expect("json by extension");
let mut merged = merge(layers, &opts).expect("a shallow merge");
merge_into(&mut merged, Value::Object(Map::new()), &opts).expect("an empty layer");
let Value::Object(map) = &merged else {
panic!("the top level is an object")
};
let Value::Number(port) = &map["port"] else {
panic!("8080 is a number")
};
assert_eq!(named::number(port), named::number(&Number::from_u64(8080)));
let tree = dir(&[("cycle.json", r#"{"a":"${b}","b":"${a}"}"#)]);
let (layers, _) = load_layers(&[tree.path().join("cycle.json")], None).expect("json");
let merged = merge(layers, &MergeOptions::default()).expect("one layer");
let err = interpolate(merged, &StubEnv).expect_err("a is b is a");
match &err {
InterpError::Cycle(cycle) => assert!(named::cycle(cycle).starts_with("reference cycle:")),
other => panic!("expected a cycle, got {other}"),
}
let tree = dir(&[("syntax.json", r#"{"a":"${}"}"#)]);
let (layers, _) = load_layers(&[tree.path().join("syntax.json")], None).expect("json");
let merged = merge(layers, &MergeOptions::default()).expect("one layer");
let err = interpolate(merged, &StubEnv).expect_err("`${}` names nothing");
match &err {
InterpError::Problems(problems) => match &problems[0] {
Problem::Syntax { error, .. } => {
assert_eq!(named::syntax(error), Syntax::EmptyRef.to_string())
}
other => panic!("expected a syntax problem, got {other:?}"),
},
other => panic!("expected problems, got {other}"),
}
let tree = dir(&[
("layer", "{}"),
("null.json", r#"{"a":null}"#),
("big.json", r#"{"id":10000000000000000001}"#),
("inf.toml", "timeout = inf\n"),
]);
let err = load_layers(&[tree.path().join("layer")], None).expect_err("no extension, no format");
assert!(matches!(
err.downcast_ref::<LoadError>(),
Some(LoadError::UnknownExtension { .. })
));
let (layers, formats) =
load_layers(&[tree.path().join("null.json")], None).expect("json by extension");
assert_eq!(formats, vec![Format::Json]);
let merged = merge(layers, &MergeOptions::default()).expect("one layer");
let err = knf::format::emit(merged, Format::Toml, true, None).expect_err("a null");
let Some(TomlError::Null(report)) = err.downcast_ref::<TomlError>() else {
panic!("expected the null variant, got {err}")
};
let _: &NullInToml = report;
let mut hand_built = Map::new();
hand_built.insert("d".to_string(), Value::Datetime("nope".to_string()));
let err = knf::format::emit(Value::Object(hand_built), Format::Toml, true, None)
.expect_err("`nope` is not a datetime");
let Some(TomlError::Datetime(report)) = err.downcast_ref::<TomlError>() else {
panic!("expected the datetime variant, got {err}")
};
let _: &BadDatetime = report;
let (layers, _) = load_layers(&[tree.path().join("big.json")], None).expect("json");
let merged = merge(layers, &MergeOptions::default()).expect("one layer");
let err = knf::format::emit(merged, Format::Toml, true, None).expect_err("past i64::MAX");
let Some(TomlError::Integer(report)) = err.downcast_ref::<TomlError>() else {
panic!("expected the integer variant, got {err}")
};
let _: &IntegerOutOfRange = report;
let (layers, _) = load_layers(&[tree.path().join("inf.toml")], None).expect("toml");
let merged = merge(layers, &MergeOptions::default()).expect("one layer");
let err = knf::format::emit(merged, Format::Json, true, None).expect_err("inf");
let report = err
.downcast_ref::<NonFiniteFloat>()
.expect("the typed error survives");
let _: &NonFiniteFloat = report;
assert_eq!(STDIN, "-");
}
struct StubEnv;
impl Env for StubEnv {
fn lookup(&self, name: &str) -> Option<EnvValue> {
(name == "PORT").then(|| EnvValue {
raw: "8080".to_string(),
typed: knf::value::from_json(json_or_string("8080".to_string())),
})
}
}