1use std::path::Path;
7
8use forge_core::schema::{
9 EnumDef, EnumVariant, FieldDef, FunctionArg, FunctionDef, FunctionKind, RustType,
10 SchemaRegistry, TableDef,
11};
12use syn::{Attribute, Expr, Fields, FnArg, Lit, Meta, Pat, ReturnType};
13use walkdir::WalkDir;
14
15use crate::Error;
16
17pub fn parse_project(src_dir: &Path) -> Result<SchemaRegistry, Error> {
19 let registry = SchemaRegistry::new();
20
21 for entry in WalkDir::new(src_dir)
22 .into_iter()
23 .filter_map(|e| e.ok())
24 .filter(|e| e.path().extension().map(|ext| ext == "rs").unwrap_or(false))
25 {
26 let content = std::fs::read_to_string(entry.path())?;
27 if let Err(e) = parse_file(&content, ®istry) {
28 tracing::debug!(file = ?entry.path(), error = %e, "Failed to parse file");
29 }
30 }
31
32 Ok(registry)
33}
34
35fn parse_file(content: &str, registry: &SchemaRegistry) -> Result<(), Error> {
37 let file = syn::parse_file(content).map_err(|e| Error::Template(e.to_string()))?;
38
39 for item in file.items {
40 match item {
41 syn::Item::Struct(item_struct) => {
42 if has_forge_model_attr(&item_struct.attrs) {
43 if let Some(table) = parse_model(&item_struct) {
44 registry.register_table(table);
45 }
46 }
47 }
48 syn::Item::Enum(item_enum) => {
49 if has_forge_enum_attr(&item_enum.attrs) {
50 if let Some(enum_def) = parse_enum(&item_enum) {
51 registry.register_enum(enum_def);
52 }
53 }
54 }
55 syn::Item::Fn(item_fn) => {
56 if let Some(func) = parse_function(&item_fn) {
57 registry.register_function(func);
58 }
59 }
60 _ => {}
61 }
62 }
63
64 Ok(())
65}
66
67fn has_forge_model_attr(attrs: &[Attribute]) -> bool {
69 attrs.iter().any(|attr| {
70 let path = attr.path();
71 path.is_ident("model")
72 || path.segments.len() == 2
73 && path.segments[0].ident == "forge"
74 && path.segments[1].ident == "model"
75 })
76}
77
78fn has_forge_enum_attr(attrs: &[Attribute]) -> bool {
80 attrs.iter().any(|attr| {
81 let path = attr.path();
82 path.is_ident("forge_enum")
83 || path.is_ident("enum_type")
84 || path.segments.len() == 2
85 && path.segments[0].ident == "forge"
86 && path.segments[1].ident == "enum_type"
87 })
88}
89
90fn parse_model(item: &syn::ItemStruct) -> Option<TableDef> {
92 let struct_name = item.ident.to_string();
93 let table_name = get_table_name_from_attrs(&item.attrs).unwrap_or_else(|| {
94 let snake = to_snake_case(&struct_name);
95 pluralize(&snake)
96 });
97
98 let mut table = TableDef::new(&table_name, &struct_name);
99
100 table.doc = get_doc_comment(&item.attrs);
102
103 if let Fields::Named(fields) = &item.fields {
105 for field in &fields.named {
106 if let Some(field_name) = &field.ident {
107 let field_def = parse_field(field_name.to_string(), &field.ty, &field.attrs);
108 table.fields.push(field_def);
109 }
110 }
111 }
112
113 Some(table)
114}
115
116fn parse_field(name: String, ty: &syn::Type, attrs: &[Attribute]) -> FieldDef {
118 let rust_type = type_to_rust_type(ty);
119 let mut field = FieldDef::new(&name, rust_type);
120 field.column_name = to_snake_case(&name);
121 field.doc = get_doc_comment(attrs);
122 field
123}
124
125fn parse_enum(item: &syn::ItemEnum) -> Option<EnumDef> {
127 let enum_name = item.ident.to_string();
128 let mut enum_def = EnumDef::new(&enum_name);
129 enum_def.doc = get_doc_comment(&item.attrs);
130
131 for variant in &item.variants {
132 let variant_name = variant.ident.to_string();
133 let mut enum_variant = EnumVariant::new(&variant_name);
134 enum_variant.doc = get_doc_comment(&variant.attrs);
135
136 if let Some((_, Expr::Lit(lit))) = &variant.discriminant {
138 if let Lit::Int(int_lit) = &lit.lit {
139 if let Ok(value) = int_lit.base10_parse::<i32>() {
140 enum_variant.int_value = Some(value);
141 }
142 }
143 }
144
145 enum_def.variants.push(enum_variant);
146 }
147
148 Some(enum_def)
149}
150
151fn parse_function(item: &syn::ItemFn) -> Option<FunctionDef> {
153 let kind = get_function_kind(&item.attrs)?;
154 let func_name = item.sig.ident.to_string();
155
156 let return_type = match &item.sig.output {
158 ReturnType::Default => RustType::Custom("()".to_string()),
159 ReturnType::Type(_, ty) => extract_result_type(ty),
160 };
161
162 let mut func = FunctionDef::new(&func_name, kind, return_type);
163 func.doc = get_doc_comment(&item.attrs);
164 func.is_async = item.sig.asyncness.is_some();
165
166 let mut skip_first = true;
168 for arg in &item.sig.inputs {
169 if let FnArg::Typed(pat_type) = arg {
170 if skip_first {
172 skip_first = false;
173 let type_str = quote::quote!(#pat_type.ty).to_string();
175 if type_str.contains("Context")
176 || type_str.contains("QueryContext")
177 || type_str.contains("MutationContext")
178 || type_str.contains("ActionContext")
179 {
180 continue;
181 }
182 }
183
184 if let Pat::Ident(pat_ident) = &*pat_type.pat {
186 let arg_name = pat_ident.ident.to_string();
187 let arg_type = type_to_rust_type(&pat_type.ty);
188 func.args.push(FunctionArg::new(arg_name, arg_type));
189 }
190 }
191 }
192
193 Some(func)
194}
195
196fn get_function_kind(attrs: &[Attribute]) -> Option<FunctionKind> {
198 for attr in attrs {
199 let path = attr.path();
200 let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
201
202 let kind_str = if segments.len() == 2 && segments[0] == "forge" {
204 Some(segments[1].as_str())
205 } else if segments.len() == 1 {
206 Some(segments[0].as_str())
207 } else {
208 None
209 };
210
211 if let Some(kind) = kind_str {
212 match kind {
213 "query" => return Some(FunctionKind::Query),
214 "mutation" => return Some(FunctionKind::Mutation),
215 "action" => return Some(FunctionKind::Action),
216 "job" => return Some(FunctionKind::Job),
217 "cron" => return Some(FunctionKind::Cron),
218 "workflow" => return Some(FunctionKind::Workflow),
219 _ => {}
220 }
221 }
222 }
223 None
224}
225
226fn extract_result_type(ty: &syn::Type) -> RustType {
228 let type_str = quote::quote!(#ty).to_string().replace(' ', "");
229
230 if let Some(rest) = type_str.strip_prefix("Result<") {
232 let mut depth = 0;
234 let mut end_idx = 0;
235 for (i, c) in rest.chars().enumerate() {
236 match c {
237 '<' => depth += 1,
238 '>' => {
239 if depth == 0 {
240 end_idx = i;
241 break;
242 }
243 depth -= 1;
244 }
245 ',' if depth == 0 => {
246 end_idx = i;
247 break;
248 }
249 _ => {}
250 }
251 }
252 let inner = &rest[..end_idx];
253 return type_to_rust_type(
254 &syn::parse_str(inner)
255 .unwrap_or_else(|_| syn::parse_str::<syn::Type>("String").unwrap()),
256 );
257 }
258
259 type_to_rust_type(ty)
260}
261
262fn type_to_rust_type(ty: &syn::Type) -> RustType {
264 let type_str = quote::quote!(#ty).to_string().replace(' ', "");
265
266 match type_str.as_str() {
268 "String" | "&str" => RustType::String,
269 "i32" => RustType::I32,
270 "i64" => RustType::I64,
271 "f32" => RustType::F32,
272 "f64" => RustType::F64,
273 "bool" => RustType::Bool,
274 "Uuid" | "uuid::Uuid" => RustType::Uuid,
275 "DateTime<Utc>" | "chrono::DateTime<Utc>" | "chrono::DateTime<chrono::Utc>" => {
276 RustType::DateTime
277 }
278 "NaiveDate" | "chrono::NaiveDate" => RustType::Date,
279 "NaiveTime" | "chrono::NaiveTime" => RustType::Custom("NaiveTime".to_string()),
280 "serde_json::Value" | "Value" => RustType::Json,
281 "Vec<u8>" => RustType::Bytes,
282 _ => {
283 if let Some(inner) = type_str
285 .strip_prefix("Option<")
286 .and_then(|s| s.strip_suffix('>'))
287 {
288 let inner_type = match inner {
289 "String" => RustType::String,
290 "i32" => RustType::I32,
291 "i64" => RustType::I64,
292 "f64" => RustType::F64,
293 "bool" => RustType::Bool,
294 "Uuid" => RustType::Uuid,
295 _ => RustType::String, };
297 return RustType::Option(Box::new(inner_type));
298 }
299
300 if let Some(inner) = type_str
302 .strip_prefix("Vec<")
303 .and_then(|s| s.strip_suffix('>'))
304 {
305 let inner_type = match inner {
306 "String" => RustType::String,
307 "i32" => RustType::I32,
308 "u8" => return RustType::Bytes,
309 _ => RustType::String,
310 };
311 return RustType::Vec(Box::new(inner_type));
312 }
313
314 RustType::Custom(type_str)
316 }
317 }
318}
319
320fn get_table_name_from_attrs(attrs: &[Attribute]) -> Option<String> {
322 for attr in attrs {
323 if attr.path().is_ident("table") {
324 if let Meta::List(list) = &attr.meta {
325 let tokens = list.tokens.to_string();
326 if let Some(value) = extract_name_value(&tokens) {
327 return Some(value);
328 }
329 }
330 }
331 }
332 None
333}
334
335fn get_attribute_string_value(attr: &Attribute) -> Option<String> {
337 if let Meta::NameValue(nv) = &attr.meta {
338 if let Expr::Lit(lit) = &nv.value {
339 if let Lit::Str(s) = &lit.lit {
340 return Some(s.value());
341 }
342 }
343 }
344 None
345}
346
347fn get_doc_comment(attrs: &[Attribute]) -> Option<String> {
349 let docs: Vec<String> = attrs
350 .iter()
351 .filter_map(|attr| {
352 if attr.path().is_ident("doc") {
353 get_attribute_string_value(attr)
354 } else {
355 None
356 }
357 })
358 .collect();
359
360 if docs.is_empty() {
361 None
362 } else {
363 Some(
364 docs.into_iter()
365 .map(|s| s.trim().to_string())
366 .collect::<Vec<_>>()
367 .join("\n"),
368 )
369 }
370}
371
372fn extract_name_value(s: &str) -> Option<String> {
374 let parts: Vec<&str> = s.splitn(2, '=').collect();
375 if parts.len() == 2 {
376 let value = parts[1].trim();
377 if let Some(stripped) = value.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
378 return Some(stripped.to_string());
379 }
380 }
381 None
382}
383
384fn to_snake_case(s: &str) -> String {
386 let mut result = String::new();
387 for (i, c) in s.chars().enumerate() {
388 if c.is_uppercase() {
389 if i > 0 {
390 result.push('_');
391 }
392 result.push(c.to_lowercase().next().unwrap());
393 } else {
394 result.push(c);
395 }
396 }
397 result
398}
399
400fn pluralize(s: &str) -> String {
402 if s.ends_with('s')
403 || s.ends_with("sh")
404 || s.ends_with("ch")
405 || s.ends_with('x')
406 || s.ends_with('z')
407 {
408 format!("{}es", s)
409 } else if let Some(stem) = s.strip_suffix('y') {
410 if !s.ends_with("ay") && !s.ends_with("ey") && !s.ends_with("oy") && !s.ends_with("uy") {
411 format!("{}ies", stem)
412 } else {
413 format!("{}s", s)
414 }
415 } else {
416 format!("{}s", s)
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_parse_model_source() {
426 let source = r#"
427 #[model]
428 struct User {
429 #[id]
430 id: Uuid,
431 email: String,
432 name: Option<String>,
433 #[indexed]
434 created_at: DateTime<Utc>,
435 }
436 "#;
437
438 let registry = SchemaRegistry::new();
439 parse_file(source, ®istry).unwrap();
440
441 let table = registry.get_table("users").unwrap();
442 assert_eq!(table.struct_name, "User");
443 assert_eq!(table.fields.len(), 4);
444 }
445
446 #[test]
447 fn test_parse_enum_source() {
448 let source = r#"
449 #[forge_enum]
450 enum ProjectStatus {
451 Draft,
452 Active,
453 Completed,
454 }
455 "#;
456
457 let registry = SchemaRegistry::new();
458 parse_file(source, ®istry).unwrap();
459
460 let enum_def = registry.get_enum("ProjectStatus").unwrap();
461 assert_eq!(enum_def.variants.len(), 3);
462 }
463
464 #[test]
465 fn test_to_snake_case() {
466 assert_eq!(to_snake_case("UserProfile"), "user_profile");
467 assert_eq!(to_snake_case("ID"), "i_d");
468 assert_eq!(to_snake_case("createdAt"), "created_at");
469 }
470
471 #[test]
472 fn test_pluralize() {
473 assert_eq!(pluralize("user"), "users");
474 assert_eq!(pluralize("category"), "categories");
475 assert_eq!(pluralize("box"), "boxes");
476 assert_eq!(pluralize("address"), "addresses");
477 }
478
479 #[test]
480 fn test_parse_query_function() {
481 let source = r#"
482 #[query]
483 async fn get_user(ctx: QueryContext, id: Uuid) -> Result<User> {
484 todo!()
485 }
486 "#;
487
488 let registry = SchemaRegistry::new();
489 parse_file(source, ®istry).unwrap();
490
491 let func = registry.get_function("get_user").unwrap();
492 assert_eq!(func.name, "get_user");
493 assert_eq!(func.kind, FunctionKind::Query);
494 assert!(func.is_async);
495 }
496
497 #[test]
498 fn test_parse_mutation_function() {
499 let source = r#"
500 #[mutation]
501 async fn create_user(ctx: MutationContext, name: String, email: String) -> Result<User> {
502 todo!()
503 }
504 "#;
505
506 let registry = SchemaRegistry::new();
507 parse_file(source, ®istry).unwrap();
508
509 let func = registry.get_function("create_user").unwrap();
510 assert_eq!(func.name, "create_user");
511 assert_eq!(func.kind, FunctionKind::Mutation);
512 assert_eq!(func.args.len(), 2);
513 assert_eq!(func.args[0].name, "name");
514 assert_eq!(func.args[1].name, "email");
515 }
516
517 #[test]
518 fn test_parse_action_function() {
519 let source = r#"
520 #[action]
521 async fn send_notification(ctx: ActionContext, message: String) -> Result<()> {
522 todo!()
523 }
524 "#;
525
526 let registry = SchemaRegistry::new();
527 parse_file(source, ®istry).unwrap();
528
529 let func = registry.get_function("send_notification").unwrap();
530 assert_eq!(func.name, "send_notification");
531 assert_eq!(func.kind, FunctionKind::Action);
532 }
533}