use super::id::DocId;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Tier {
Committed,
Local,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FileEntry {
pub path: String,
pub sha256: String,
pub bytes: u64,
pub store: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct Located<T> {
pub path: String,
pub line: usize,
pub item: T,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Frontmatter {
pub map: Option<Map<String, Value>>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Doc {
pub id: DocId,
pub path: String,
pub dir: Option<String>,
pub tier: Tier,
pub slug: String,
pub frontmatter: Frontmatter,
pub title: Option<String>,
pub chapters: Vec<String>,
pub bundle_files: Vec<String>,
pub links: Vec<Located<String>>,
pub anchors: Vec<Located<String>>,
pub path_cites: Vec<Located<String>>,
pub body_lines: usize,
pub body_hash: String,
}
impl Doc {
pub fn fm(&self) -> Option<&Map<String, Value>> {
self.frontmatter.map.as_ref()
}
pub fn str_field(&self, key: &str) -> Option<String> {
match self.fm()?.get(key)? {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
pub fn list_field(&self, key: &str) -> Vec<String> {
list_of(self.fm().and_then(|m| m.get(key)))
}
pub fn status(&self) -> Option<String> {
self.str_field("status")
}
pub fn rel(&self) -> BTreeMap<String, Vec<String>> {
let mut out = BTreeMap::new();
if let Some(Value::Object(m)) = self.fm().and_then(|m| m.get("rel")) {
for (k, v) in m {
out.insert(k.clone(), list_of(Some(v)));
}
}
out
}
pub fn files(&self) -> Vec<FileEntry> {
let Some(Value::Array(items)) = self.fm().and_then(|m| m.get("files")) else { return vec![] };
items
.iter()
.filter_map(|v| {
let o = v.as_object()?;
Some(FileEntry {
path: o.get("path")?.as_str()?.to_string(),
sha256: o.get("sha256").and_then(Value::as_str).unwrap_or("").to_string(),
bytes: o.get("bytes").and_then(Value::as_u64).unwrap_or(0),
store: o.get("store").and_then(Value::as_str).unwrap_or("repo").to_string(),
})
})
.collect()
}
pub fn base_dir(&self) -> String {
match &self.dir {
Some(d) => d.clone(),
None => self.path.rsplit_once('/').map(|(d, _)| d.to_string()).unwrap_or_default(),
}
}
}
pub fn list_of(v: Option<&Value>) -> Vec<String> {
match v {
Some(Value::Array(a)) => a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect(),
Some(Value::String(s)) if !s.is_empty() => vec![s.clone()],
_ => vec![],
}
}