claude_agent/output_style/
loader.rs1use 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}