Skip to main content

subcog/cli/
namespaces.rs

1//! CLI command for listing memory namespaces.
2
3use crate::models::Namespace;
4use serde::Serialize;
5use std::io::{self, Write};
6use std::str::FromStr;
7
8/// Information about a namespace.
9#[derive(Debug, Clone, Serialize)]
10pub struct NamespaceInfo {
11    /// Namespace identifier.
12    pub namespace: String,
13    /// Description of the namespace.
14    pub description: String,
15    /// Signal words that trigger this namespace.
16    pub signal_words: Vec<String>,
17}
18
19impl NamespaceInfo {
20    /// Creates a new namespace info.
21    fn new(namespace: Namespace, description: &str, signal_words: &[&str]) -> Self {
22        Self {
23            namespace: namespace.to_string(),
24            description: description.to_string(),
25            signal_words: signal_words.iter().map(|s| (*s).to_string()).collect(),
26        }
27    }
28}
29
30/// Returns all namespace information.
31#[must_use]
32pub fn get_all_namespaces() -> Vec<NamespaceInfo> {
33    vec![
34        NamespaceInfo::new(
35            Namespace::Decisions,
36            "Architectural and design decisions",
37            &["decided", "chose", "going with"],
38        ),
39        NamespaceInfo::new(
40            Namespace::Patterns,
41            "Discovered patterns and conventions",
42            &["always", "never", "convention"],
43        ),
44        NamespaceInfo::new(
45            Namespace::Learnings,
46            "Lessons learned from debugging",
47            &["TIL", "learned", "discovered"],
48        ),
49        NamespaceInfo::new(
50            Namespace::Context,
51            "Important background information",
52            &["because", "constraint", "requirement"],
53        ),
54        NamespaceInfo::new(
55            Namespace::TechDebt,
56            "Technical debt tracking",
57            &["TODO", "FIXME", "temporary", "hack"],
58        ),
59        NamespaceInfo::new(
60            Namespace::Blockers,
61            "Blockers and impediments",
62            &["blocked", "waiting", "depends on"],
63        ),
64        NamespaceInfo::new(
65            Namespace::Progress,
66            "Work progress and milestones",
67            &["completed", "milestone", "shipped"],
68        ),
69        NamespaceInfo::new(
70            Namespace::Apis,
71            "API documentation and contracts",
72            &["endpoint", "request", "response"],
73        ),
74        NamespaceInfo::new(
75            Namespace::Config,
76            "Configuration details",
77            &["environment", "setting", "variable"],
78        ),
79        NamespaceInfo::new(
80            Namespace::Security,
81            "Security findings and notes",
82            &["vulnerability", "CVE", "auth"],
83        ),
84        NamespaceInfo::new(
85            Namespace::Testing,
86            "Test strategies and edge cases",
87            &["test", "edge case", "coverage"],
88        ),
89    ]
90}
91
92/// Output format for namespaces command.
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94pub enum NamespacesOutputFormat {
95    /// Table format (default).
96    #[default]
97    Table,
98    /// JSON format.
99    Json,
100    /// YAML format.
101    Yaml,
102}
103
104impl FromStr for NamespacesOutputFormat {
105    type Err = std::convert::Infallible;
106
107    fn from_str(s: &str) -> Result<Self, Self::Err> {
108        Ok(match s.to_lowercase().as_str() {
109            "json" => Self::Json,
110            "yaml" => Self::Yaml,
111            _ => Self::Table,
112        })
113    }
114}
115
116/// Writes namespaces as a table to the given writer.
117///
118/// # Errors
119///
120/// Returns an error if writing fails.
121pub fn write_table<W: Write>(
122    writer: &mut W,
123    namespaces: &[NamespaceInfo],
124    verbose: bool,
125) -> io::Result<()> {
126    if verbose {
127        writeln!(
128            writer,
129            "{:<14}{:<38}SIGNAL WORDS",
130            "NAMESPACE", "DESCRIPTION"
131        )?;
132        for ns in namespaces {
133            writeln!(
134                writer,
135                "{:<14}{:<38}{}",
136                ns.namespace,
137                ns.description,
138                ns.signal_words.join(", ")
139            )?;
140        }
141    } else {
142        writeln!(writer, "{:<14}DESCRIPTION", "NAMESPACE")?;
143        for ns in namespaces {
144            writeln!(writer, "{:<14}{}", ns.namespace, ns.description)?;
145        }
146    }
147    Ok(())
148}
149
150/// Writes namespaces as JSON to the given writer.
151///
152/// # Errors
153///
154/// Returns an error if serialization or writing fails.
155pub fn write_json<W: Write>(
156    writer: &mut W,
157    namespaces: &[NamespaceInfo],
158) -> Result<(), Box<dyn std::error::Error>> {
159    let json = serde_json::to_string_pretty(namespaces)?;
160    writeln!(writer, "{json}")?;
161    Ok(())
162}
163
164/// Writes namespaces as YAML to the given writer.
165///
166/// # Errors
167///
168/// Returns an error if serialization or writing fails.
169pub fn write_yaml<W: Write>(
170    writer: &mut W,
171    namespaces: &[NamespaceInfo],
172) -> Result<(), Box<dyn std::error::Error>> {
173    let yaml = serde_yaml_ng::to_string(namespaces)?;
174    write!(writer, "{yaml}")?;
175    Ok(())
176}
177
178/// Executes the namespaces command.
179///
180/// # Errors
181///
182/// Returns an error if serialization or output fails.
183pub fn cmd_namespaces(
184    format: NamespacesOutputFormat,
185    verbose: bool,
186) -> Result<(), Box<dyn std::error::Error>> {
187    let namespaces = get_all_namespaces();
188    let stdout = io::stdout();
189    let mut handle = stdout.lock();
190
191    match format {
192        NamespacesOutputFormat::Table => {
193            write_table(&mut handle, &namespaces, verbose)?;
194            Ok(())
195        },
196        NamespacesOutputFormat::Json => write_json(&mut handle, &namespaces),
197        NamespacesOutputFormat::Yaml => write_yaml(&mut handle, &namespaces),
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_get_all_namespaces() {
207        let namespaces = get_all_namespaces();
208        assert_eq!(namespaces.len(), 11);
209
210        // Verify first namespace
211        assert_eq!(namespaces[0].namespace, "decisions");
212        assert_eq!(
213            namespaces[0].description,
214            "Architectural and design decisions"
215        );
216        assert!(!namespaces[0].signal_words.is_empty());
217    }
218
219    #[test]
220    fn test_output_format_from_str() {
221        assert_eq!(
222            NamespacesOutputFormat::from_str("json").unwrap(),
223            NamespacesOutputFormat::Json
224        );
225        assert_eq!(
226            NamespacesOutputFormat::from_str("JSON").unwrap(),
227            NamespacesOutputFormat::Json
228        );
229        assert_eq!(
230            NamespacesOutputFormat::from_str("yaml").unwrap(),
231            NamespacesOutputFormat::Yaml
232        );
233        assert_eq!(
234            NamespacesOutputFormat::from_str("table").unwrap(),
235            NamespacesOutputFormat::Table
236        );
237        assert_eq!(
238            NamespacesOutputFormat::from_str("invalid").unwrap(),
239            NamespacesOutputFormat::Table
240        );
241    }
242
243    #[test]
244    fn test_all_namespaces_have_signal_words() {
245        let namespaces = get_all_namespaces();
246        for ns in namespaces {
247            assert!(
248                !ns.signal_words.is_empty(),
249                "Namespace {} should have signal words",
250                ns.namespace
251            );
252        }
253    }
254
255    #[test]
256    fn test_namespace_descriptions_not_empty() {
257        let namespaces = get_all_namespaces();
258        for ns in namespaces {
259            assert!(
260                !ns.description.is_empty(),
261                "Namespace {} should have a description",
262                ns.namespace
263            );
264        }
265    }
266
267    #[test]
268    fn test_write_table_simple() {
269        let namespaces = get_all_namespaces();
270        let mut buffer = Vec::new();
271        write_table(&mut buffer, &namespaces, false).unwrap();
272        let output = String::from_utf8(buffer).unwrap();
273        assert!(output.contains("NAMESPACE"));
274        assert!(output.contains("DESCRIPTION"));
275        assert!(output.contains("decisions"));
276    }
277
278    #[test]
279    fn test_write_table_verbose() {
280        let namespaces = get_all_namespaces();
281        let mut buffer = Vec::new();
282        write_table(&mut buffer, &namespaces, true).unwrap();
283        let output = String::from_utf8(buffer).unwrap();
284        assert!(output.contains("SIGNAL WORDS"));
285        assert!(output.contains("decided, chose, going with"));
286    }
287
288    #[test]
289    fn test_write_json() {
290        let namespaces = get_all_namespaces();
291        let mut buffer = Vec::new();
292        write_json(&mut buffer, &namespaces).unwrap();
293        let output = String::from_utf8(buffer).unwrap();
294        assert!(output.contains("\"namespace\""));
295        assert!(output.contains("\"decisions\""));
296    }
297
298    #[test]
299    fn test_write_yaml() {
300        let namespaces = get_all_namespaces();
301        let mut buffer = Vec::new();
302        write_yaml(&mut buffer, &namespaces).unwrap();
303        let output = String::from_utf8(buffer).unwrap();
304        assert!(output.contains("namespace:"));
305        assert!(output.contains("decisions"));
306    }
307}