Skip to main content

spec_driven_docs/services/
reader.rs

1//! Embedded document shelves: list and fetch by short name.
2//!
3//! A shelf is one embedded directory whose files a reader addresses by stem
4//! — optionally with a kind prefix stripped, so `sdd spec distribution`
5//! finds `SPEC-distribution.md`. What the shelves contain is `embedded`'s
6//! business.
7
8use include_dir::Dir;
9
10use crate::domain::docs_catalog::ShelfId;
11
12/// One addressable shelf of embedded documents.
13#[derive(Debug, Clone, Copy)]
14pub struct Shelf {
15    /// Which shelf this is, as the catalog names it.
16    pub id: ShelfId,
17    /// The embedded directory.
18    pub dir: &'static Dir<'static>,
19    /// A kind prefix hidden from short names, e.g. `SPEC-`.
20    pub strip: Option<&'static str>,
21}
22
23/// The method chapters and glossary.
24pub const METHOD: Shelf = Shelf {
25    id: ShelfId::Method,
26    dir: &crate::embedded::METHOD,
27    strip: None,
28};
29/// The spec documents, addressed without their `SPEC-` prefix.
30pub const SPECS: Shelf = Shelf {
31    id: ShelfId::Spec,
32    dir: &crate::embedded::SPECS,
33    strip: Some("SPEC-"),
34};
35/// The templates, addressed without their `TEMPLATE-` prefix.
36pub const TEMPLATES: Shelf = Shelf {
37    id: ShelfId::Template,
38    dir: &crate::embedded::TEMPLATES,
39    strip: Some("TEMPLATE-"),
40};
41
42fn short_name<'a>(shelf: &Shelf, file_name: &'a str) -> Option<&'a str> {
43    let stem = file_name.strip_suffix(".md")?;
44    shelf
45        .strip
46        .map_or(Some(stem), |prefix| stem.strip_prefix(prefix))
47}
48
49/// Every short name the shelf offers, sorted.
50#[must_use]
51pub fn list(shelf: &Shelf) -> Vec<String> {
52    let mut names: Vec<String> = shelf
53        .dir
54        .files()
55        .filter_map(|file| {
56            let name = file.path().as_os_str().to_str()?;
57            short_name(shelf, name).map(String::from)
58        })
59        .collect();
60    names.sort();
61    names
62}
63
64/// Fetch one document by short name, full stem, or file name.
65#[must_use]
66pub fn get(shelf: &Shelf, name: &str) -> Option<&'static str> {
67    let wanted = name.strip_suffix(".md").unwrap_or(name);
68    shelf.dir.files().find_map(|file| {
69        let file_name = file.path().as_os_str().to_str()?;
70        let stem = file_name.strip_suffix(".md")?;
71        let matches = stem == wanted || short_name(shelf, file_name) == Some(wanted);
72        if matches { file.contents_utf8() } else { None }
73    })
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn shelves_list_their_short_names() {
82        assert!(list(&METHOD).contains(&"glossary".to_string()));
83        assert!(list(&SPECS).contains(&"distribution".to_string()));
84        assert!(list(&TEMPLATES).contains(&"adr".to_string()));
85    }
86
87    #[test]
88    fn documents_resolve_by_short_and_full_names() {
89        let by_short = get(&SPECS, "distribution").unwrap();
90        assert_eq!(get(&SPECS, "SPEC-distribution").unwrap(), by_short);
91        assert_eq!(get(&SPECS, "SPEC-distribution.md").unwrap(), by_short);
92        assert!(by_short.contains("distribution:instances-operate-offline"));
93        assert!(get(&SPECS, "nonexistent").is_none());
94    }
95}