oas_forge/
generics.rs

1use crate::index::Registry;
2use std::collections::HashSet;
3
4pub struct Monomorphizer<'a> {
5    registry: &'a mut Registry,
6    _processed_generics: HashSet<String>,
7}
8
9impl<'a> Monomorphizer<'a> {
10    pub fn new(registry: &'a mut Registry) -> Self {
11        Self {
12            registry,
13            _processed_generics: HashSet::new(),
14        }
15    }
16
17    /// Scans text for generic patterns like $Page<User> and generates concrete schemas.
18    /// Returns the text with $Page<User> replaced by $Page_User (which will be resolved to ref later).
19    pub fn process(&mut self, content: &str) -> String {
20        self.resolve_generics_in_text(content)
21    }
22
23    fn resolve_generics_in_text(&mut self, text: &str) -> String {
24        let mut result = String::new();
25        let chars: Vec<char> = text.chars().collect();
26        let mut i = 0;
27
28        while i < chars.len() {
29            if chars[i] == '$' && i + 1 < chars.len() && chars[i + 1].is_alphabetic() {
30                // Potential generic start
31                let start = i;
32                i += 1;
33                while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
34                    i += 1;
35                }
36                let name: String = chars[start + 1..i].iter().collect();
37
38                if i < chars.len() && chars[i] == '<' {
39                    // It is a generic! $Name<
40                    i += 1; // skip <
41                    let arg_start = i;
42                    let mut depth = 1;
43                    while i < chars.len() && depth > 0 {
44                        if chars[i] == '<' {
45                            depth += 1;
46                        } else if chars[i] == '>' {
47                            depth -= 1;
48                        }
49                        i += 1;
50                    }
51                    // i is now after the closing >
52                    // args_str exclude closing >
53                    let args_str: String = chars[arg_start..i - 1].iter().collect();
54
55                    // Create Concrete Schema
56                    let concrete_name = self.monomorphize(&name, &args_str);
57
58                    // Replace in text: $Page_User
59                    result.push('$');
60                    result.push_str(&concrete_name);
61                } else {
62                    // Just a regular $Name, push what we scanned
63                    result.push_str(&text[start..i]);
64                }
65            } else {
66                result.push(chars[i]);
67                i += 1;
68            }
69        }
70        result
71    }
72
73    /// Creates a concrete schema from a blueprint and args.
74    /// e.g. Name="Page", Args="User" -> "Page_User"
75    pub fn monomorphize(&mut self, name: &str, args_str: &str) -> String {
76        // 1. Recursive resolve args (handle nested $Result<Page<User>>)
77        let args = self.split_args(args_str);
78
79        // 2. Normalize Args (e.g. resolve inner generics first)
80        let resolved_args: Vec<String> = args
81            .into_iter()
82            .map(|arg| {
83                if arg.contains('<') {
84                    let processed = self.resolve_generics_in_text(&arg);
85                    processed.trim_start_matches('$').to_string()
86                } else {
87                    arg.trim_start_matches('$').to_string()
88                }
89            })
90            .collect();
91
92        // 3. Generate Concrete Name
93        let suffix = if resolved_args.is_empty() {
94            "Generic".to_string()
95        } else {
96            resolved_args.join("_")
97        };
98        let concrete_name = format!("{}_{}", name, suffix);
99
100        if self.registry.concrete_schemas.contains_key(&concrete_name) {
101            return concrete_name;
102        }
103
104        // 4. Instantiate Blueprint
105        if let Some(blueprint) = self.registry.blueprints.get(name).cloned() {
106            let mut content = blueprint.body.clone();
107
108            // Check arg count
109            if resolved_args.len() != blueprint.params.len() {
110                log::error!(
111                    "Blueprint {} expects {} args, got {}. Using raw args.",
112                    name,
113                    blueprint.params.len(),
114                    resolved_args.len()
115                );
116            }
117
118            // Named Substitution: Replace $Param with $Arg
119            for (idx, param) in blueprint.params.iter().enumerate() {
120                if let Some(arg) = resolved_args.get(idx) {
121                    // Pattern to replace: "$T" -> "$Arg"
122                    // We replace literal "$" + param name
123                    let target = format!("${}", param);
124                    let replacement = format!("${}", arg);
125                    content = content.replace(&target, &replacement);
126                }
127            }
128
129            self.registry
130                .concrete_schemas
131                .insert(concrete_name.clone(), content);
132        } else {
133            log::warn!("Blueprint {} not found", name);
134        }
135
136        concrete_name
137    }
138
139    fn split_args(&self, args_str: &str) -> Vec<String> {
140        let mut args = Vec::new();
141        let mut start = 0;
142        let mut depth = 0;
143        let chars = args_str.char_indices().peekable();
144
145        if args_str.trim().is_empty() {
146            return Vec::new();
147        }
148        for (i, c) in chars {
149            match c {
150                '<' => depth += 1,
151                '>' => depth -= 1,
152                ',' if depth == 0 => {
153                    args.push(args_str[start..i].trim().to_string());
154                    // we need to skip the comma which is at i
155                    start = i + 1;
156                }
157                _ => {}
158            }
159        }
160        if start < args_str.len() {
161            args.push(args_str[start..].trim().to_string());
162        }
163        args
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    #[test]
171    fn test_monomorphize_named() {
172        let mut registry = Registry::new();
173        registry.insert_blueprint(
174            "Page".to_string(),
175            vec!["T".to_string()],
176            "data: $ref: $T".to_string(),
177        );
178
179        let mut mono = Monomorphizer::new(&mut registry);
180        let result = mono.process("scheme: $ref: $Page<User>");
181
182        // Should generate Page_User
183        assert_eq!(result, "scheme: $ref: $Page_User");
184
185        // Verify concrete schema content
186        let concrete = registry.concrete_schemas.get("Page_User").unwrap();
187        assert_eq!(concrete, "data: $ref: $User");
188    }
189
190    #[test]
191    fn test_nested_generics() {
192        let mut registry = Registry::new();
193        registry.insert_blueprint(
194            "Wrapper".to_string(),
195            vec!["T".to_string()],
196            "wrap: $T".to_string(),
197        );
198        registry.insert_blueprint(
199            "Inner".to_string(),
200            vec!["U".to_string()],
201            "in: $U".to_string(),
202        );
203
204        let mut mono = Monomorphizer::new(&mut registry);
205        let result = mono.process("$Wrapper<$Inner<Item>>");
206
207        assert_eq!(result, "$Wrapper_Inner_Item");
208
209        // Verify intermediate
210        assert!(registry.concrete_schemas.contains_key("Inner_Item"));
211        let inner = registry.concrete_schemas.get("Inner_Item").unwrap();
212        assert_eq!(inner, "in: $Item");
213
214        // Verify outer
215        assert!(registry.concrete_schemas.contains_key("Wrapper_Inner_Item"));
216        let wrapper = registry.concrete_schemas.get("Wrapper_Inner_Item").unwrap();
217        // Wrapper expects wrap: $T. T is Inner_Item. So wrap: $Inner_Item.
218        assert_eq!(wrapper, "wrap: $Inner_Item");
219    }
220}