1use crate::dsl;
2use crate::error::{Error, Result};
3use crate::generics::Monomorphizer;
4use crate::index::Registry;
5use crate::preprocessor;
6use crate::visitor::{self, ExtractedItem};
7use regex::Regex;
8use std::collections::HashSet;
9use std::path::PathBuf;
10use std::sync::OnceLock;
11use walkdir::WalkDir;
12
13#[derive(Debug, Clone)]
15pub struct Snippet {
16 pub content: String,
17 pub file_path: PathBuf,
18 pub line_number: usize,
19 pub operation_id: Option<String>,
20}
21
22pub fn preprocess_macros(snippet: &Snippet, registry: &mut Registry) -> Snippet {
25 let content = &snippet.content;
26 let mut new_lines = Vec::new();
27
28 static GENERIC_RE: OnceLock<Regex> = OnceLock::new();
30 let generic_re =
31 GENERIC_RE.get_or_init(|| Regex::new(r"\$([a-zA-Z0-9_]+)<([a-zA-Z0-9_, ]+)>").unwrap());
32
33 static MACRO_INSERT_RE: OnceLock<Regex> = OnceLock::new();
34 let macro_insert_re = MACRO_INSERT_RE
35 .get_or_init(|| Regex::new(r"^(\s*)(-)?\s*@insert\s+([a-zA-Z0-9_]+)$").unwrap());
36
37 static MACRO_EXTEND_RE: OnceLock<Regex> = OnceLock::new();
38 let macro_extend_re =
39 MACRO_EXTEND_RE.get_or_init(|| Regex::new(r"^(\s*)@extend\s+(.+)$").unwrap());
40
41 static ARRAY_SHORT_RE: OnceLock<Regex> = OnceLock::new();
47 let array_short_re =
48 ARRAY_SHORT_RE.get_or_init(|| Regex::new(r"\$Vec<([a-zA-Z0-9_]+)>").unwrap());
49
50 for line in content.lines() {
51 let current_lines = vec![line.to_string()];
52
53 for sub_line in current_lines {
65 let mut processed_line = sub_line.clone();
66
67 while let Some(caps) = array_short_re.captures(&processed_line.clone()) {
70 let full_match = caps.get(0).unwrap().as_str();
71 let type_name = caps.get(1).unwrap().as_str();
72 let replacement = format!(
74 "{{ type: array, items: {{ $ref: \"#/components/schemas/{}\" }} }}",
75 type_name
76 );
77 processed_line = processed_line.replace(full_match, &replacement);
78 }
79
80 while let Some(caps) = generic_re.captures(&processed_line.clone()) {
83 let full_match = caps.get(0).unwrap().as_str();
84 let name = caps.get(1).unwrap().as_str();
85 let args_raw = caps.get(2).unwrap().as_str();
86
87 let mut mono = Monomorphizer::new(registry);
89 let concrete_name = mono.monomorphize(name, args_raw);
90
91 let replacement = format!("${}", concrete_name);
93 processed_line = processed_line.replace(full_match, &replacement);
94 }
95
96 if let Some(caps) = macro_insert_re.captures(&processed_line) {
98 let indent = &caps[1];
99 let name = &caps[3];
100
101 if !registry.fragments.contains_key(name) {
102 let final_indent = format!("{}- ", indent);
103 new_lines.push(format!(
104 "{}$ref: \"#/components/parameters/{}\"",
105 final_indent, name
106 ));
107 continue;
108 }
109 }
110
111 if let Some(caps) = macro_extend_re.captures(&processed_line) {
113 let indent = &caps[1];
114 let content = &caps[2];
115 let escaped_content = content.replace('\'', "''");
116 new_lines.push(format!("{}x-openapi-extend: '{}'", indent, escaped_content));
117 continue;
118 }
119
120 new_lines.push(processed_line);
121 }
122 }
123
124 Snippet {
125 content: new_lines.join("\n"),
126 file_path: snippet.file_path.clone(),
127 line_number: snippet.line_number,
128 operation_id: snippet.operation_id.clone(),
129 }
130}
131
132pub fn substitute_smart_references(content: &str, schemas: &HashSet<String>) -> String {
133 let mut result = String::with_capacity(content.len());
134 let chars: Vec<char> = content.chars().collect();
135 let mut i = 0;
136
137 while i < chars.len() {
138 if chars[i] == '$' {
139 let mut j = i + 1;
140 if j < chars.len() && (chars[j].is_alphabetic() || chars[j] == '_') {
141 while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') {
142 j += 1;
143 }
144
145 let ident: String = chars[i + 1..j].iter().collect();
146
147 if schemas.contains(&ident) {
148 let is_quoted = i > 0 && chars[i - 1] == '"';
149
150 if !is_quoted {
151 result.push('"');
152 }
153 result.push_str("#/components/schemas/");
154 result.push_str(&ident);
155 if !is_quoted {
156 result.push('"');
157 }
158
159 i = j;
160 continue;
161 }
162 }
163 }
164 result.push(chars[i]);
165 i += 1;
166 }
167 result
168}
169
170fn finalize_substitution(content: &str) -> String {
171 let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
172 let step1 = content.replace(r"\$", "$");
173 step1.replace("{{CARGO_PKG_VERSION}}", &version)
174}
175
176pub fn scan_directories(roots: &[PathBuf], includes: &[PathBuf]) -> Result<Vec<Snippet>> {
177 let mut registry = Registry::new();
178 let mut operation_snippets: Vec<Snippet> = Vec::new();
179 let mut files_found = false;
180
181 let mut all_paths = Vec::new();
182
183 for root in roots {
184 for entry in WalkDir::new(root) {
185 let entry = entry.map_err(|e| Error::Io(std::io::Error::other(e)))?;
186 let path = entry.path().to_path_buf();
187 if path.is_file() {
188 all_paths.push(path);
189 }
190 }
191 }
192 for path in includes {
193 if path.exists() {
194 all_paths.push(path.to_path_buf());
195 }
196 }
197
198 if !all_paths.is_empty() {
199 files_found = true;
200 }
201
202 for path in all_paths {
204 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
205 match ext {
206 "rs" => {
207 let extracted = visitor::extract_from_file(path.clone())?;
208 for item in extracted {
209 match item {
210 ExtractedItem::Schema {
211 name,
212 content,
213 line,
214 } => {
215 if let Some(n) = name {
216 registry.insert_schema(n, content.clone());
217 }
218 operation_snippets.push(Snippet {
219 content,
220 file_path: path.clone(),
221 line_number: line,
222 operation_id: None,
223 });
224 }
225 ExtractedItem::RouteDSL {
226 content,
227 line,
228 operation_id,
229 } => {
230 operation_snippets.push(Snippet {
231 content,
232 file_path: path.clone(),
233 line_number: line,
234 operation_id: Some(operation_id),
235 });
236 }
237 ExtractedItem::Fragment {
238 name,
239 params,
240 content,
241 ..
242 } => {
243 registry.insert_fragment(name, params, content);
244 }
245 ExtractedItem::Blueprint {
246 name,
247 params,
248 content,
249 ..
250 } => {
251 registry.insert_blueprint(name, params, content);
252 }
253 }
254 }
255 }
256 "json" | "yaml" | "yml" => {
257 let content = std::fs::read_to_string(&path)?;
258 operation_snippets.push(Snippet {
259 content,
260 file_path: path.clone(),
261 line_number: 1,
262 operation_id: None,
263 });
264 }
265 _ => {}
266 }
267 }
268 }
269
270 let mut preprocessed_snippets = Vec::new();
272 for snippet in operation_snippets {
273 let macrod_snippet = preprocess_macros(&snippet, &mut registry);
275
276 let expanded_content = preprocessor::preprocess(¯od_snippet.content, ®istry);
278
279 let final_content = if let Some(op_id) = ¯od_snippet.operation_id {
281 let lines: Vec<String> = expanded_content.lines().map(|s| s.to_string()).collect();
282 if let Some(yaml) = dsl::parse_route_dsl(&lines, op_id) {
283 yaml
284 } else {
285 expanded_content
289 }
290 } else {
291 expanded_content
292 };
293
294 preprocessed_snippets.push(Snippet {
295 content: final_content,
296 file_path: macrod_snippet.file_path,
297 line_number: macrod_snippet.line_number,
298 operation_id: macrod_snippet.operation_id,
299 });
300 }
301
302 let mut monomorphizer = Monomorphizer::new(&mut registry);
304 let mut mono_snippets: Vec<Snippet> = Vec::new();
305
306 for snippet in preprocessed_snippets {
307 let mono_content = monomorphizer.process(&snippet.content);
308 mono_snippets.push(Snippet {
309 content: mono_content,
310 file_path: snippet.file_path,
311 line_number: snippet.line_number,
312 operation_id: snippet.operation_id,
313 });
314 }
315
316 let mut generated_snippets = Vec::new();
318 for (name, content) in ®istry.concrete_schemas {
319 let wrapped = format!(
320 "components:\n schemas:\n {}:\n{}",
321 name,
322 indent(content)
323 );
324 generated_snippets.push(Snippet {
325 content: wrapped,
326 file_path: PathBuf::from("<generated>"),
327 line_number: 1,
328 operation_id: None,
329 });
330 }
331 mono_snippets.extend(generated_snippets);
332
333 let mut all_schemas = registry.schemas.keys().cloned().collect::<HashSet<_>>();
335 all_schemas.extend(registry.concrete_schemas.keys().cloned());
336
337 let mut final_snippets = Vec::new();
338 for snippet in mono_snippets {
339 let subbed = substitute_smart_references(&snippet.content, &all_schemas);
340 let finalized_content = finalize_substitution(&subbed);
341 final_snippets.push(Snippet {
342 content: finalized_content,
343 file_path: snippet.file_path,
344 line_number: snippet.line_number,
345 operation_id: snippet.operation_id,
346 });
347 }
348
349 if !files_found {
350 return Err(Error::NoFilesFound);
351 }
352
353 Ok(final_snippets)
354}
355
356fn indent(s: &str) -> String {
357 s.lines()
358 .map(|l| format!(" {}", l))
359 .collect::<Vec<_>>()
360 .join("\n")
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_escaping() {
369 let input = r"price: \$100";
370 let output = finalize_substitution(input);
371 assert_eq!(output, "price: $100");
372 }
373
374 #[test]
375 fn test_vec_macro() {
376 let mut registry = Registry::new();
377 let snippet = Snippet {
378 content: "tags: $Vec<Tag>".to_string(),
379 file_path: PathBuf::from("test.rs"),
380 line_number: 1,
381 operation_id: None,
382 };
383 let processed = preprocess_macros(&snippet, &mut registry);
384 assert!(processed.content.contains("type: array"));
385 assert!(processed.content.contains("items:"));
386 assert!(
387 processed
388 .content
389 .contains("$ref: \"#/components/schemas/Tag\"")
390 );
391 }
392
393 }