1use std::path::Path;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum CommentStyle {
6 Rust,
8 TypeScript,
10}
11
12impl CommentStyle {
13 pub fn detect(root: &Path) -> Option<Self> {
15 if root.join("Cargo.toml").exists() {
16 Some(Self::Rust)
17 } else if root.join("package.json").exists() {
18 Some(Self::TypeScript)
19 } else {
20 None
21 }
22 }
23
24 pub fn from_lang(lang: &str) -> Option<Self> {
25 match lang.to_lowercase().as_str() {
26 "rust" | "rs" => Some(Self::Rust),
27 "typescript" | "ts" | "javascript" | "js" => Some(Self::TypeScript),
28 _ => None,
29 }
30 }
31}
32
33pub fn generate_template(style: CommentStyle) -> String {
38 let sections = vec![
39 Section::heading("@c4 container"),
40 Section::heading("[Project Name]").h1(),
41 Section::blank(),
42 Section::line("[TODO: One-line description — what this system does and why it exists.]"),
43 Section::blank(),
44 Section::heading("C4 Context").h2(),
45 Section::blank(),
46 Section::code_block(
47 "mermaid",
48 &[
49 "C4Context",
50 " title System Context Diagram",
51 "",
52 " Person(user, \"TODO: User\", \"TODO: Primary user/actor\")",
53 " System(system, \"TODO: System Name\", \"TODO: System purpose\")",
54 " System_Ext(ext1, \"TODO: External System\", \"TODO: External dependency\")",
55 "",
56 " Rel(user, system, \"Uses\")",
57 " Rel(system, ext1, \"TODO: relationship\", \"TODO: protocol\")",
58 "",
59 " UpdateLayoutConfig($c4ShapeInRow=\"3\", $c4BoundaryInRow=\"1\")",
60 ],
61 ),
62 Section::blank(),
63 Section::heading("Data Flow").h2(),
64 Section::blank(),
65 Section::line("1. TODO: Primary command/request flow (e.g., Frontend -> API -> Service -> DB)"),
66 Section::line("2. TODO: Primary data/response flow (e.g., DB -> Service -> Frontend)"),
67 Section::line("3. TODO: Secondary flows (settings, config, async jobs, etc.)"),
68 Section::blank(),
69 Section::heading("Concurrency & Data Patterns").h2(),
70 Section::blank(),
71 Section::line("- TODO: Key concurrency primitives (locks, channels, atomics, async, etc.)"),
72 Section::line("- TODO: Data access patterns (caching, buffering, connection pooling, etc.)"),
73 Section::blank(),
74 Section::heading("Deployment").h2(),
75 Section::blank(),
76 Section::line("- TODO: Where does this run? (local, cloud, hybrid, embedded)"),
77 Section::line("- TODO: Key infrastructure (Docker, K8s, serverless, etc.)"),
78 Section::blank(),
79 Section::heading("External Dependencies").h2(),
80 Section::blank(),
81 Section::line("- TODO: Third-party APIs and services"),
82 Section::line("- TODO: Databases and storage systems"),
83 ];
84
85 render(style, §ions)
86}
87
88enum Section {
91 Line(String),
92 Blank,
93 Heading { text: String, level: u8 },
94 CodeBlock { lang: String, lines: Vec<String> },
95}
96
97impl Section {
98 fn heading(text: &str) -> Self {
99 Self::Heading {
100 text: text.to_string(),
101 level: 0,
102 }
103 }
104
105 fn h1(self) -> Self {
106 match self {
107 Self::Heading { text, .. } => Self::Heading { text, level: 1 },
108 other => other,
109 }
110 }
111
112 fn h2(self) -> Self {
113 match self {
114 Self::Heading { text, .. } => Self::Heading { text, level: 2 },
115 other => other,
116 }
117 }
118
119 fn line(text: &str) -> Self {
120 Self::Line(text.to_string())
121 }
122
123 fn blank() -> Self {
124 Self::Blank
125 }
126
127 fn code_block(lang: &str, lines: &[&str]) -> Self {
128 Self::CodeBlock {
129 lang: lang.to_string(),
130 lines: lines.iter().map(|s| s.to_string()).collect(),
131 }
132 }
133}
134
135fn render(style: CommentStyle, sections: &[Section]) -> String {
136 let mut out = String::new();
137
138 for section in sections {
139 match section {
140 Section::Blank => {
141 out.push_str(&comment_line(style, ""));
142 out.push('\n');
143 }
144 Section::Line(text) => {
145 out.push_str(&comment_line(style, text));
146 out.push('\n');
147 }
148 Section::Heading { text, level } => {
149 let prefix = match level {
150 1 => "# ",
151 2 => "## ",
152 3 => "### ",
153 _ => "",
154 };
155 out.push_str(&comment_line(style, &format!("{}{}", prefix, text)));
156 out.push('\n');
157 }
158 Section::CodeBlock { lang, lines } => {
159 out.push_str(&comment_line(style, &format!("```{}", lang)));
160 out.push('\n');
161 for line in lines {
162 out.push_str(&comment_line(style, line));
163 out.push('\n');
164 }
165 out.push_str(&comment_line(style, "```"));
166 out.push('\n');
167 }
168 }
169 }
170
171 out
172}
173
174fn comment_line(style: CommentStyle, text: &str) -> String {
175 match style {
176 CommentStyle::Rust => {
177 if text.is_empty() {
178 "//!".to_string()
179 } else {
180 format!("//! {}", text)
181 }
182 }
183 CommentStyle::TypeScript => {
184 if text.is_empty() {
185 " *".to_string()
186 } else {
187 format!(" * {}", text)
188 }
189 }
190 }
191}
192
193pub fn wrap_jsdoc(content: &str) -> String {
195 format!("/**\n{} */\n", content)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn rust_template_has_c4_marker() {
204 let out = generate_template(CommentStyle::Rust);
205 assert!(out.contains("//! @c4 container"));
206 }
207
208 #[test]
209 fn rust_template_has_all_sections() {
210 let out = generate_template(CommentStyle::Rust);
211 assert!(out.contains("# [Project Name]"));
212 assert!(out.contains("## C4 Context"));
213 assert!(out.contains("## Data Flow"));
214 assert!(out.contains("## Concurrency & Data Patterns"));
215 assert!(out.contains("## Deployment"));
216 assert!(out.contains("## External Dependencies"));
217 }
218
219 #[test]
220 fn rust_template_has_mermaid_block() {
221 let out = generate_template(CommentStyle::Rust);
222 assert!(out.contains("```mermaid"));
223 assert!(out.contains("C4Context"));
224 assert!(out.contains("```"));
225 }
226
227 #[test]
228 fn rust_template_has_todo_placeholders() {
229 let out = generate_template(CommentStyle::Rust);
230 assert!(out.contains("TODO:"));
231 assert!(out.contains("[TODO: One-line description"));
232 }
233
234 #[test]
235 fn typescript_template_uses_jsdoc_style() {
236 let out = generate_template(CommentStyle::TypeScript);
237 assert!(out.contains(" * @c4 container"));
238 assert!(out.contains(" * ## Data Flow"));
239 }
240
241 #[test]
242 fn detect_rust_from_cargo_toml() {
243 let tmp = tempfile::TempDir::new().unwrap();
244 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
245 assert_eq!(CommentStyle::detect(tmp.path()), Some(CommentStyle::Rust));
246 }
247
248 #[test]
249 fn detect_ts_from_package_json() {
250 let tmp = tempfile::TempDir::new().unwrap();
251 std::fs::write(tmp.path().join("package.json"), "").unwrap();
252 assert_eq!(
253 CommentStyle::detect(tmp.path()),
254 Some(CommentStyle::TypeScript)
255 );
256 }
257
258 #[test]
259 fn from_lang_parsing() {
260 assert_eq!(CommentStyle::from_lang("rust"), Some(CommentStyle::Rust));
261 assert_eq!(CommentStyle::from_lang("rs"), Some(CommentStyle::Rust));
262 assert_eq!(CommentStyle::from_lang("ts"), Some(CommentStyle::TypeScript));
263 assert_eq!(CommentStyle::from_lang("unknown"), None);
264 }
265
266 #[test]
267 fn blank_lines_are_comment_only() {
268 let out = generate_template(CommentStyle::Rust);
269 assert!(out.contains("\n//!\n"));
271 assert!(!out.contains("//! \n"));
272 }
273}