use super::super::field::{Field, FieldKind, ValueKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form {
pub text: &'static str,
pub examples: &'static [&'static str],
}
pub const DURATION_FORMS: &[Form] = &[
Form {
text: "500ms, 2s, 5m, 1h",
examples: &["500ms", "2s", "5m", "1h"],
},
Form {
text: "a bare number is milliseconds",
examples: &["250"],
},
];
pub const DURATION_REFUSALS: &[Form] = &[
Form {
text: "a negative",
examples: &["-1s"],
},
Form {
text: "a unit shep does not know",
examples: &["3 fortnights"],
},
];
pub const MEMORY_FORMS: &[Form] = &[
Form {
text: "512M, 2G",
examples: &["512M", "2G"],
},
Form {
text: "a bare number is bytes",
examples: &["1048576"],
},
];
pub const MEMORY_REFUSALS: &[Form] = &[
Form {
text: "a two-letter unit",
examples: &["10MB", "2GB"],
},
Form {
text: "a lowercase unit",
examples: &["512m", "2g"],
},
Form {
text: "a fraction",
examples: &["1.5G"],
},
];
pub const BOOL_FORMS: &[Form] = &[Form {
text: "true or false",
examples: &["true", "false"],
}];
pub const INTEGER_FORMS: &[Form] = &[Form {
text: "a whole number",
examples: &["3"],
}];
pub const NUMBER_FORMS: &[Form] = &[Form {
text: "a number",
examples: &["10", "2.5"],
}];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Bullets {
pub accepts: Vec<String>,
pub refuses: Vec<String>,
}
impl Bullets {
#[must_use]
pub fn is_empty(&self) -> bool {
self.accepts.is_empty() && self.refuses.is_empty()
}
}
fn table_for(field: &Field) -> (&'static [Form], &'static [Form]) {
match field.value_kind {
Some(ValueKind::UpDuration) => return (DURATION_FORMS, DURATION_REFUSALS),
Some(ValueKind::MemSize) => return (MEMORY_FORMS, MEMORY_REFUSALS),
None => {}
}
match field.kind {
FieldKind::Bool => (BOOL_FORMS, &[]),
FieldKind::Integer => (INTEGER_FORMS, &[]),
FieldKind::Number => (NUMBER_FORMS, &[]),
_ => (&[], &[]),
}
}
#[must_use]
pub fn bullets(field: &Field) -> Bullets {
if !field.accepts.is_empty() || !field.refuses.is_empty() {
return Bullets {
accepts: field.accepts.clone(),
refuses: field.refuses.clone(),
};
}
let (accepts, refuses) = table_for(field);
Bullets {
accepts: accepts.iter().map(|f| f.text.to_owned()).collect(),
refuses: refuses.iter().map(|f| f.text.to_owned()).collect(),
}
}
#[cfg(test)]
mod tests {
use super::super::fixtures::text_field;
use super::*;
use shep_core::values::{MemSize, UpDuration};
#[test]
fn every_accepted_duration_form_parses() {
for form in DURATION_FORMS {
for example in form.examples {
assert!(
example.parse::<UpDuration>().is_ok(),
"{} is listed as accepted but does not parse",
example
);
}
}
}
#[test]
fn every_refused_duration_form_fails_to_parse() {
for form in DURATION_REFUSALS {
for example in form.examples {
assert!(
example.parse::<UpDuration>().is_err(),
"{} is listed as refused but parses",
example
);
}
}
}
#[test]
fn every_accepted_memory_form_parses() {
for form in MEMORY_FORMS {
for example in form.examples {
assert!(
example.parse::<MemSize>().is_ok(),
"{} is listed as accepted but does not parse",
example
);
}
}
}
#[test]
fn every_refused_memory_form_fails_to_parse() {
for form in MEMORY_REFUSALS {
for example in form.examples {
assert!(
example.parse::<MemSize>().is_err(),
"{} is listed as refused but parses",
example
);
}
}
}
fn forms_named(text: &str) -> usize {
text.split(',').flat_map(|part| part.split(" or ")).count()
}
#[test]
fn every_form_the_text_names_has_an_example() {
for (name, forms) in [
("DURATION_FORMS", DURATION_FORMS),
("DURATION_REFUSALS", DURATION_REFUSALS),
("MEMORY_FORMS", MEMORY_FORMS),
("MEMORY_REFUSALS", MEMORY_REFUSALS),
("BOOL_FORMS", BOOL_FORMS),
("INTEGER_FORMS", INTEGER_FORMS),
("NUMBER_FORMS", NUMBER_FORMS),
] {
for form in forms {
assert!(
forms_named(form.text) <= form.examples.len(),
"{name}: text \"{}\" names {} forms but examples has only {}",
form.text,
forms_named(form.text),
form.examples.len()
);
}
}
}
#[test]
fn every_accepted_number_form_parses_as_a_finite_float() {
for form in NUMBER_FORMS {
for example in form.examples {
let parsed = example.parse::<f64>();
assert!(
parsed.is_ok_and(|value| value.is_finite()),
"{example} is listed as accepted and the gate must file it"
);
}
}
}
#[test]
fn a_field_with_its_own_accepts_replaces_the_type_table() {
let mut field = text_field("cwd");
field.value_kind = Some(ValueKind::UpDuration);
field.accepts = vec!["only this".to_owned()];
let bullets = bullets(&field);
assert_eq!(bullets.accepts, vec!["only this".to_owned()]);
}
#[test]
fn a_field_with_no_accepts_takes_the_type_table() {
let mut field = text_field("min_uptime");
field.value_kind = Some(ValueKind::UpDuration);
let bullets = bullets(&field);
assert!(bullets.accepts.len() > 1);
}
#[test]
fn a_plain_text_field_with_no_annotation_has_no_bullets() {
let bullets = bullets(&text_field("fold"));
assert!(bullets.is_empty());
}
#[test]
fn every_type_table_reaches_the_panel() {
let cases = [
(FieldKind::Bool, None, BOOL_FORMS, &[] as &[Form]),
(FieldKind::Integer, None, INTEGER_FORMS, &[]),
(FieldKind::Number, None, NUMBER_FORMS, &[]),
(
FieldKind::Text,
Some(ValueKind::MemSize),
MEMORY_FORMS,
MEMORY_REFUSALS,
),
(
FieldKind::Text,
Some(ValueKind::UpDuration),
DURATION_FORMS,
DURATION_REFUSALS,
),
];
for (kind, value_kind, accepts, refuses) in cases {
let mut field = text_field("max_memory");
field.kind = kind.clone();
field.value_kind = value_kind;
let bullets = bullets(&field);
let texts = |forms: &[Form]| -> Vec<String> {
forms.iter().map(|form| form.text.to_owned()).collect()
};
assert_eq!(bullets.accepts, texts(accepts), "{kind:?} accepts");
assert_eq!(bullets.refuses, texts(refuses), "{kind:?} refuses");
}
}
}