use crate::index::Registry;
use regex::Regex;
use std::sync::OnceLock;
static INSERT_RE: OnceLock<Regex> = OnceLock::new();
static EXTEND_RE: OnceLock<Regex> = OnceLock::new();
pub fn preprocess(content: &str, registry: &Registry) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut new_lines = Vec::new();
let insert_re =
INSERT_RE.get_or_init(|| Regex::new(r"@insert\s+([a-zA-Z0-9_]+)(?:\((.*)\))?").unwrap());
let extend_re =
EXTEND_RE.get_or_init(|| Regex::new(r"@extend\s+([a-zA-Z0-9_]+)(?:\((.*)\))?").unwrap());
fn parse_args_from_caps(args_str: Option<regex::Match>) -> Vec<String> {
match args_str {
Some(m) => {
let s = m.as_str();
if s.trim().is_empty() {
Vec::new()
} else {
s.split(',')
.map(|x| x.trim().trim_matches('"').to_string())
.collect()
}
}
None => Vec::new(),
}
}
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if let Some(caps) = insert_re.captures(line) {
let name = caps.get(1).unwrap().as_str();
let args = parse_args_from_caps(caps.get(2));
if let Some(fragment) = registry.fragments.get(name) {
let expanded = substitute_fragment_args(&fragment.body, &fragment.params, &args);
let indent = line
.chars()
.take_while(|c| c.is_whitespace())
.collect::<String>();
let trim_start = line.trim_start();
let doc_marker = if trim_start.starts_with("///") {
Some("///")
} else if trim_start.starts_with("//!") {
Some("//!")
} else {
None
};
if !expanded.trim().is_empty() {
for frag_line in expanded.lines() {
if let Some(marker) = doc_marker {
new_lines.push(format!("{}{} {}", indent, marker, frag_line));
} else {
new_lines.push(format!("{}{}", indent, frag_line));
}
}
}
} else {
log::warn!("Fragment '{}' not found for @insert", name);
new_lines.push(line.to_string());
}
} else if let Some(caps) = extend_re.captures(line) {
let name = caps.get(1).unwrap().as_str();
let args_raw = caps.get(2).map(|m| m.as_str()).unwrap_or("");
let indent = line
.chars()
.take_while(|c| c.is_whitespace())
.collect::<String>();
let marker_val = if args_raw.is_empty() {
name.to_string()
} else {
format!("{}({})", name, args_raw)
};
new_lines.push(format!("{}x-openapi-extend: \"{}\"", indent, marker_val));
} else {
new_lines.push(line.to_string());
}
i += 1;
}
let phase_a_output = new_lines.join("\n");
match serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&phase_a_output) {
Ok(mut root) => {
process_value(&mut root, registry);
serde_yaml_ng::to_string(&root).unwrap_or(phase_a_output)
}
Err(_) => {
phase_a_output
}
}
}
fn process_value(val: &mut serde_yaml_ng::Value, registry: &Registry) {
if let serde_yaml_ng::Value::Mapping(map) = val {
let extend_key = serde_yaml_ng::Value::String("x-openapi-extend".to_string());
let mut fragment_to_merge = None;
if let Some(extend_val) = map.remove(&extend_key) {
if let Some(extend_str) = extend_val.as_str() {
fragment_to_merge = Some(extend_str.to_string());
}
}
if let Some(extend_str) = fragment_to_merge {
let (name, args) = parse_extend_str(&extend_str);
if let Some(fragment) = registry.fragments.get(&name) {
let expanded = substitute_fragment_args(&fragment.body, &fragment.params, &args);
if let Ok(frag_val) = serde_yaml_ng::from_str::<serde_yaml_ng::Value>(&expanded) {
merge_values(val, frag_val);
} else {
log::warn!("Fragment '{}' body is not valid YAML", name);
}
} else {
log::warn!("Fragment '{}' not found for @extend", name);
}
}
if let serde_yaml_ng::Value::Mapping(map) = val {
for (_, v) in map {
process_value(v, registry);
}
}
} else if let serde_yaml_ng::Value::Sequence(seq) = val {
for v in seq {
process_value(v, registry);
}
}
}
fn merge_values(target: &mut serde_yaml_ng::Value, source: serde_yaml_ng::Value) {
match (target, source) {
(serde_yaml_ng::Value::Mapping(t_map), serde_yaml_ng::Value::Mapping(s_map)) => {
for (k, v) in s_map {
if let Some(existing) = t_map.get_mut(&k) {
merge_values(existing, v);
} else {
t_map.insert(k, v);
}
}
}
(t, s) => {
*t = s;
}
}
}
fn parse_extend_str(s: &str) -> (String, Vec<String>) {
if let Some(idx) = s.find('(') {
let name = s[..idx].trim().to_string();
let args_str = s[idx + 1..].trim_end_matches(')');
let args = if args_str.trim().is_empty() {
Vec::new()
} else {
args_str
.split(',')
.map(|x| x.trim().trim_matches('"').to_string())
.collect()
};
(name, args)
} else {
(s.trim().to_string(), Vec::new())
}
}
fn substitute_fragment_args(fragment: &str, params: &[String], args: &[String]) -> String {
let mut result = fragment.to_string();
for (i, param) in params.iter().enumerate() {
if let Some(arg) = args.get(i) {
let placeholder = format!("{{{{{}}}}}", param); result = result.replace(&placeholder, arg);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_with_indentation() {
let mut registry = Registry::new();
registry.insert_fragment(
"Headers".to_string(),
vec![],
"header: x-val\nother: y-val".to_string(),
);
let input = " @insert Headers(\"\")";
let output = preprocess(input, ®istry);
let expected = "header: x-val\nother: y-val\n";
assert_eq!(output, expected);
}
#[test]
fn test_fragment_with_args() {
let mut registry = Registry::new();
registry.insert_fragment(
"Field".to_string(),
vec!["name".to_string()],
"name: {{name}}".to_string(),
);
let input = "@insert Field(\"my-name\")";
let output = preprocess(input, ®istry);
assert_eq!(output, "name: my-name\n");
}
#[test]
fn test_missing_fragment() {
let registry = Registry::new();
let input = "@insert Missing(\"\")";
let output = preprocess(input, ®istry);
assert_eq!(output, "@insert Missing(\"\")");
}
}