1use serde::Serialize;
2use std::collections::HashMap;
3
4#[derive(Clone)]
8pub struct DocEntry(HashMap<String, serde_json::Value>);
9
10impl Serialize for DocEntry {
11 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
12 where
13 S: serde::Serializer,
14 {
15 self.0.serialize(serializer)
16 }
17}
18
19impl DocEntry {
20 fn base() -> HashMap<String, serde_json::Value> {
21 let mut map = HashMap::new();
22 map.insert("phase".to_string(), serde_json::Value::String("runtime".to_string()));
23 map
24 }
25
26 #[must_use]
28 pub fn note(text: &str) -> Self {
29 let mut map = Self::base();
30 map.insert("kind".to_string(), serde_json::Value::String("note".to_string()));
31 map.insert("text".to_string(), serde_json::Value::String(text.to_string()));
32 DocEntry(map)
33 }
34
35 #[must_use]
37 pub fn tag(names: &[&str]) -> Self {
38 let mut map = Self::base();
39 map.insert("kind".to_string(), serde_json::Value::String("tag".to_string()));
40 let values: Vec<serde_json::Value> =
41 names.iter().map(|n| serde_json::Value::String((*n).to_string())).collect();
42 map.insert("names".to_string(), serde_json::Value::Array(values));
43 DocEntry(map)
44 }
45
46 #[must_use]
48 pub fn kv(label: &str, value: serde_json::Value) -> Self {
49 let mut map = Self::base();
50 map.insert("kind".to_string(), serde_json::Value::String("kv".to_string()));
51 map.insert("label".to_string(), serde_json::Value::String(label.to_string()));
52 map.insert("value".to_string(), value);
53 DocEntry(map)
54 }
55
56 #[must_use]
58 pub fn code(label: &str, content: &str, lang: Option<&str>) -> Self {
59 let mut map = Self::base();
60 map.insert("kind".to_string(), serde_json::Value::String("code".to_string()));
61 map.insert("label".to_string(), serde_json::Value::String(label.to_string()));
62 map.insert("content".to_string(), serde_json::Value::String(content.to_string()));
63 if let Some(l) = lang {
64 map.insert("lang".to_string(), serde_json::Value::String(l.to_string()));
65 }
66 DocEntry(map)
67 }
68
69 pub fn json_doc(label: &str, value: &impl Serialize) -> Self {
71 let json_str = serde_json::to_string_pretty(value).unwrap_or_default();
72 Self::code(label, &json_str, Some("json"))
73 }
74
75 #[must_use]
77 pub fn table(label: &str, columns: &[&str], rows: &[&[&str]]) -> Self {
78 let mut map = Self::base();
79 map.insert("kind".to_string(), serde_json::Value::String("table".to_string()));
80 map.insert("label".to_string(), serde_json::Value::String(label.to_string()));
81
82 let col_values: Vec<serde_json::Value> =
83 columns.iter().map(|c| serde_json::Value::String((*c).to_string())).collect();
84 map.insert("columns".to_string(), serde_json::Value::Array(col_values));
85
86 let row_values: Vec<serde_json::Value> = rows
87 .iter()
88 .map(|row| {
89 let cells: Vec<serde_json::Value> =
90 row.iter().map(|c| serde_json::Value::String((*c).to_string())).collect();
91 serde_json::Value::Array(cells)
92 })
93 .collect();
94 map.insert("rows".to_string(), serde_json::Value::Array(row_values));
95 DocEntry(map)
96 }
97
98 #[must_use]
100 pub fn link(label: &str, url: &str) -> Self {
101 let mut map = Self::base();
102 map.insert("kind".to_string(), serde_json::Value::String("link".to_string()));
103 map.insert("label".to_string(), serde_json::Value::String(label.to_string()));
104 map.insert("url".to_string(), serde_json::Value::String(url.to_string()));
105 DocEntry(map)
106 }
107
108 #[must_use]
110 pub fn section(title: &str, markdown: &str) -> Self {
111 let mut map = Self::base();
112 map.insert("kind".to_string(), serde_json::Value::String("section".to_string()));
113 map.insert("title".to_string(), serde_json::Value::String(title.to_string()));
114 map.insert("markdown".to_string(), serde_json::Value::String(markdown.to_string()));
115 DocEntry(map)
116 }
117
118 #[must_use]
120 pub fn mermaid(code: &str, title: Option<&str>) -> Self {
121 let mut map = Self::base();
122 map.insert("kind".to_string(), serde_json::Value::String("mermaid".to_string()));
123 map.insert("code".to_string(), serde_json::Value::String(code.to_string()));
124 if let Some(t) = title {
125 map.insert("title".to_string(), serde_json::Value::String(t.to_string()));
126 }
127 DocEntry(map)
128 }
129
130 #[must_use]
132 pub fn screenshot(path: &str, alt: Option<&str>) -> Self {
133 let mut map = Self::base();
134 map.insert("kind".to_string(), serde_json::Value::String("screenshot".to_string()));
135 map.insert("path".to_string(), serde_json::Value::String(path.to_string()));
136 if let Some(a) = alt {
137 map.insert("alt".to_string(), serde_json::Value::String(a.to_string()));
138 }
139 DocEntry(map)
140 }
141
142 #[must_use]
144 pub fn custom(type_name: &str, data: serde_json::Value) -> Self {
145 let mut map = Self::base();
146 map.insert("kind".to_string(), serde_json::Value::String("custom".to_string()));
147 map.insert("type".to_string(), serde_json::Value::String(type_name.to_string()));
148 map.insert("data".to_string(), data);
149 DocEntry(map)
150 }
151
152 #[must_use]
154 pub fn with_children(mut self, children: Vec<DocEntry>) -> Self {
155 if !children.is_empty() {
156 let child_values: Vec<serde_json::Value> = children
157 .into_iter()
158 .map(|c| serde_json::to_value(c).unwrap())
159 .collect();
160 self.0
161 .insert("children".to_string(), serde_json::Value::Array(child_values));
162 }
163 self
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn note_serializes_correctly() {
173 let entry = DocEntry::note("hello");
174 let json = serde_json::to_value(&entry).unwrap();
175 assert_eq!(json["kind"], "note");
176 assert_eq!(json["text"], "hello");
177 assert_eq!(json["phase"], "runtime");
178 }
179
180 #[test]
181 fn tag_serializes_correctly() {
182 let entry = DocEntry::tag(&["smoke", "fast"]);
183 let json = serde_json::to_value(&entry).unwrap();
184 assert_eq!(json["kind"], "tag");
185 assert_eq!(json["names"], serde_json::json!(["smoke", "fast"]));
186 assert_eq!(json["phase"], "runtime");
187 }
188
189 #[test]
190 fn kv_serializes_correctly() {
191 let entry = DocEntry::kv("count", serde_json::json!(42));
192 let json = serde_json::to_value(&entry).unwrap();
193 assert_eq!(json["kind"], "kv");
194 assert_eq!(json["label"], "count");
195 assert_eq!(json["value"], 42);
196 }
197
198 #[test]
199 fn code_serializes_correctly() {
200 let entry = DocEntry::code("snippet", "fn main() {}", Some("rust"));
201 let json = serde_json::to_value(&entry).unwrap();
202 assert_eq!(json["kind"], "code");
203 assert_eq!(json["label"], "snippet");
204 assert_eq!(json["content"], "fn main() {}");
205 assert_eq!(json["lang"], "rust");
206 }
207
208 #[test]
209 fn json_doc_serializes_as_code_with_json_lang() {
210 let data = serde_json::json!({"key": "value"});
211 let entry = DocEntry::json_doc("payload", &data);
212 let json = serde_json::to_value(&entry).unwrap();
213 assert_eq!(json["kind"], "code");
214 assert_eq!(json["lang"], "json");
215 }
216
217 #[test]
218 fn table_serializes_correctly() {
219 let entry = DocEntry::table("results", &["name", "score"], &[&["Alice", "100"]]);
220 let json = serde_json::to_value(&entry).unwrap();
221 assert_eq!(json["kind"], "table");
222 assert_eq!(json["columns"], serde_json::json!(["name", "score"]));
223 assert_eq!(json["rows"], serde_json::json!([["Alice", "100"]]));
224 }
225
226 #[test]
227 fn link_serializes_correctly() {
228 let entry = DocEntry::link("docs", "https://example.com");
229 let json = serde_json::to_value(&entry).unwrap();
230 assert_eq!(json["kind"], "link");
231 assert_eq!(json["url"], "https://example.com");
232 }
233
234 #[test]
235 fn section_serializes_correctly() {
236 let entry = DocEntry::section("Details", "## Heading\nBody text");
237 let json = serde_json::to_value(&entry).unwrap();
238 assert_eq!(json["kind"], "section");
239 assert_eq!(json["title"], "Details");
240 assert_eq!(json["markdown"], "## Heading\nBody text");
241 }
242
243 #[test]
244 fn mermaid_serializes_correctly() {
245 let entry = DocEntry::mermaid("graph TD; A-->B;", Some("Flow"));
246 let json = serde_json::to_value(&entry).unwrap();
247 assert_eq!(json["kind"], "mermaid");
248 assert_eq!(json["code"], "graph TD; A-->B;");
249 assert_eq!(json["title"], "Flow");
250 }
251
252 #[test]
253 fn screenshot_serializes_correctly() {
254 let entry = DocEntry::screenshot("/tmp/shot.png", Some("Login page"));
255 let json = serde_json::to_value(&entry).unwrap();
256 assert_eq!(json["kind"], "screenshot");
257 assert_eq!(json["path"], "/tmp/shot.png");
258 assert_eq!(json["alt"], "Login page");
259 }
260
261 #[test]
262 fn custom_serializes_correctly() {
263 let entry = DocEntry::custom("metrics", serde_json::json!({"latency_ms": 42}));
264 let json = serde_json::to_value(&entry).unwrap();
265 assert_eq!(json["kind"], "custom");
266 assert_eq!(json["type"], "metrics");
267 assert_eq!(json["data"]["latency_ms"], 42);
268 }
269}