use crate::layer::{Layer, LayerCtx, LayerError, LayerOutput, Warning, WarningKind};
use crate::registry::Registry;
use crate::source::{Origin, SourceKind};
use crate::value::Value;
pub struct CliLayer {
given: Vec<(String, Given)>,
}
enum Given {
Text(String),
Shaped(Value),
Unrepresentable,
}
impl CliLayer {
pub fn new(given: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
Self {
given: given
.into_iter()
.map(|(key, value)| (key.into(), Given::Text(value.into())))
.collect(),
}
}
pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.given.push((key.into(), Given::Text(value.into())));
self
}
pub fn with_value(mut self, key: impl Into<String>, value: Value) -> Self {
self.given.push((key.into(), Given::Shaped(value)));
self
}
pub fn with_unrepresentable(mut self, key: impl Into<String>) -> Self {
self.given.push((key.into(), Given::Unrepresentable));
self
}
pub fn is_empty(&self) -> bool {
self.given.is_empty()
}
fn origin(&self, registry: Registry, key: &str) -> Origin {
let named = registry
.lookup_exact(key)
.and_then(|id| registry.get(id).cli.first().copied());
Origin::new(SourceKind::CLI, named.unwrap_or(key))
}
}
impl Layer for CliLayer {
fn source(&self) -> SourceKind {
SourceKind::CLI
}
fn load(&self, ctx: &LayerCtx) -> Result<LayerOutput, LayerError> {
let mut out = LayerOutput::new();
for (key, given) in &self.given {
let origin = self.origin(ctx.registry(), key);
let entry = match given {
Given::Text(raw) => ctx.entry_for_key(key, raw, origin),
Given::Shaped(value) => ctx.entry_from_value(key, value.clone(), origin),
Given::Unrepresentable => {
let named = origin.describe();
out.warn(
Warning::at(
format!(
"{named} was given a value that is not text, so {key} keeps the \
value it had"
),
origin,
)
.of(WarningKind::WrongType),
);
continue;
}
};
match entry {
Ok(entry) => out.push(entry),
Err(warning) => out.warn(warning),
}
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::{PropMeta, Scope};
use crate::resolve::{resolve, Layers};
use crate::ty::{Parser, Ty};
use crate::value::Const;
static PROPS: &[PropMeta] = &[
PropMeta {
default: Some(Const::Int(4)),
envs: &["HK_JOBS"],
cli: &["--jobs", "-j"],
..PropMeta::new("jobs", Ty::Uint)
},
PropMeta {
cli: &["--colour", "--no-colour"],
..PropMeta::new("colour", Ty::Bool)
},
PropMeta {
parse: Some(Parser::ListByComma),
cli: &["--exclude"],
..PropMeta::new("exclude", Ty::List(&Ty::String))
},
PropMeta {
scope: Scope::Global,
cli: &["--trusted"],
..PropMeta::new("trusted", Ty::Bool)
},
PropMeta::new("stash", Ty::String),
PropMeta {
cli: &["--concurrency"],
renamed_to: Some("jobs"),
..PropMeta::new("concurrency", Ty::Uint)
},
];
const REGISTRY: Registry = Registry::new(PROPS);
#[test]
fn a_flag_that_was_given_sets_its_setting() {
let cli = CliLayer::new([("jobs", "8")]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
assert_eq!(
resolved.origin_key("jobs").map(|o| o.describe()),
Some("--jobs")
);
}
#[test]
fn the_command_line_outranks_everything() {
let cli = CliLayer::new([("jobs", "8")]);
let env = crate::env::EnvLayer::new([("HK_JOBS".to_string(), "6".to_string())]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli).then(&env)).expect("resolves");
assert_eq!(resolved.get_key("jobs"), Some(&Value::Int(8)));
}
#[test]
fn a_flag_that_was_not_given_is_not_an_entry() {
let cli = CliLayer::new(Vec::<(String, String)>::new());
assert!(cli.is_empty());
let env = crate::env::EnvLayer::new([("HK_JOBS".to_string(), "6".to_string())]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli).then(&env)).expect("resolves");
assert_eq!(
resolved.get_key("jobs"),
Some(&Value::Int(6)),
"the environment should still be what set it"
);
assert_eq!(resolved.get_key("colour"), None, "no flag, no value");
}
#[test]
fn a_value_that_already_has_a_shape_is_taken_as_it_is() {
let cli = CliLayer::new(Vec::<(String, String)>::new())
.with_value("colour", Value::Bool(false))
.with_value(
"exclude",
Value::List(vec![Value::from("target"), Value::from("dist")]),
);
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(resolved.get_key("colour"), Some(&Value::Bool(false)));
assert_eq!(
resolved.get_key("exclude"),
Some(&Value::List(vec![
Value::from("target"),
Value::from("dist")
]))
);
}
#[test]
fn a_flag_may_set_a_setting_no_file_can() {
let cli = CliLayer::new([("trusted", "true")]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(resolved.get_key("trusted"), Some(&Value::Bool(true)));
assert!(resolved.warnings.is_empty(), "{:?}", resolved.warnings);
}
#[test]
fn a_setting_with_no_declared_flag_is_named_by_its_key() {
let cli = CliLayer::new([("stash", "none")]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(resolved.get_key("stash"), Some(&Value::from("none")));
assert_eq!(
resolved.origin_key("stash").map(|o| o.describe()),
Some("stash")
);
}
#[test]
fn a_flag_bound_to_an_old_name_is_named_by_the_old_name() {
let cli = CliLayer::new([("concurrency", "8")]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(
resolved.get_key("jobs"),
Some(&Value::Int(8)),
"still folds"
);
assert_eq!(
resolved.origin_key("jobs").map(|o| o.describe()),
Some("--concurrency")
);
}
#[test]
fn a_value_that_is_not_text_is_said_rather_than_rendered() {
let cli = CliLayer::new([("jobs", "8")]).with_unrepresentable("exclude");
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(
resolved.get_key("jobs"),
Some(&Value::Int(8)),
"the rest is unaffected"
);
assert_eq!(
resolved.get_key("exclude"),
None,
"and this keeps what it had"
);
let kinds: Vec<_> = resolved.warnings.iter().map(|w| w.kind).collect();
assert_eq!(kinds, vec![crate::layer::WarningKind::WrongType]);
assert!(
resolved.warnings[0]
.message
.starts_with("--exclude was given a value that is not text"),
"{:?}",
resolved.warnings[0].message
);
}
#[test]
fn a_flag_bound_to_nothing_is_a_warning_rather_than_a_crash() {
let cli = CliLayer::new([("nonesuch", "1"), ("jobs", "lots")]);
let resolved = resolve(REGISTRY, Layers::new().then(&cli)).expect("resolves");
assert_eq!(
resolved.get_key("jobs"),
Some(&Value::Int(4)),
"the default"
);
let kinds: Vec<_> = resolved.warnings.iter().map(|w| w.kind).collect();
assert_eq!(
kinds,
vec![
crate::layer::WarningKind::UnknownSetting,
crate::layer::WarningKind::WrongType
]
);
}
}