use serde_yaml::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChildrenNorm {
pub children: bool,
pub source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeriesNorm {
pub series: bool,
pub order: Option<Vec<String>>,
}
pub fn normalize_children(v: &Value) -> ChildrenNorm {
match v {
Value::Bool(b) => ChildrenNorm { children: *b, source: None },
Value::String(s) => {
let t = s.trim();
match t {
"true" => ChildrenNorm { children: true, source: None },
"false" | "" => ChildrenNorm { children: false, source: None },
_ => ChildrenNorm { children: true, source: Some(s.clone()) },
}
}
_ => ChildrenNorm { children: false, source: None },
}
}
pub fn normalize_series(v: &Value) -> SeriesNorm {
match v {
Value::Bool(b) => SeriesNorm { series: *b, order: None },
Value::String(s) => match s.trim() {
"true" => SeriesNorm { series: true, order: None },
_ => SeriesNorm { series: false, order: None },
},
Value::Sequence(items) => {
if items.is_empty() {
return SeriesNorm { series: true, order: None };
}
let mut order = Vec::with_capacity(items.len());
for it in items {
match it {
Value::String(s) => order.push(s.clone()),
_ => return SeriesNorm { series: false, order: None },
}
}
SeriesNorm { series: true, order: Some(order) }
}
_ => SeriesNorm { series: false, order: None },
}
}
pub fn normalize_credit_rows(v: &Value) -> Result<Vec<String>, String> {
fn push_rows(out: &mut Vec<String>, raw: &str) {
for line in raw.lines() {
let t = line.trim();
if !t.is_empty() {
out.push(t.to_string());
}
}
}
let mut rows = Vec::new();
match v {
Value::Null => {}
Value::String(s) => push_rows(&mut rows, s),
Value::Sequence(items) => {
for it in items {
match it {
Value::String(s) => push_rows(&mut rows, s),
Value::Null => {}
other => {
return Err(format!(
"expected a string or a list of strings, found a list holding {}",
shape_name(other)
))
}
}
}
}
other => {
return Err(format!(
"expected a string or a list of strings, found {}",
shape_name(other)
))
}
}
Ok(rows)
}
fn shape_name(v: &Value) -> &'static str {
match v {
Value::Null => "nothing",
Value::Bool(_) => "a true/false value",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Sequence(_) => "a list",
Value::Mapping(_) => "a set of key/value pairs",
Value::Tagged(_) => "a tagged value",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &str) -> Value {
Value::String(v.to_string())
}
#[test]
fn children_bool_true() {
assert_eq!(
normalize_children(&Value::Bool(true)),
ChildrenNorm { children: true, source: None }
);
}
#[test]
fn children_bool_false() {
assert_eq!(
normalize_children(&Value::Bool(false)),
ChildrenNorm { children: false, source: None }
);
}
#[test]
fn children_string_true_false() {
assert_eq!(
normalize_children(&s("true")),
ChildrenNorm { children: true, source: None }
);
assert_eq!(
normalize_children(&s("false")),
ChildrenNorm { children: false, source: None }
);
}
#[test]
fn children_empty_string_is_off() {
assert_eq!(
normalize_children(&s("")),
ChildrenNorm { children: false, source: None }
);
}
#[test]
fn children_wikilink() {
assert_eq!(
normalize_children(&s("[[News]]")),
ChildrenNorm { children: true, source: Some("[[News]]".to_string()) }
);
}
#[test]
fn children_bare_path() {
assert_eq!(
normalize_children(&s("news/index.md")),
ChildrenNorm { children: true, source: Some("news/index.md".to_string()) }
);
}
#[test]
fn children_other_types_off() {
assert_eq!(
normalize_children(&Value::Null),
ChildrenNorm { children: false, source: None }
);
assert_eq!(
normalize_children(&Value::Number(3.into())),
ChildrenNorm { children: false, source: None }
);
}
#[test]
fn series_bool() {
assert_eq!(
normalize_series(&Value::Bool(true)),
SeriesNorm { series: true, order: None }
);
assert_eq!(
normalize_series(&Value::Bool(false)),
SeriesNorm { series: false, order: None }
);
}
#[test]
fn series_list_of_strings() {
let seq = Value::Sequence(vec![s("[[Ch 1]]"), s("[[Ch 2]]")]);
assert_eq!(
normalize_series(&seq),
SeriesNorm {
series: true,
order: Some(vec!["[[Ch 1]]".to_string(), "[[Ch 2]]".to_string()])
}
);
}
#[test]
fn series_malformed_list_off() {
let seq = Value::Sequence(vec![s("[[Ch 1]]"), Value::Bool(true)]);
assert_eq!(
normalize_series(&seq),
SeriesNorm { series: false, order: None }
);
}
#[test]
fn series_empty_list_is_flag() {
assert_eq!(
normalize_series(&Value::Sequence(vec![])),
SeriesNorm { series: true, order: None }
);
}
#[test]
fn series_other_off() {
assert_eq!(
normalize_series(&Value::Null),
SeriesNorm { series: false, order: None }
);
}
#[test]
fn children_roundtrip_via_to_value() {
let raw = s("[[News]]");
let reexpressed = serde_yaml::to_value("[[News]]").unwrap();
assert_eq!(normalize_children(&raw), normalize_children(&reexpressed));
}
#[test]
fn credit_single_string_is_one_row() {
assert_eq!(normalize_credit_rows(&s("作者 糜緒洋")).unwrap(), vec!["作者 糜緒洋"]);
}
#[test]
fn credit_block_scalar_is_one_row_per_line() {
let block = s("作者 糜緒洋\n編輯 謝丁\n首發媒體 [端傳媒](https://x)\n");
assert_eq!(
normalize_credit_rows(&block).unwrap(),
vec"]
);
}
#[test]
fn credit_list_is_one_row_per_item() {
let seq = Value::Sequence(vec![s("作者 X"), s("編輯 Y")]);
assert_eq!(normalize_credit_rows(&seq).unwrap(), vec!["作者 X", "編輯 Y"]);
}
#[test]
fn credit_rows_are_trimmed_and_blank_lines_dropped() {
assert_eq!(
normalize_credit_rows(&s(" 作者 X \n\n \n編輯 Y\n")).unwrap(),
vec!["作者 X", "編輯 Y"]
);
}
#[test]
fn credit_empty_shapes_yield_no_rows() {
assert!(normalize_credit_rows(&s("")).unwrap().is_empty());
assert!(normalize_credit_rows(&s(" \n ")).unwrap().is_empty());
assert!(normalize_credit_rows(&Value::Sequence(vec![])).unwrap().is_empty());
assert!(normalize_credit_rows(&Value::Null).unwrap().is_empty());
}
#[test]
fn credit_wrong_shapes_report_what_was_found() {
let err = normalize_credit_rows(&Value::Bool(true)).unwrap_err();
assert!(err.contains("true/false"), "message names the shape: {err}");
let err = normalize_credit_rows(&Value::Mapping(Default::default())).unwrap_err();
assert!(err.contains("key/value"), "message names the shape: {err}");
let err = normalize_credit_rows(&Value::Sequence(vec![s("作者 X"), Value::Bool(true)])).unwrap_err();
assert!(err.contains("list holding"), "message names the offending item: {err}");
}
}