oag-core 0.20.5

OpenAPI 3.2 parser, IR, and transforms for oag
Documentation
use regex::Regex;

use super::pack::BundledLayout;

/// Produce a single bundled file by rendering sections, stripping imports, and concatenating.
pub fn bundle_sections(layout: &BundledLayout, rendered_sections: Vec<(&str, String)>) -> String {
    let strip_regexes: Vec<Regex> = layout
        .strip_patterns
        .iter()
        .filter_map(|p| Regex::new(p).ok())
        .collect();

    let import_patterns: Vec<String> = layout.strip_import_patterns.clone();

    let mut output = String::new();
    output.push_str("// Auto-generated by oag — do not edit (bundled)\n\n");

    for (label, content) in rendered_sections {
        output.push_str(&format!("// === {label} ===\n\n"));

        let mut stripped = content.clone();

        // Strip header lines matching strip_patterns
        for regex in &strip_regexes {
            stripped = stripped
                .lines()
                .filter(|line| !regex.is_match(line))
                .collect::<Vec<_>>()
                .join("\n");
        }

        // Strip relative imports
        stripped = strip_relative_imports(&stripped, &import_patterns);

        output.push_str(&stripped);
        output.push('\n');
    }

    output
}

/// Remove import lines/blocks that reference any of the given patterns.
fn strip_relative_imports(content: &str, patterns: &[String]) -> String {
    let lines: Vec<&str> = content.lines().collect();
    let mut result = Vec::new();
    let mut i = 0;

    let is_match = |s: &str| -> bool { patterns.iter().any(|p| s.contains(p.as_str())) };

    while i < lines.len() {
        let trimmed = lines[i].trim();

        if trimmed.starts_with("import ") || trimmed.starts_with("import type ") {
            // Single-line import ending with `;`
            if trimmed.ends_with(';') {
                if is_match(trimmed) {
                    i += 1;
                    continue;
                }
                result.push(lines[i]);
                i += 1;
                continue;
            }

            // Multi-line import — scan ahead to the closing `} from "...";`
            let start = i;
            i += 1;
            let mut found_close = false;
            while i < lines.len() {
                let t = lines[i].trim();
                if t.starts_with("} from ") {
                    found_close = true;
                    if is_match(t) {
                        // Skip entire block
                        i += 1;
                    } else {
                        // Keep entire block
                        result.extend_from_slice(&lines[start..=i]);
                        i += 1;
                    }
                    break;
                }
                i += 1;
            }
            if !found_close {
                // Unterminated import block — keep as-is
                result.extend_from_slice(&lines[start..i]);
            }
            continue;
        }

        result.push(lines[i]);
        i += 1;
    }

    result.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_strip_relative_imports() {
        let content = r#"import type {
  Pet,
  NewPet,
} from "./types";
import { streamSse, type SSEOptions } from "./sse";

export class ApiClient {"#;

        let patterns = vec!["\"./types\"".to_string(), "\"./sse\"".to_string()];

        let stripped = strip_relative_imports(content, &patterns);
        assert!(!stripped.contains("from \"./types\""));
        assert!(!stripped.contains("from \"./sse\""));
        assert!(stripped.contains("export class ApiClient {"));
    }

    #[test]
    fn test_strip_auto_generated_header() {
        let layout = super::BundledLayout {
            output_path: "index.ts".to_string(),
            sections: vec![],
            strip_patterns: vec!["^// Auto-generated by oag.*$".to_string()],
            strip_import_patterns: vec![],
        };

        let content = "// Auto-generated by oag — do not edit\nexport interface Foo {}".to_string();
        let rendered = vec![("Types", content)];
        let result = bundle_sections(&layout, rendered);
        // The section content should not have the header
        assert!(result.contains("export interface Foo {}"));
        // Only the bundled header should remain
        assert_eq!(
            result.matches("Auto-generated").count(),
            1,
            "only the bundled header, not the section header"
        );
    }
}