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 fm(&self) -> Option<&Map<String, Value>> {
69 self.frontmatter.map.as_ref()
70 }
71
72 pub fn str_field(&self, key: &str) -> Option<String> {
73 match self.fm()?.get(key)? {
74 Value::String(s) => Some(s.clone()),
75 Value::Number(n) => Some(n.to_string()),
76 Value::Bool(b) => Some(b.to_string()),
77 _ => None,
78 }
79 }
80
81 pub fn list_field(&self, key: &str) -> Vec<String> {
82 list_of(self.fm().and_then(|m| m.get(key)))
83 }
84
85 pub fn status(&self) -> Option<String> {
86 self.str_field("status")
87 }
88
89 pub fn rel(&self) -> BTreeMap<String, Vec<String>> {
91 let mut out = BTreeMap::new();
92 if let Some(Value::Object(m)) = self.fm().and_then(|m| m.get("rel")) {
93 for (k, v) in m {
94 out.insert(k.clone(), list_of(Some(v)));
95 }
96 }
97 out
98 }
99
100 pub fn files(&self) -> Vec<FileEntry> {
101 let Some(Value::Array(items)) = self.fm().and_then(|m| m.get("files")) else { return vec![] };
102 items
103 .iter()
104 .filter_map(|v| {
105 let o = v.as_object()?;
106 Some(FileEntry {
107 path: o.get("path")?.as_str()?.to_string(),
108 sha256: o.get("sha256").and_then(Value::as_str).unwrap_or("").to_string(),
109 bytes: o.get("bytes").and_then(Value::as_u64).unwrap_or(0),
110 store: o.get("store").and_then(Value::as_str).unwrap_or("repo").to_string(),
111 })
112 })
113 .collect()
114 }
115
116 pub fn base_dir(&self) -> String {
118 match &self.dir {
119 Some(d) => d.clone(),
120 None => self.path.rsplit_once('/').map(|(d, _)| d.to_string()).unwrap_or_default(),
121 }
122 }
123}
124
125pub fn list_of(v: Option<&Value>) -> Vec<String> {
126 match v {
127 Some(Value::Array(a)) => a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect(),
128 Some(Value::String(s)) if !s.is_empty() => vec![s.clone()],
129 _ => vec![],
130 }
131}