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    ) -> crate::Result<OutputStyle> {
33        let source = SourceType::from_str_opt(fm.source_type.as_deref());
34
35        let style = OutputStyle::new(fm.name, fm.description, body)
36            .with_source_type(source)
37            .with_keep_coding_instructions(fm.keep_coding_instructions);
38
39        Ok(style)
40    }
41}
42
43impl DocumentLoader<OutputStyle> for OutputStyleLoader {
44    fn parse_content(&self, content: &str, path: Option<&Path>) -> crate::Result<OutputStyle> {
45        let doc = parse_frontmatter::<OutputStyleFrontmatter>(content)?;
46        self.build_style(doc.frontmatter, doc.body, path)
47    }
48
49    fn doc_type_name(&self) -> &'static str {
50        "output style"
51    }
52
53    fn file_filter(&self) -> fn(&Path) -> bool {
54        is_markdown
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::output_style::OutputStyleSourceType;
62
63    #[test]
64    fn test_parse_output_style_with_frontmatter() {
65        let content = r#"---
66name: test-style
67description: A test output style
68keep-coding-instructions: true
69---
70
71# Custom Instructions
72
73This is the custom prompt content.
74"#;
75
76        let loader = OutputStyleLoader::new();
77        let style = loader.parse_content(content, None).unwrap();
78
79        assert_eq!(style.name, "test-style");
80        assert_eq!(style.description, "A test output style");
81        assert!(style.keep_coding_instructions);
82        assert!(style.prompt.contains("Custom Instructions"));
83    }
84
85    #[test]
86    fn test_parse_output_style_without_keep_coding() {
87        let content = r#"---
88name: concise
89description: Be concise
90---
91
92Be brief and to the point.
93"#;
94
95        let loader = OutputStyleLoader::new();
96        let style = loader.parse_content(content, None).unwrap();
97
98        assert_eq!(style.name, "concise");
99        assert!(!style.keep_coding_instructions);
100    }
101
102    #[test]
103    fn test_parse_output_style_without_frontmatter() {
104        let content = "Just some content without frontmatter";
105        let loader = OutputStyleLoader::new();
106        let result = loader.parse_content(content, None);
107
108        assert!(result.is_err());
109    }
110
111    #[test]
112    fn test_parse_output_style_with_source_type() {
113        let content = r#"---
114name: builtin-style
115description: A builtin style
116source_type: builtin
117---
118
119Content here.
120"#;
121
122        let loader = OutputStyleLoader::new();
123        let style = loader.parse_content(content, None).unwrap();
124
125        assert_eq!(style.source_type, OutputStyleSourceType::Builtin);
126    }
127}