1use crate::error::AqlError;
8use serde::Serialize;
9use std::path::Path;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13#[non_exhaustive]
14pub enum Stack {
15 Express,
17 React,
19 Node,
21 Go,
23 Rust,
25}
26
27#[derive(Debug, Clone, Serialize)]
29pub struct DetectedStacks {
30 pub stacks: Vec<Stack>,
32 pub has_tests: bool,
34}
35
36pub fn detect_stacks(root: &Path) -> DetectedStacks {
41 let mut stacks = Vec::new();
42 let mut has_tests = false;
43
44 if let Ok(content) = std::fs::read_to_string(root.join("package.json")) {
46 if let Ok(pkg) = serde_json::from_str::<serde_json::Value>(&content) {
47 let all_deps = collect_npm_deps(&pkg);
48
49 if all_deps.contains(&"express") {
50 stacks.push(Stack::Express);
51 }
52 if all_deps.contains(&"react")
53 || all_deps.contains(&"react-dom")
54 || all_deps.contains(&"next")
55 {
56 stacks.push(Stack::React);
57 }
58 if stacks.is_empty() {
59 stacks.push(Stack::Node);
60 }
61 if all_deps.contains(&"jest")
62 || all_deps.contains(&"vitest")
63 || all_deps.contains(&"mocha")
64 {
65 has_tests = true;
66 }
67 }
68 }
69
70 if root.join("go.mod").exists() {
72 stacks.push(Stack::Go);
73 }
74
75 if root.join("Cargo.toml").exists() {
77 stacks.push(Stack::Rust);
78 }
79
80 DetectedStacks { stacks, has_tests }
81}
82
83pub fn generate_schema(detected: &DetectedStacks) -> String {
85 let mut lines = Vec::new();
86 lines.push(r#"<schema version="1.0">"#.to_string());
87
88 for stack in &detected.stacks {
89 match stack {
90 Stack::Express => {
91 lines.push(r#" <define tag="route" description="HTTP route handler">"#.into());
92 lines.push(
93 r#" <attr name="method" type="enum" values="GET,POST,PUT,DELETE,PATCH" required />"#.into(),
94 );
95 lines.push(r#" <attr name="path" type="string" required />"#.into());
96 lines.push(
97 r#" <attr name="auth" type="enum" values="required,optional,none" />"#
98 .into(),
99 );
100 lines.push(r#" </define>"#.into());
101 lines
102 .push(r#" <define tag="middleware" description="Express middleware">"#.into());
103 lines.push(
104 r#" <attr name="scope" type="enum" values="global,router,route" />"#.into(),
105 );
106 lines.push(r#" </define>"#.into());
107 lines.push(
108 r#" <extractor name="express" run="aql extract express" globs="**/*.ts,**/*.js" />"#
109 .into(),
110 );
111 }
112 Stack::React => {
113 lines.push(r#" <define tag="component" description="React component">"#.into());
114 lines.push(r#" <attr name="name" type="string" />"#.into());
115 lines.push(r#" </define>"#.into());
116 lines.push(r#" <define tag="hook" description="React hook">"#.into());
117 lines.push(r#" <attr name="name" type="string" />"#.into());
118 lines.push(r#" <attr name="custom" type="boolean" />"#.into());
119 lines.push(r#" </define>"#.into());
120 lines.push(
121 r#" <extractor name="react" run="aql extract react" globs="**/*.tsx,**/*.jsx" />"#
122 .into(),
123 );
124 }
125 Stack::Node => {
126 }
128 Stack::Go => {
129 lines
130 .push(r#" <define tag="handler" description="HTTP handler function">"#.into());
131 lines.push(
132 r#" <attr name="method" type="enum" values="GET,POST,PUT,DELETE,PATCH" />"#
133 .into(),
134 );
135 lines.push(r#" <attr name="path" type="string" />"#.into());
136 lines.push(r#" </define>"#.into());
137 lines.push(
138 r#" <extractor name="go_http" run="aql extract go_http" globs="**/*.go" />"#
139 .into(),
140 );
141 }
142 Stack::Rust => {
143 }
145 }
146 }
147
148 if detected.has_tests {
149 lines.push(r#" <define tag="describe" description="Test suite">"#.into());
150 lines.push(r#" <attr name="name" type="string" />"#.into());
151 lines.push(r#" </define>"#.into());
152 lines.push(r#" <define tag="test" description="Test case">"#.into());
153 lines.push(r#" <attr name="name" type="string" />"#.into());
154 lines.push(r#" <attr name="skip" type="boolean" />"#.into());
155 lines.push(r#" </define>"#.into());
156 lines.push(
157 r#" <extractor name="test" run="aql extract test" globs="**/*.test.ts,**/*.spec.ts,**/*.test.js,**/*.spec.js" />"#
158 .into(),
159 );
160 }
161
162 lines.push("</schema>".to_string());
163 lines.join("\n")
164}
165
166#[cfg(feature = "fs")]
171pub fn init_project(root: &Path) -> Result<String, AqlError> {
172 let config_dir = root.join(".config");
173 let schema_path = config_dir.join("aql.schema");
174
175 if schema_path.exists() {
176 return Err(AqlError::new(format!(
177 ".config/aql.schema already exists at {}",
178 schema_path.display()
179 )));
180 }
181
182 let detected = detect_stacks(root);
183 let schema = generate_schema(&detected);
184
185 std::fs::create_dir_all(&config_dir).map_err(|e| {
186 format!(
187 "Failed to create .config directory at {}: {e}",
188 config_dir.display()
189 )
190 })?;
191 std::fs::write(&schema_path, &schema)
192 .map_err(|e| format!("Failed to write schema at {}: {e}", schema_path.display()))?;
193
194 Ok(schema)
195}
196
197fn collect_npm_deps(pkg: &serde_json::Value) -> Vec<&str> {
198 let mut deps = Vec::new();
199 for key in &["dependencies", "devDependencies", "peerDependencies"] {
200 if let Some(obj) = pkg.get(key).and_then(|v| v.as_object()) {
201 for name in obj.keys() {
202 deps.push(name.as_str());
203 }
204 }
205 }
206 deps
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn detect_stacks_empty_dir() {
215 let detected = detect_stacks(Path::new("/nonexistent"));
217
218 assert!(detected.stacks.is_empty(), "no stacks in nonexistent dir");
220 assert!(!detected.has_tests, "no tests in nonexistent dir");
221 }
222
223 #[test]
224 fn generate_schema_express_with_tests() {
225 let detected = DetectedStacks {
227 stacks: vec![Stack::Express],
228 has_tests: true,
229 };
230
231 let schema = generate_schema(&detected);
233
234 assert!(
236 schema.contains(r#"<define tag="route""#),
237 "should define route tag"
238 );
239 assert!(
240 schema.contains(r#"<extractor name="express""#),
241 "should configure express extractor"
242 );
243 assert!(
244 schema.contains(r#"<define tag="test""#),
245 "should define test tag when has_tests"
246 );
247 assert!(
248 schema.contains(r#"<extractor name="test""#),
249 "should configure test extractor when has_tests"
250 );
251 }
252
253 #[test]
254 fn generate_schema_react() {
255 let detected = DetectedStacks {
257 stacks: vec![Stack::React],
258 has_tests: false,
259 };
260
261 let schema = generate_schema(&detected);
263
264 assert!(
266 schema.contains(r#"<define tag="component""#),
267 "should define component tag"
268 );
269 assert!(
270 schema.contains(r#"<define tag="hook""#),
271 "should define hook tag"
272 );
273 assert!(
274 schema.contains(r#"<extractor name="react""#),
275 "should configure react extractor"
276 );
277 }
278
279 #[test]
280 fn generate_schema_go() {
281 let detected = DetectedStacks {
283 stacks: vec![Stack::Go],
284 has_tests: false,
285 };
286
287 let schema = generate_schema(&detected);
289
290 assert!(
292 schema.contains(r#"<define tag="handler""#),
293 "should define handler tag"
294 );
295 assert!(
296 schema.contains(r#"<extractor name="go_http""#),
297 "should configure go_http extractor"
298 );
299 }
300}