1#![allow(dead_code)]
21
22use std::collections::BTreeMap;
23
24use serde::Deserialize;
25use thiserror::Error;
26
27pub const TEMPLATE: &str = include_str!("../templates/omne-readme-template.md");
33
34#[derive(Debug, Clone)]
40pub struct Vars {
41 pub volume: String,
42 pub distro: String,
43 pub distro_version: String,
44 pub created: String,
45 pub kernel_source: String,
46 pub distro_source: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
54pub struct ManifestFrontmatter {
55 pub volume: String,
56 pub distro: String,
57 #[serde(rename = "distro-version")]
58 pub distro_version: String,
59 pub created: String,
60 #[serde(rename = "kernel-source")]
61 pub kernel_source: String,
62 #[serde(rename = "distro-source")]
63 pub distro_source: String,
64}
65
66#[derive(Debug, Error)]
70pub enum Error {
71 #[error(".omne/omne.md is missing its `---...---` YAML frontmatter fences")]
72 MissingFrontmatter,
73
74 #[error(".omne/omne.md frontmatter is missing required field: {field}")]
75 MissingField { field: String },
76
77 #[error(".omne/omne.md frontmatter is not valid YAML: {0}")]
78 Yaml(#[from] serde_yml::Error),
79
80 #[error("invalid source format '{value}' in .omne/omne.md — expected org/repo")]
81 InvalidSourceFormat { value: String },
82}
83
84pub fn stamp(vars: &Vars) -> String {
91 TEMPLATE
92 .replace("{{volume}}", &vars.volume)
93 .replace("{{distro}}", &vars.distro)
94 .replace("{{distro-version}}", &vars.distro_version)
95 .replace("{{created}}", &vars.created)
96 .replace("{{kernel-source}}", &vars.kernel_source)
97 .replace("{{distro-source}}", &vars.distro_source)
98}
99
100pub fn parse_frontmatter(md: &str) -> Result<ManifestFrontmatter, Error> {
109 let yaml_body = extract_frontmatter_block(md)?;
110
111 let mut map: BTreeMap<String, String> = serde_yml::from_str(&yaml_body)?;
117
118 fn take(map: &mut BTreeMap<String, String>, key: &str) -> Result<String, Error> {
119 map.remove(key).ok_or_else(|| Error::MissingField {
120 field: key.to_string(),
121 })
122 }
123
124 let volume = take(&mut map, "volume")?;
127 let distro = take(&mut map, "distro")?;
128 let distro_version = take(&mut map, "distro-version")?;
129 let created = take(&mut map, "created")?;
130 let kernel_source = take(&mut map, "kernel-source")?;
131 let distro_source = take(&mut map, "distro-source")?;
132
133 Ok(ManifestFrontmatter {
134 volume,
135 distro,
136 distro_version,
137 created,
138 kernel_source,
139 distro_source,
140 })
141}
142
143pub(crate) fn extract_frontmatter_block(md: &str) -> Result<String, Error> {
156 let mut lines = md.lines();
157
158 match lines.next() {
159 Some("---") => {}
160 _ => return Err(Error::MissingFrontmatter),
161 }
162
163 let mut body = String::new();
164 let mut closed = false;
165 for line in lines {
166 if line == "---" {
167 closed = true;
168 break;
169 }
170 body.push_str(line);
171 body.push('\n');
172 }
173
174 if !closed {
175 return Err(Error::MissingFrontmatter);
176 }
177
178 Ok(body)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn sample_vars() -> Vars {
186 Vars {
187 volume: "my-app".to_string(),
188 distro: "omne-faber".to_string(),
189 distro_version: "1.0.0".to_string(),
190 created: "2026-04-09".to_string(),
191 kernel_source: "omne-org/omne".to_string(),
192 distro_source: "omne-org/omne-faber".to_string(),
193 }
194 }
195
196 #[test]
199 fn template_contains_all_placeholders() {
200 for placeholder in [
201 "{{volume}}",
202 "{{distro}}",
203 "{{distro-version}}",
204 "{{created}}",
205 "{{kernel-source}}",
206 "{{distro-source}}",
207 ] {
208 assert!(
209 TEMPLATE.contains(placeholder),
210 "template must contain {placeholder}, template was:\n{TEMPLATE}",
211 );
212 }
213 }
214
215 #[test]
218 fn stamp_replaces_all_placeholders() {
219 let out = stamp(&sample_vars());
220 assert!(
221 !out.contains("{{"),
222 "stamped output still contains `{{{{`:\n{out}",
223 );
224 assert!(
225 !out.contains("}}"),
226 "stamped output still contains `}}}}`:\n{out}",
227 );
228 }
229
230 #[test]
231 fn stamp_contains_volume_name() {
232 let out = stamp(&sample_vars());
233 assert!(out.contains("my-app"), "missing volume name:\n{out}");
234 }
235
236 #[test]
237 fn stamp_contains_distro_name() {
238 let out = stamp(&sample_vars());
239 assert!(out.contains("omne-faber"), "missing distro name:\n{out}");
240 }
241
242 #[test]
243 fn stamp_contains_distro_version() {
244 let out = stamp(&sample_vars());
245 assert!(out.contains("1.0.0"), "missing distro version:\n{out}");
246 }
247
248 #[test]
249 fn stamp_contains_created_date() {
250 let out = stamp(&sample_vars());
251 assert!(out.contains("2026-04-09"), "missing created date:\n{out}");
252 }
253
254 #[test]
255 fn stamp_contains_kernel_source() {
256 let out = stamp(&sample_vars());
258 assert!(
259 out.contains("kernel-source: omne-org/omne"),
260 "missing `kernel-source` frontmatter line:\n{out}",
261 );
262 }
263
264 #[test]
265 fn stamp_contains_distro_source() {
266 let out = stamp(&sample_vars());
268 assert!(
269 out.contains("distro-source: omne-org/omne-faber"),
270 "missing `distro-source` frontmatter line:\n{out}",
271 );
272 }
273
274 #[test]
275 fn stamp_output_starts_with_yaml_fence() {
276 let out = stamp(&sample_vars());
277 assert!(
278 out.starts_with("---\n") || out.starts_with("---\r\n"),
279 "stamped output should begin with `---` fence, got: {:?}",
280 &out.chars().take(10).collect::<String>(),
281 );
282 }
283
284 #[test]
287 fn parse_frontmatter_round_trips_with_stamp() {
288 let vars = sample_vars();
289 let stamped = stamp(&vars);
290 let parsed =
291 parse_frontmatter(&stamped).expect("parsing a freshly-stamped manifest should succeed");
292
293 assert_eq!(parsed.volume, vars.volume);
294 assert_eq!(parsed.distro, vars.distro);
295 assert_eq!(parsed.distro_version, vars.distro_version);
296 assert_eq!(parsed.created, vars.created);
297 assert_eq!(parsed.kernel_source, vars.kernel_source);
298 assert_eq!(parsed.distro_source, vars.distro_source);
299 }
300
301 #[test]
302 fn parse_frontmatter_errors_on_no_fences() {
303 let md = "# MANIFEST\n\nNo frontmatter here.\n";
304 match parse_frontmatter(md) {
305 Err(Error::MissingFrontmatter) => {}
306 other => panic!("expected MissingFrontmatter, got {other:?}"),
307 }
308 }
309
310 #[test]
311 fn parse_frontmatter_errors_on_unclosed_fence() {
312 let md = "---\nvolume: x\ndistro: y\n\n# body without closing fence\n";
313 match parse_frontmatter(md) {
314 Err(Error::MissingFrontmatter) => {}
315 other => panic!("expected MissingFrontmatter on unclosed fence, got {other:?}"),
316 }
317 }
318
319 #[test]
320 fn parse_frontmatter_errors_on_missing_kernel_source() {
321 let md = "---\n\
325 volume: my-app\n\
326 distro: omne-faber\n\
327 distro-version: 1.0.0\n\
328 created: 2026-04-09\n\
329 distro-source: omne-org/omne-faber\n\
330 ---\n\
331 \n\
332 # body\n";
333 match parse_frontmatter(md) {
334 Err(Error::MissingField { field }) => {
335 assert_eq!(field, "kernel-source");
336 }
337 other => panic!("expected MissingField kernel-source, got {other:?}"),
338 }
339 }
340
341 #[test]
342 fn parse_frontmatter_errors_on_missing_distro_source() {
343 let md = "---\n\
344 volume: my-app\n\
345 distro: omne-faber\n\
346 distro-version: 1.0.0\n\
347 created: 2026-04-09\n\
348 kernel-source: omne-org/omne\n\
349 ---\n\
350 \n\
351 # body\n";
352 match parse_frontmatter(md) {
353 Err(Error::MissingField { field }) => {
354 assert_eq!(field, "distro-source");
355 }
356 other => panic!("expected MissingField distro-source, got {other:?}"),
357 }
358 }
359}