claude_agent/output_style/
loader.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use super::OutputStyle;
6use crate::common::{DocumentLoader, SourceType, is_markdown, parse_frontmatter};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct OutputStyleFrontmatter {
10    pub name: String,
11    #[serde(default)]
12    pub description: String,
13    #[serde(default, rename = "keep-coding-instructions")]
14    pub keep_coding_instructions: bool,
15    #[serde(default)]
16    pub source_type: Option<String>,
17}
18
19#[derive(Debug, Clone, Copy, Default)]
20pub struct OutputStyleLoader;
21
22impl OutputStyleLoader {
23    pub fn new() -> Self {
24        Self
25    }
26
27    fn build_style(
28        &self,
29        fm: OutputStyleFrontmatter,
30        body: String,
31        _path: Option<&Path>,
32    ) -> OutputStyle {
33        let source = SourceType::from_str_opt(fm.source_type.as_deref());
34
35        OutputStyle::new(fm.name, fm.description, body)
36            .with_source_type(source)
37            .with_keep_coding_instructions(fm.keep_coding_instructions)
38    }
39}
40
41impl DocumentLoader<OutputStyle> for OutputStyleLoader {
42    fn parse_content(&self, content: &str, path: Option<&Path>) -> crate::Result<OutputStyle> {
43        let doc = parse_frontmatter::<OutputStyleFrontmatter>(content)?;
44        Ok(self.build_style(doc.frontmatter, doc.body, path))
45    }
46
47    fn doc_type_name(&self) -> &'static str {
48        "output style"
49    }
50
51    fn file_filter(&self) -> fn(&Path) -> bool {
52        is_markdown
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::output_style::OutputStyleSourceType;
60
61    #[test]
62    fn test_parse_output_style_with_frontmatter() {
63        let content = r#"---
64name: test-style
65description: A test output style
66keep-coding-instructions: true
67---
68
69# Custom Instructions
70
71This is the custom prompt content.
72"#;
73
74        let loader = OutputStyleLoader::new();
75        let style = loader.parse_content(content, None).unwrap();
76
77        assert_eq!(style.name, "test-style");
78        assert_eq!(style.description, "A test output style");
79        assert!(style.keep_coding_instructions);
80        assert!(style.prompt.contains("Custom Instructions"));
81    }
82
83    #[test]
84    fn test_parse_output_style_without_keep_coding() {
85        let content = r#"---
86name: concise
87description: Be concise
88---
89
90Be brief and to the point.
91"#;
92
93        let loader = OutputStyleLoader::new();
94        let style = loader.parse_content(content, None).unwrap();
95
96        assert_eq!(style.name, "concise");
97        assert!(!style.keep_coding_instructions);
98    }
99
100    #[test]
101    fn test_parse_output_style_without_frontmatter() {
102        let content = "Just some content without frontmatter";
103        let loader = OutputStyleLoader::new();
104        let result = loader.parse_content(content, None);
105
106        assert!(result.is_err());
107    }
108
109    #[test]
110    fn test_parse_output_style_with_source_type() {
111        let content = r#"---
112name: builtin-style
113description: A builtin style
114source_type: builtin
115---
116
117Content here.
118"#;
119
120        let loader = OutputStyleLoader::new();
121        let style = loader.parse_content(content, None).unwrap();
122
123        assert_eq!(style.source_type, OutputStyleSourceType::Builtin);
124    }
125}