oas_forge/
preprocessor.rs1use crate::index::Registry;
2use regex::Regex;
3use std::sync::OnceLock;
4
5static INSERT_RE: OnceLock<Regex> = OnceLock::new();
6static EXTEND_RE: OnceLock<Regex> = OnceLock::new();
7
8pub fn preprocess(content: &str, registry: &Registry) -> String {
10 let lines: Vec<&str> = content.lines().collect();
11 let mut new_lines = Vec::new();
12
13 let insert_re =
17 INSERT_RE.get_or_init(|| Regex::new(r"@insert\s+([a-zA-Z0-9_]+)(?:\((.*)\))?").unwrap());
18 let extend_re =
19 EXTEND_RE.get_or_init(|| Regex::new(r"@extend\s+([a-zA-Z0-9_]+)(?:\((.*)\))?").unwrap());
20
21 fn parse_args_from_caps(args_str: Option<regex::Match>) -> Vec<String> {
23 match args_str {
24 Some(m) => {
25 let s = m.as_str();
26 if s.trim().is_empty() {
27 Vec::new()
28 } else {
29 s.split(',')
30 .map(|x| x.trim().trim_matches('"').to_string())
31 .collect()
32 }
33 }
34 None => Vec::new(),
35 }
36 }
37
38 let mut i = 0;
43 while i < lines.len() {
44 let line = lines[i];
45
46 if let Some(caps) = insert_re.captures(line) {
47 let name = caps.get(1).unwrap().as_str();
49 let args = parse_args_from_caps(caps.get(2));
50
51 if let Some(fragment) = registry.fragments.get(name) {
52 let expanded = substitute_fragment_args(&fragment.body, &fragment.params, &args);
53 let indent = line
54 .chars()
55 .take_while(|c| c.is_whitespace())
56 .collect::<String>();
57
58 let trim_start = line.trim_start();
60 let doc_marker = if trim_start.starts_with("///") {
61 Some("///")
62 } else if trim_start.starts_with("//!") {
63 Some("//!")
64 } else {
65 None
66 };
67
68 if !expanded.trim().is_empty() {
69 for frag_line in expanded.lines() {
70 if let Some(marker) = doc_marker {
71 new_lines.push(format!("{}{} {}", indent, marker, frag_line));
74 } else {
75 new_lines.push(format!("{}{}", indent, frag_line));
76 }
77 }
78 }
79 } else {
80 log::warn!("Fragment '{}' not found for @insert", name);
81 new_lines.push(line.to_string());
82 }
83 } else if let Some(caps) = extend_re.captures(line) {
84 let name = caps.get(1).unwrap().as_str();
86 let args_raw = caps.get(2).map(|m| m.as_str()).unwrap_or("");
87
88 let indent = line
91 .chars()
92 .take_while(|c| c.is_whitespace())
93 .collect::<String>();
94 let marker_val = if args_raw.is_empty() {
96 name.to_string()
97 } else {
98 format!("{}({})", name, args_raw)
99 };
100 new_lines.push(format!("{}x-openapi-extend: \"{}\"", indent, marker_val));
101 } else {
102 new_lines.push(line.to_string());
103 }
104 i += 1;
105 }
106
107 let phase_a_output = new_lines.join("\n");
108
109 match serde_yaml::from_str::<serde_yaml::Value>(&phase_a_output) {
112 Ok(mut root) => {
113 process_value(&mut root, registry);
114 serde_yaml::to_string(&root).unwrap_or(phase_a_output)
115 }
116 Err(_) => {
117 phase_a_output
123 }
124 }
125}
126
127fn process_value(val: &mut serde_yaml::Value, registry: &Registry) {
128 if let serde_yaml::Value::Mapping(map) = val {
129 let extend_key = serde_yaml::Value::String("x-openapi-extend".to_string());
131
132 let mut fragment_to_merge = None;
133
134 if let Some(extend_val) = map.remove(&extend_key) {
135 if let Some(extend_str) = extend_val.as_str() {
136 fragment_to_merge = Some(extend_str.to_string());
137 }
138 }
139
140 if let Some(extend_str) = fragment_to_merge {
148 let (name, args) = parse_extend_str(&extend_str);
151
152 if let Some(fragment) = registry.fragments.get(&name) {
153 let expanded = substitute_fragment_args(&fragment.body, &fragment.params, &args);
154 if let Ok(frag_val) = serde_yaml::from_str::<serde_yaml::Value>(&expanded) {
155 merge_values(val, frag_val);
156 } else {
157 log::warn!("Fragment '{}' body is not valid YAML", name);
158 }
159 } else {
160 log::warn!("Fragment '{}' not found for @extend", name);
161 }
162 }
163
164 if let serde_yaml::Value::Mapping(map) = val {
167 for (_, v) in map {
168 process_value(v, registry);
169 }
170 }
171 } else if let serde_yaml::Value::Sequence(seq) = val {
172 for v in seq {
173 process_value(v, registry);
174 }
175 }
176}
177
178fn merge_values(target: &mut serde_yaml::Value, source: serde_yaml::Value) {
179 match (target, source) {
180 (serde_yaml::Value::Mapping(t_map), serde_yaml::Value::Mapping(s_map)) => {
181 for (k, v) in s_map {
182 if let Some(existing) = t_map.get_mut(&k) {
183 merge_values(existing, v);
184 } else {
185 t_map.insert(k, v);
186 }
187 }
188 }
189 (t, s) => {
190 *t = s;
191 }
192 }
193}
194
195fn parse_extend_str(s: &str) -> (String, Vec<String>) {
196 if let Some(idx) = s.find('(') {
197 let name = s[..idx].trim().to_string();
198 let args_str = s[idx + 1..].trim_end_matches(')');
199 let args = if args_str.trim().is_empty() {
200 Vec::new()
201 } else {
202 args_str
203 .split(',')
204 .map(|x| x.trim().trim_matches('"').to_string())
205 .collect()
206 };
207 (name, args)
208 } else {
209 (s.trim().to_string(), Vec::new())
210 }
211}
212
213fn substitute_fragment_args(fragment: &str, params: &[String], args: &[String]) -> String {
215 let mut result = fragment.to_string();
216 for (i, param) in params.iter().enumerate() {
217 if let Some(arg) = args.get(i) {
218 let placeholder = format!("{{{{{}}}}}", param); result = result.replace(&placeholder, arg);
220 }
221 }
222 result
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_insert_with_indentation() {
231 let mut registry = Registry::new();
232 registry.insert_fragment(
233 "Headers".to_string(),
234 vec![],
235 "header: x-val\nother: y-val".to_string(),
236 );
237
238 let input = " @insert Headers(\"\")";
239 let output = preprocess(input, ®istry);
240
241 let expected = "header: x-val\nother: y-val\n";
243 assert_eq!(output, expected);
244 }
245
246 #[test]
247 fn test_fragment_with_args() {
248 let mut registry = Registry::new();
249 registry.insert_fragment(
250 "Field".to_string(),
251 vec!["name".to_string()],
252 "name: {{name}}".to_string(),
253 );
254
255 let input = "@insert Field(\"my-name\")";
256 let output = preprocess(input, ®istry);
257 assert_eq!(output, "name: my-name\n");
258 }
259
260 #[test]
261 fn test_missing_fragment() {
262 let registry = Registry::new();
263 let input = "@insert Missing(\"\")";
264 let output = preprocess(input, ®istry);
265 assert_eq!(output, "@insert Missing(\"\")");
274 }
275}