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 if !expanded.trim().is_empty() {
58 for frag_line in expanded.lines() {
59 new_lines.push(format!("{}{}", indent, frag_line));
60 }
61 }
62 } else {
63 log::warn!("Fragment '{}' not found for @insert", name);
64 new_lines.push(line.to_string());
65 }
66 } else if let Some(caps) = extend_re.captures(line) {
67 let name = caps.get(1).unwrap().as_str();
69 let args_raw = caps.get(2).map(|m| m.as_str()).unwrap_or("");
70
71 let indent = line
74 .chars()
75 .take_while(|c| c.is_whitespace())
76 .collect::<String>();
77 let marker_val = if args_raw.is_empty() {
79 name.to_string()
80 } else {
81 format!("{}({})", name, args_raw)
82 };
83 new_lines.push(format!("{}x-openapi-extend: \"{}\"", indent, marker_val));
84 } else {
85 new_lines.push(line.to_string());
86 }
87 i += 1;
88 }
89
90 let phase_a_output = new_lines.join("\n");
91
92 match serde_yaml::from_str::<serde_yaml::Value>(&phase_a_output) {
95 Ok(mut root) => {
96 process_value(&mut root, registry);
97 serde_yaml::to_string(&root).unwrap_or(phase_a_output)
98 }
99 Err(_) => {
100 phase_a_output
106 }
107 }
108}
109
110fn process_value(val: &mut serde_yaml::Value, registry: &Registry) {
111 if let serde_yaml::Value::Mapping(map) = val {
112 let extend_key = serde_yaml::Value::String("x-openapi-extend".to_string());
114
115 let mut fragment_to_merge = None;
116
117 if let Some(extend_val) = map.remove(&extend_key) {
118 if let Some(extend_str) = extend_val.as_str() {
119 fragment_to_merge = Some(extend_str.to_string());
120 }
121 }
122
123 if let Some(extend_str) = fragment_to_merge {
131 let (name, args) = parse_extend_str(&extend_str);
134
135 if let Some(fragment) = registry.fragments.get(&name) {
136 let expanded = substitute_fragment_args(&fragment.body, &fragment.params, &args);
137 if let Ok(frag_val) = serde_yaml::from_str::<serde_yaml::Value>(&expanded) {
138 merge_values(val, frag_val);
139 } else {
140 log::warn!("Fragment '{}' body is not valid YAML", name);
141 }
142 } else {
143 log::warn!("Fragment '{}' not found for @extend", name);
144 }
145 }
146
147 if let serde_yaml::Value::Mapping(map) = val {
150 for (_, v) in map {
151 process_value(v, registry);
152 }
153 }
154 } else if let serde_yaml::Value::Sequence(seq) = val {
155 for v in seq {
156 process_value(v, registry);
157 }
158 }
159}
160
161fn merge_values(target: &mut serde_yaml::Value, source: serde_yaml::Value) {
162 match (target, source) {
163 (serde_yaml::Value::Mapping(t_map), serde_yaml::Value::Mapping(s_map)) => {
164 for (k, v) in s_map {
165 if let Some(existing) = t_map.get_mut(&k) {
166 merge_values(existing, v);
167 } else {
168 t_map.insert(k, v);
169 }
170 }
171 }
172 (t, s) => {
173 *t = s;
174 }
175 }
176}
177
178fn parse_extend_str(s: &str) -> (String, Vec<String>) {
179 if let Some(idx) = s.find('(') {
180 let name = s[..idx].trim().to_string();
181 let args_str = s[idx + 1..].trim_end_matches(')');
182 let args = if args_str.trim().is_empty() {
183 Vec::new()
184 } else {
185 args_str
186 .split(',')
187 .map(|x| x.trim().trim_matches('"').to_string())
188 .collect()
189 };
190 (name, args)
191 } else {
192 (s.trim().to_string(), Vec::new())
193 }
194}
195
196fn substitute_fragment_args(fragment: &str, params: &[String], args: &[String]) -> String {
198 let mut result = fragment.to_string();
199 for (i, param) in params.iter().enumerate() {
200 if let Some(arg) = args.get(i) {
201 let placeholder = format!("{{{{{}}}}}", param); result = result.replace(&placeholder, arg);
203 }
204 }
205 result
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_insert_with_indentation() {
214 let mut registry = Registry::new();
215 registry.insert_fragment(
216 "Headers".to_string(),
217 vec![],
218 "header: x-val\nother: y-val".to_string(),
219 );
220
221 let input = " @insert Headers(\"\")";
222 let output = preprocess(input, ®istry);
223
224 let expected = "header: x-val\nother: y-val\n";
226 assert_eq!(output, expected);
227 }
228
229 #[test]
230 fn test_fragment_with_args() {
231 let mut registry = Registry::new();
232 registry.insert_fragment(
233 "Field".to_string(),
234 vec!["name".to_string()],
235 "name: {{name}}".to_string(),
236 );
237
238 let input = "@insert Field(\"my-name\")";
239 let output = preprocess(input, ®istry);
240 assert_eq!(output, "name: my-name\n");
241 }
242
243 #[test]
244 fn test_missing_fragment() {
245 let registry = Registry::new();
246 let input = "@insert Missing(\"\")";
247 let output = preprocess(input, ®istry);
248 assert_eq!(output, "@insert Missing(\"\")");
257 }
258}