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