Skip to main content

specfold_core/import/
convert.rs

1//! Converting an old document's frontmatter to the specfold shape.
2
3use super::map::{ImportMap, SourceRule};
4use crate::model::doc::list_of;
5use crate::model::stamp;
6use serde_json::{Map, Value};
7
8pub struct Converted {
9    pub fm: Map<String, Value>,
10    pub status_from: String,
11    pub aliases: Vec<String>,
12    pub unresolved: Vec<String>,
13}
14
15pub fn status(map: &ImportMap, rule: &SourceRule, old: &str, allowed: &[String]) -> Option<String> {
16    let mapped = rule.status.get(old).or_else(|| map.status.get(old)).cloned().unwrap_or_else(|| old.to_string());
17    allowed.contains(&mapped).then_some(mapped)
18}
19
20pub fn frontmatter(
21    map: &ImportMap,
22    rule: &SourceRule,
23    old: &Map<String, Value>,
24    allowed: &[String],
25    fallback_title: &str,
26) -> Converted {
27    let mut fm = Map::new();
28    let mut rel = Map::new();
29    let mut unresolved = vec![];
30    let mut aliases: Vec<String> = rule.alias_fields.iter().flat_map(|f| list_of(old.get(f))).collect();
31    for (k, v) in old {
32        let target = map.keys.get(k).map(String::as_str).unwrap_or(k.as_str());
33        match target {
34            "-" | "id" | "status" | "created" | "updated" | "title" => {}
35            t if t.starts_with("rel.") => {
36                let kind = &t[4..];
37                let list = rel.entry(kind.to_string()).or_insert_with(|| Value::Array(vec![]));
38                if let Value::Array(items) = list {
39                    items.extend(list_of(Some(v)).into_iter().map(Value::String));
40                }
41            }
42            "aliases" => aliases.extend(list_of(Some(v))),
43            "rel" => {
44                if let Value::Object(m) = v {
45                    rel.extend(m.clone());
46                }
47            }
48            t => {
49                fm.insert(t.to_string(), v.clone());
50            }
51        }
52    }
53    let status_from = old.get("status").and_then(Value::as_str).unwrap_or("").trim().to_string();
54    let status_new = if status_from.is_empty() {
55        rule.default_status.clone().filter(|s| allowed.contains(s))
56    } else {
57        status(map, rule, &status_from, allowed)
58    };
59    let status_new = status_new.unwrap_or_else(|| {
60        unresolved.push(format!("status '{status_from}' has no mapping"));
61        allowed.first().cloned().unwrap_or_else(|| "draft".into())
62    });
63    let date = |k: &str| {
64        old.get(k)
65            .and_then(|v| v.as_str().map(str::to_string).or_else(|| v.as_i64().map(|n| n.to_string())))
66            .and_then(|s| stamp::normalize_date(&s))
67    };
68    let created = date("created").or_else(|| date("date"));
69    let updated = date("updated").or_else(|| created.clone());
70    let title =
71        old.get("title").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| fallback_title.to_string());
72    let mut out = Map::new();
73    out.insert("title".into(), Value::String(title));
74    out.insert("status".into(), Value::String(status_new));
75    if let Some(c) = created {
76        out.insert("created".into(), Value::String(c));
77    }
78    if let Some(u) = updated {
79        out.insert("updated".into(), Value::String(u));
80    }
81    out.extend(fm);
82    if !rel.is_empty() {
83        out.insert("rel".into(), Value::Object(rel));
84    }
85    aliases.sort();
86    aliases.dedup();
87    Converted { fm: out, status_from, aliases, unresolved }
88}