1#![warn(missing_docs)]
2
3use nargo_ir::IRModule;
8use nargo_types::Result;
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct ApiMetadata {
18 pub endpoints: Vec<ApiEndpoint>,
20 pub types: HashMap<String, TypeDefinition>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
28pub struct ApiEndpoint {
29 pub controller: String,
31 pub name: String,
33 pub method: String,
35 pub path: String,
37 pub params: Vec<ApiParam>,
39 pub return_type: Option<String>,
41 pub is_async: bool,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, Default)]
49pub struct ApiParam {
50 pub name: String,
52 pub param_type: ApiParamType,
54 pub type_name: String,
56 pub extractor_param: Option<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
64pub enum ApiParamType {
65 Body,
67 Path,
69 Query,
71 Header,
73 CurrentUser,
75 Session,
77 I18n,
79 #[default]
81 Regular,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, Default)]
88pub struct TypeDefinition {
89 pub name: String,
91 pub fields: Vec<TypeField>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, Default)]
99pub struct TypeField {
100 pub name: String,
102 pub type_name: String,
104 pub optional: bool,
106}
107
108pub struct ApiMetadataExtractor;
112
113impl ApiMetadataExtractor {
114 pub fn extract(source: &str, _ir: &IRModule) -> Result<ApiMetadata> {
123 let mut metadata = ApiMetadata::default();
124
125 Self::extract_endpoints_from_source(source, &mut metadata)?;
126 Self::extract_types_from_source(source, &mut metadata)?;
127
128 Ok(metadata)
129 }
130
131 fn extract_endpoints_from_source(source: &str, metadata: &mut ApiMetadata) -> Result<()> {
140 let http_decorator_re = Regex::new(r#"@http\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*\)"#)?;
141 let export_function_re = Regex::new(r#"(async)?\s*function\s+(\w+)\s*\(([\s\S]*?)\)\s*(?::\s*([^\{]+?))?\s*\{"#)?;
142 let export_async_function_re = Regex::new(r#"export\s+(async)\s+function\s+(\w+)\s*\(([\s\S]*?)\)\s*(?::\s*([^\{]+?))?\s*\{"#)?;
143 let class_method_re = Regex::new(r#"(async)?\s*(\w+)\s*\(([\s\S]*?)\)\s*(?::\s*([^\{]+?))?\s*\{"#)?;
144 let class_def_re = Regex::new(r#"class\s+(\w+)\s*\{"#)?;
145
146 let lines: Vec<&str> = source.lines().collect();
147 let mut i = 0;
148 let mut current_controller = "Default".to_string();
149
150 while i < lines.len() {
151 let line = lines[i];
152
153 if let Some(class_caps) = class_def_re.captures(line) {
154 let class_name = class_caps[1].to_string();
155 if class_name.ends_with("Controller") {
156 current_controller = class_name;
157 }
158 }
159
160 if let Some(captures) = http_decorator_re.captures(line) {
161 let method = captures[1].to_string().to_uppercase();
162 let path = captures[2].to_string();
163
164 i += 1;
165 while i < lines.len() && lines[i].trim().is_empty() {
166 i += 1;
167 }
168
169 if i < lines.len() {
170 let next_line = lines[i];
171
172 if let Some(func_captures) = export_async_function_re.captures(&format!("{}{}", next_line, Self::collect_until_brace(&lines, i))) {
173 let is_async = func_captures.get(1).is_some();
174 let full_name = func_captures[2].to_string();
175 let (controller, name) = Self::parse_controller_and_name(&full_name);
176 let params_str = func_captures[3].to_string();
177 let return_type = func_captures.get(4).map(|m| m.as_str().trim().to_string());
178
179 let params = Self::parse_params(¶ms_str);
180
181 metadata.endpoints.push(ApiEndpoint { controller: if controller.is_empty() { current_controller.clone() } else { controller }, name, method, path, params, return_type, is_async });
182 }
183 else if let Some(func_captures) = class_method_re.captures(&format!("{}{}", next_line, Self::collect_until_brace(&lines, i))) {
184 let is_async = func_captures.get(1).is_some();
185 let name = func_captures[2].to_string();
186 let params_str = func_captures[3].to_string();
187 let return_type = func_captures.get(4).map(|m| m.as_str().trim().to_string());
188
189 let params = Self::parse_params(¶ms_str);
190
191 if !name.starts_with("constructor") && !name.starts_with('_') {
192 metadata.endpoints.push(ApiEndpoint { controller: current_controller.clone(), name, method, path, params, return_type, is_async });
193 }
194 }
195 else if let Some(func_captures) = export_function_re.captures(&format!("{}{}", next_line, Self::collect_until_brace(&lines, i))) {
196 let is_async = func_captures.get(1).is_some();
197 let full_name = func_captures[2].to_string();
198 let (controller, name) = Self::parse_controller_and_name(&full_name);
199 let params_str = func_captures[3].to_string();
200 let return_type = func_captures.get(4).map(|m| m.as_str().trim().to_string());
201
202 let params = Self::parse_params(¶ms_str);
203
204 metadata.endpoints.push(ApiEndpoint { controller: if controller.is_empty() { current_controller.clone() } else { controller }, name, method, path, params, return_type, is_async });
205 }
206 }
207 }
208
209 i += 1;
210 }
211
212 Ok(())
213 }
214
215 fn parse_controller_and_name(full_name: &str) -> (String, String) {
223 if let Some(idx) = full_name.find('.') { (full_name[..idx].to_string(), full_name[idx + 1..].to_string()) } else { ("Default".to_string(), full_name.to_string()) }
224 }
225
226 fn collect_until_brace(lines: &[&str], start: usize) -> String {
235 let mut result = String::new();
236 let mut i = start;
237
238 while i < lines.len() && !lines[i].contains('{') {
239 result.push_str(lines[i]);
240 i += 1;
241 }
242
243 if i < lines.len() {
244 if let Some(idx) = lines[i].find('{') {
245 result.push_str(&lines[i][0..idx]);
246 }
247 }
248
249 result
250 }
251
252 fn parse_params(params_str: &str) -> Vec<ApiParam> {
260 let mut params = Vec::new();
261
262 let param_re = Regex::new(r#"(?:@(\w+)(?:\s*\(\s*(?:['"]([^'"]*)['"])?\s*\))?\s+)?(\w+)\s*(?::\s*([^,\s]+))?"#).unwrap();
263
264 for caps in param_re.captures_iter(params_str) {
265 let decorator_name = caps.get(1).map(|m| m.as_str());
266 let extractor_param = caps.get(2).map(|m| m.as_str().to_string());
267 let name = caps[3].to_string();
268 let type_name = caps.get(4).map(|m| m.as_str().to_string()).unwrap_or_else(|| "any".to_string());
269
270 let param_type = match decorator_name {
271 Some("Body") => ApiParamType::Body,
272 Some("Path") => ApiParamType::Path,
273 Some("Query") => ApiParamType::Query,
274 Some("Header") => ApiParamType::Header,
275 Some("CurrentUser") => ApiParamType::CurrentUser,
276 Some("Session") => ApiParamType::Session,
277 Some("I18n") => ApiParamType::I18n,
278 _ => ApiParamType::Regular,
279 };
280
281 params.push(ApiParam { name, param_type, type_name, extractor_param });
282 }
283
284 params
285 }
286
287 fn extract_types_from_source(source: &str, metadata: &mut ApiMetadata) -> Result<()> {
296 let class_re = Regex::new(r#"class\s+(\w+)\s*\{([\s\S]*?)\}"#)?;
297 let interface_re = Regex::new(r#"interface\s+(\w+)\s*\{([\s\S]*?)\}"#)?;
298 let _type_alias_re = Regex::new(r#"type\s+(\w+)\s*=\s*([^;]+);"#)?;
299
300 for caps in class_re.captures_iter(source) {
301 let name = caps[1].to_string();
302 if !name.ends_with("Controller") {
303 let body = caps[2].to_string();
304 let fields = Self::parse_fields(&body);
305
306 metadata.types.insert(name.clone(), TypeDefinition { name, fields });
307 }
308 }
309
310 for caps in interface_re.captures_iter(source) {
311 let name = caps[1].to_string();
312 let body = caps[2].to_string();
313 let fields = Self::parse_fields(&body);
314
315 metadata.types.insert(name.clone(), TypeDefinition { name, fields });
316 }
317
318 Ok(())
319 }
320
321 fn parse_fields(body: &str) -> Vec<TypeField> {
329 let mut fields = Vec::new();
330 let field_re = Regex::new(r#"(\w+)([!?])?\s*:\s*([^;!\n]+)(?:[;!])?"#).unwrap();
331
332 for caps in field_re.captures_iter(body) {
333 let name = caps[1].to_string();
334 let optional = caps.get(2).map_or(false, |m| m.as_str() == "?");
335 let type_name = caps[3].to_string().trim().to_string();
336
337 fields.push(TypeField { name, type_name, optional });
338 }
339
340 fields
341 }
342}