use serde_norway::Value as Yaml;
use crate::body;
use crate::doc::Doc;
use crate::error::{OpysError, Result};
use crate::project_config::ProjectConfig;
use crate::refs;
pub(crate) struct DocRows {
pub id: Option<String>,
pub num: Option<i64>,
pub type_name: Option<String>,
pub status: Option<String>,
pub created: Option<String>,
pub updated: Option<String>,
pub title: String,
pub body: String,
pub tags: Vec<TagRow>,
pub relations: Vec<RelRow>,
pub fm_fields: Vec<FmRow>,
}
pub(crate) struct TagRow {
pub seq: i64,
pub tag: String,
pub key: String,
pub value: Option<String>,
}
pub(crate) struct RelRow {
pub field: &'static str,
pub seq: i64,
pub ref_id: String,
pub ref_num: Option<i64>,
pub raw_value: String,
pub title: String,
pub struck: bool,
}
pub(crate) struct FmRow {
pub key: String,
pub value_yaml: Option<String>,
pub value: Option<String>,
pub kind: &'static str,
}
pub(crate) fn id_num(id: &str) -> Option<i64> {
id.rsplit_once('-').and_then(|(_, n)| n.parse::<i64>().ok())
}
pub(crate) fn split_tag(t: &str) -> (&str, Option<&str>) {
match t.find([':', '=']) {
Some(i) => (&t[..i], Some(&t[i + 1..])),
None => (t, None),
}
}
pub(crate) fn decompose(pcfg: &ProjectConfig, doc: &Doc) -> Result<DocRows> {
let mut out = DocRows {
id: None,
num: None,
type_name: None,
status: None,
created: None,
updated: None,
title: body::title(&doc.body),
body: doc.body.clone(),
tags: Vec::new(),
relations: Vec::new(),
fm_fields: Vec::new(),
};
for (k, v) in &doc.frontmatter.map {
let Some(key) = k.as_str() else { continue };
match key {
"id" | "status" | "created" | "updated" if v.is_string() => {
let s = v.as_str().expect("checked is_string").to_string();
match key {
"id" => out.id = Some(s),
"status" => out.status = Some(s),
"created" => out.created = Some(s),
"updated" => out.updated = Some(s),
_ => unreachable!(),
}
}
"tags" if is_string_seq(v) => {
let Yaml::Sequence(seq) = v else {
unreachable!()
};
for (i, item) in seq.iter().enumerate() {
let tag = item.as_str().expect("checked all-string").to_string();
let (tkey, tval) = split_tag(&tag);
out.tags.push(TagRow {
seq: i as i64,
key: tkey.to_string(),
value: tval.map(str::to_string),
tag,
});
}
}
_ if relation_field(key).is_some() && is_string_map(v) => {
let field = relation_field(key).expect("just checked");
let Yaml::Mapping(m) = v else { unreachable!() };
for (i, (rk, rv)) in m.iter().enumerate() {
let ref_id = rk.as_str().expect("checked all-string").to_string();
let raw = rv.as_str().expect("checked all-string").to_string();
out.relations.push(RelRow {
field,
seq: i as i64,
ref_num: id_num(&ref_id),
title: refs::unstrike(&raw).to_string(),
struck: refs::is_struck(&raw),
ref_id,
raw_value: raw,
});
}
}
_ => out.fm_fields.extend(fm_rows(key, v)?),
}
}
out.num = out.id.as_deref().and_then(id_num);
out.type_name = out
.id
.as_deref()
.and_then(|id| pcfg.type_name_for_id(id))
.map(str::to_string);
Ok(out)
}
fn fm_rows(key: &str, v: &Yaml) -> Result<Vec<FmRow>> {
let value_yaml = serde_norway::to_string(v)
.map_err(|e| OpysError::Store(format!("cannot serialize field '{key}': {e}")))?;
let (value, kind) = match v {
Yaml::String(s) => (Some(s.clone()), "str"),
Yaml::Bool(b) => (Some(b.to_string()), "bool"),
Yaml::Number(n) if n.is_i64() || n.is_u64() => (Some(n.to_string()), "int"),
Yaml::Number(n) => (Some(n.to_string()), "float"),
Yaml::Null => (None, "null"),
Yaml::Sequence(_) => (None, "seq"),
Yaml::Mapping(_) => (None, "map"),
Yaml::Tagged(_) => (None, "tagged"),
};
let mut out = vec![FmRow {
key: key.to_string(),
value_yaml: Some(value_yaml),
value,
kind,
}];
if let Yaml::Sequence(seq) = v {
for item in seq {
let elem = match item {
Yaml::String(s) => Some(s.clone()),
Yaml::Bool(b) => Some(b.to_string()),
Yaml::Number(n) => Some(n.to_string()),
_ => None,
};
if let Some(elem) = elem {
out.push(FmRow {
key: key.to_string(),
value_yaml: None,
value: Some(elem),
kind: "elem",
});
}
}
}
Ok(out)
}
fn relation_field(key: &str) -> Option<&'static str> {
refs::RELATION_FIELDS.iter().find(|f| **f == key).copied()
}
fn is_string_seq(v: &Yaml) -> bool {
matches!(v, Yaml::Sequence(seq) if !seq.is_empty() && seq.iter().all(Yaml::is_string))
}
fn is_string_map(v: &Yaml) -> bool {
matches!(v, Yaml::Mapping(m) if !m.is_empty()
&& m.iter().all(|(k, val)| k.is_string() && val.is_string()))
}