specfold_core/model/
doc.rs1use super::id::DocId;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6use std::collections::BTreeMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "lowercase")]
10pub enum Tier {
11 Committed,
13 Local,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct FileEntry {
19 pub path: String,
21 pub sha256: String,
22 pub bytes: u64,
23 pub store: String,
25}
26
27#[derive(Debug, Clone, Serialize)]
29pub struct Located<T> {
30 pub path: String,
31 pub line: usize,
32 pub item: T,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct Frontmatter {
37 pub map: Option<Map<String, Value>>,
39 pub error: Option<String>,
41}
42
43#[derive(Debug, Clone)]
44pub struct Doc {
45 pub id: DocId,
46 pub path: String,
48 pub dir: Option<String>,
50 pub tier: Tier,
51 pub slug: String,
53 pub frontmatter: Frontmatter,
54 pub title: Option<String>,
55 pub chapters: Vec<String>,
57 pub bundle_files: Vec<String>,
59 pub links: Vec<Located<String>>,
60 pub anchors: Vec<Located<String>>,
61 pub path_cites: Vec<Located<String>>,
62 pub body_lines: usize,
63 pub body_hash: String,
65}
66
67impl Doc {
68 pub fn slug_id(&self) -> Option<String> {
71 (!self.slug.is_empty() && self.slug != self.id.key)
72 .then(|| format!("{}:{}/{}", self.id.ty, self.id.owner, self.slug))
73 }
74
75 pub fn fm(&self) -> Option<&Map<String, Value>> {
76 self.frontmatter.map.as_ref()
77 }
78
79 pub fn str_field(&self, key: &str) -> Option<String> {
80 match self.fm()?.get(key)? {
81 Value::String(s) => Some(s.clone()),
82 Value::Number(n) => Some(n.to_string()),
83 Value::Bool(b) => Some(b.to_string()),
84 _ => None,
85 }
86 }
87
88 pub fn list_field(&self, key: &str) -> Vec<String> {
89 list_of(self.fm().and_then(|m| m.get(key)))
90 }
91
92 pub fn status(&self) -> Option<String> {
93 self.str_field("status")
94 }
95
96 pub fn rel(&self) -> BTreeMap<String, Vec<String>> {
98 let mut out = BTreeMap::new();
99 if let Some(Value::Object(m)) = self.fm().and_then(|m| m.get("rel")) {
100 for (k, v) in m {
101 out.insert(k.clone(), list_of(Some(v)));
102 }
103 }
104 out
105 }
106
107 pub fn files(&self) -> Vec<FileEntry> {
108 let Some(Value::Array(items)) = self.fm().and_then(|m| m.get("files")) else { return vec![] };
109 items
110 .iter()
111 .filter_map(|v| {
112 let o = v.as_object()?;
113 Some(FileEntry {
114 path: o.get("path")?.as_str()?.to_string(),
115 sha256: o.get("sha256").and_then(Value::as_str).unwrap_or("").to_string(),
116 bytes: o.get("bytes").and_then(Value::as_u64).unwrap_or(0),
117 store: o.get("store").and_then(Value::as_str).unwrap_or("repo").to_string(),
118 })
119 })
120 .collect()
121 }
122
123 pub fn base_dir(&self) -> String {
125 match &self.dir {
126 Some(d) => d.clone(),
127 None => self.path.rsplit_once('/').map(|(d, _)| d.to_string()).unwrap_or_default(),
128 }
129 }
130}
131
132pub fn list_of(v: Option<&Value>) -> Vec<String> {
133 match v {
134 Some(Value::Array(a)) => a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect(),
135 Some(Value::String(s)) if !s.is_empty() => vec![s.clone()],
136 _ => vec![],
137 }
138}