Skip to main content

release_kit/commands/
read.rs

1//! The read-only payload commands: method, binding, snippet.
2
3use include_dir::Dir;
4
5use crate::cli::read::ReadArgs;
6use crate::commands::walk;
7use crate::embedded;
8use crate::error::RkError;
9use crate::output::Output;
10
11/// Print a method chapter, or list the chapters.
12///
13/// # Errors
14///
15/// Returns [`RkError::NotFound`] for an unknown chapter and
16/// [`RkError::Usage`] when neither a name nor `--list` is given.
17pub fn method(args: &ReadArgs) -> Result<(), RkError> {
18    flat(&embedded::METHOD, "chapter", args)
19}
20
21/// Print a technology binding, or list the bindings.
22///
23/// # Errors
24///
25/// Returns [`RkError::NotFound`] for an unknown binding and
26/// [`RkError::Usage`] when neither a name nor `--list` is given.
27pub fn binding(args: &ReadArgs) -> Result<(), RkError> {
28    flat(&embedded::BINDINGS, "binding", args)
29}
30
31/// Print one landable file, or list them all as `<tech>/<path>`.
32///
33/// # Errors
34///
35/// Returns [`RkError::NotFound`] for an unknown path and
36/// [`RkError::Usage`] when neither a name nor `--list` is given.
37pub fn snippet(args: &ReadArgs) -> Result<(), RkError> {
38    let out = Output::human();
39    let entries = walk(&embedded::SNIPPETS);
40    if args.list {
41        for (path, _) in &entries {
42            out.result_line(path);
43        }
44        return Ok(());
45    }
46    let Some(name) = args.name.as_deref() else {
47        return Err(RkError::Usage(
48            "name a snippet path, or pass --list to see them".into(),
49        ));
50    };
51    entries.iter().find(|(path, _)| path == name).map_or_else(
52        || {
53            Err(RkError::NotFound {
54                kind: "snippet",
55                name: name.to_owned(),
56            })
57        },
58        |(_, contents)| {
59            print_bytes(out, contents);
60            Ok(())
61        },
62    )
63}
64
65/// Resolve a name against the markdown files of one flat embedded root.
66///
67/// A chapter file `NN-name.md` answers to `name`, `NN-name`, and
68/// `NN-name.md`; `README.md` answers to `readme`.
69fn flat(dir: &Dir<'static>, kind: &'static str, args: &ReadArgs) -> Result<(), RkError> {
70    let out = Output::human();
71    let entries = walk(dir);
72    if args.list {
73        for (path, _) in &entries {
74            out.result_line(short_key(path));
75        }
76        return Ok(());
77    }
78    let Some(name) = args.name.as_deref() else {
79        return Err(RkError::Usage(format!(
80            "name a {kind}, or pass --list to see them"
81        )));
82    };
83    let wanted = name.to_ascii_lowercase();
84    let wanted = wanted.trim_end_matches(".md");
85    for (path, contents) in &entries {
86        let stem = path.trim_end_matches(".md");
87        if stem.eq_ignore_ascii_case(wanted) || short_key(path) == wanted {
88            print_bytes(out, contents);
89            return Ok(());
90        }
91    }
92    Err(RkError::NotFound {
93        kind,
94        name: name.to_owned(),
95    })
96}
97
98/// The listing key for a payload file: the stem, lowercased, with a
99/// leading `NN-` chapter prefix stripped.
100fn short_key(path: &str) -> String {
101    let stem = path.trim_end_matches(".md");
102    let stem = stem
103        .split_once('-')
104        .filter(|(prefix, _)| prefix.chars().all(|c| c.is_ascii_digit()))
105        .map_or(stem, |(_, rest)| rest);
106    stem.to_ascii_lowercase()
107}
108
109/// Print payload bytes as-is; every authored payload file is UTF-8.
110fn print_bytes(out: Output, contents: &[u8]) {
111    out.result_raw(&String::from_utf8_lossy(contents));
112}