use serde_json::Value;
use super::{Neighbour, all_strings};
pub(super) fn suggestions(init: Option<&Value>) -> Option<Vec<String>> {
let values = init?.get("suggest")?.as_array()?;
all_strings(values, Value::as_str)
}
pub(super) fn strings(init: Option<&Value>, key: &str) -> Vec<String> {
let Some(values) = init.and_then(|i| i.get(key)).and_then(Value::as_array) else {
return Vec::new();
};
all_strings(values, Value::as_str).unwrap_or_default()
}
pub(super) fn neighbours(init: Option<&Value>) -> Vec<Neighbour> {
let Some(values) = init
.and_then(|i| i.get("neighbours"))
.and_then(Value::as_array)
else {
return Vec::new();
};
values
.iter()
.filter_map(|entry| {
let field = entry.get("field")?.as_str()?.to_owned();
let note = entry.get("note")?.as_str()?.to_owned();
Some(Neighbour { field, note })
})
.collect()
}
#[cfg(test)]
mod tests {
use super::super::fixtures::{props, real_field_set};
use super::super::{FieldKind, FieldSet, field_from};
use super::{strings, suggestions};
use serde_json::Map;
use serde_json::json;
#[test]
fn a_field_with_init_suggest_cycles_and_still_types() {
let schema = json!({
"type": ["string", "null"],
"init": { "suggest": ["SIGTERM", "SIGINT"] }
});
let field = field_from("kill_signal", &schema, &Map::new());
assert_eq!(
field.kind,
FieldKind::Suggested(vec!["SIGTERM".into(), "SIGINT".into()])
);
assert!(field.editable, "a suggestion is not a constraint");
}
#[test]
fn kill_signal_and_cron_restart_both_carry_suggestions() {
let set = real_field_set();
for key in ["kill_signal", "cron_restart"] {
let field = set.by_key(key).unwrap_or_else(|| panic!("no {key}"));
assert!(
matches!(field.kind, FieldKind::Suggested(ref names) if !names.is_empty()),
"{key}: {:?}",
field.kind
);
}
}
#[test]
fn a_field_carries_its_example_from_the_init_block() {
let set = FieldSet::from_properties(
&props(json!({
"cwd": { "type": ["string", "null"],
"init": { "example": "/srv/app", "group": "process" } }
})),
&Map::new(),
&["process"],
);
assert_eq!(
set.by_key("cwd").unwrap().example.as_deref(),
Some("/srv/app")
);
}
#[test]
fn a_field_carries_its_accepted_and_refused_forms() {
let set = FieldSet::from_properties(
&props(json!({
"cwd": { "type": ["string", "null"], "init": {
"group": "process",
"accepts": ["an absolute or relative path", "~ expands, $VARS do not"],
"refuses": ["a path the daemon's user cannot enter"]
} }
})),
&Map::new(),
&["process"],
);
let field = set.by_key("cwd").unwrap();
assert_eq!(field.accepts.len(), 2);
assert_eq!(field.refuses.len(), 1);
}
#[test]
fn a_neighbour_carries_a_field_name_and_a_note() {
let set = FieldSet::from_properties(
&props(json!({
"cwd": { "type": ["string", "null"], "init": { "group": "process",
"neighbours": [{ "field": "script", "note": "resolved against this cwd" }] } }
})),
&Map::new(),
&["process"],
);
let neighbours = &set.by_key("cwd").unwrap().neighbours;
assert_eq!(neighbours[0].field, "script");
assert_eq!(neighbours[0].note, "resolved against this cwd");
}
#[test]
fn a_malformed_neighbour_entry_is_dropped() {
let set = FieldSet::from_properties(
&props(json!({
"cwd": { "type": ["string", "null"], "init": { "group": "process",
"neighbours": [{ "field": "script" }, { "note": "orphan" }] } }
})),
&Map::new(),
&["process"],
);
assert!(set.by_key("cwd").unwrap().neighbours.is_empty());
}
#[test]
fn a_field_without_the_new_keys_carries_empty_lists() {
let set = FieldSet::from_properties(
&props(json!({ "cwd": { "type": ["string", "null"] } })),
&Map::new(),
&[],
);
let field = set.by_key("cwd").unwrap();
assert!(field.example.is_none());
assert!(field.accepts.is_empty());
assert!(field.refuses.is_empty());
assert!(field.neighbours.is_empty());
}
#[test]
fn a_suggest_list_with_a_non_string_entry_is_rejected() {
let init = json!({ "suggest": ["a", 1] });
assert_eq!(suggestions(Some(&init)), None);
}
#[test]
fn an_accepts_list_with_a_non_string_entry_is_empty() {
let init = json!({ "accepts": ["a", 1] });
assert_eq!(strings(Some(&init), "accepts"), Vec::<String>::new());
}
}