metaxy_cli/parser/
extract.rs1use std::fs;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
6use syn::{Attribute, File, FnArg, Item, ItemFn, ReturnType};
7use walkdir::WalkDir;
8
9use super::serde as serde_attr;
10use super::types::{extract_rust_type, extract_struct_fields, extract_tuple_fields};
11use crate::config::InputConfig;
12use crate::model::{
13 EnumDef, EnumVariant, Manifest, Procedure, ProcedureKind, StructDef, VariantKind,
14};
15
16const RPC_QUERY_ATTR: &str = "rpc_query";
18const RPC_MUTATION_ATTR: &str = "rpc_mutation";
19
20fn build_glob_set(patterns: &[String]) -> Result<GlobSet> {
22 let mut builder = GlobSetBuilder::new();
23 for pattern in patterns {
24 let glob = GlobBuilder::new(pattern)
25 .literal_separator(false)
26 .build()
27 .with_context(|| format!("Invalid glob pattern: {pattern}"))?;
28 builder.add(glob);
29 }
30 builder.build().context("Failed to build glob set")
31}
32
33pub fn scan_directory(input: &InputConfig) -> Result<Manifest> {
39 let mut manifest = Manifest::default();
40
41 let include_set = build_glob_set(&input.include)?;
42 let exclude_set = build_glob_set(&input.exclude)?;
43
44 let mut file_count = 0;
45 for entry in WalkDir::new(&input.dir)
46 .into_iter()
47 .filter_map(|e| e.ok())
50 .filter(|e| {
51 if e.path().extension().is_none_or(|ext| ext != "rs") {
52 return false;
53 }
54 let rel = e.path().strip_prefix(&input.dir).unwrap_or(e.path());
55 include_set.is_match(rel) && !exclude_set.is_match(rel)
56 })
57 {
58 file_count += 1;
59 let path = entry.path();
60 let file_manifest =
61 parse_file(path).with_context(|| format!("Failed to parse {}", path.display()))?;
62
63 manifest.procedures.extend(file_manifest.procedures);
64 manifest.structs.extend(file_manifest.structs);
65 manifest.enums.extend(file_manifest.enums);
66 }
67
68 if file_count == 0 {
69 anyhow::bail!("No .rs files found in {}", input.dir.display());
70 }
71
72 manifest.procedures.sort_by(|a, b| a.name.cmp(&b.name));
74 manifest.structs.sort_by(|a, b| a.name.cmp(&b.name));
75 manifest.enums.sort_by(|a, b| a.name.cmp(&b.name));
76
77 Ok(manifest)
78}
79
80pub fn parse_file(path: &Path) -> Result<Manifest> {
82 let source =
83 fs::read_to_string(path).with_context(|| format!("Cannot read {}", path.display()))?;
84
85 let syntax: File =
86 syn::parse_file(&source).with_context(|| format!("Syntax error in {}", path.display()))?;
87
88 let mut manifest = Manifest::default();
89
90 for item in &syntax.items {
91 match item {
92 Item::Fn(func) => {
93 if let Some(procedure) = try_extract_procedure(func, path) {
94 manifest.procedures.push(procedure);
95 }
96 }
97 Item::Struct(item_struct) => {
98 if has_serde_derive(&item_struct.attrs) {
99 let generics = extract_generic_param_names(&item_struct.generics);
100 let tuple_fields = extract_tuple_fields(&item_struct.fields);
101 let fields = if tuple_fields.is_empty() {
102 extract_struct_fields(&item_struct.fields)
103 } else {
104 vec![]
105 };
106 let docs = extract_docs(&item_struct.attrs);
107 let rename_all = serde_attr::parse_rename_all(&item_struct.attrs);
108 manifest.structs.push(StructDef {
109 name: item_struct.ident.to_string(),
110 generics,
111 fields,
112 tuple_fields,
113 source_file: path.to_path_buf(),
114 docs,
115 rename_all,
116 });
117 }
118 }
119 Item::Enum(item_enum) => {
120 if has_serde_derive(&item_enum.attrs) {
121 let generics = extract_generic_param_names(&item_enum.generics);
122 let rename_all = serde_attr::parse_rename_all(&item_enum.attrs);
123 let tagging = serde_attr::parse_enum_tagging(&item_enum.attrs);
124 let variants = extract_enum_variants(item_enum);
125 let docs = extract_docs(&item_enum.attrs);
126 manifest.enums.push(EnumDef {
127 name: item_enum.ident.to_string(),
128 generics,
129 variants,
130 source_file: path.to_path_buf(),
131 docs,
132 rename_all,
133 tagging,
134 });
135 }
136 }
137 _ => {}
138 }
139 }
140
141 Ok(manifest)
142}
143
144fn extract_docs(attrs: &[Attribute]) -> Option<String> {
148 let lines: Vec<String> = attrs
149 .iter()
150 .filter_map(|attr| {
151 if !attr.path().is_ident("doc") {
152 return None;
153 }
154 if let syn::Meta::NameValue(nv) = &attr.meta
155 && let syn::Expr::Lit(syn::ExprLit {
156 lit: syn::Lit::Str(s),
157 ..
158 }) = &nv.value
159 {
160 let text = s.value();
161 return Some(text.strip_prefix(' ').unwrap_or(&text).to_string());
163 }
164 None
165 })
166 .collect();
167
168 if lines.is_empty() {
169 None
170 } else {
171 Some(lines.join("\n"))
172 }
173}
174
175fn try_extract_procedure(func: &ItemFn, path: &Path) -> Option<Procedure> {
178 let kind = detect_rpc_kind(&func.attrs)?;
179 let name = func.sig.ident.to_string();
180 let docs = extract_docs(&func.attrs);
181
182 let input = func.sig.inputs.iter().find_map(|arg| {
183 let FnArg::Typed(pat) = arg else { return None };
184 if is_headers_type(&pat.ty) {
186 return None;
187 }
188 Some(extract_rust_type(&pat.ty))
189 });
190
191 let output = match &func.sig.output {
192 ReturnType::Default => None,
193 ReturnType::Type(_, ty) => {
194 let rust_type = extract_rust_type(ty);
195 if rust_type.name == "Result" && !rust_type.generics.is_empty() {
197 rust_type.generics.into_iter().next()
198 } else {
199 Some(rust_type)
200 }
201 }
202 };
203
204 let timeout_ms = extract_timeout_ms(&func.attrs);
205 let idempotent = extract_idempotent(&func.attrs);
206
207 Some(Procedure {
208 name,
209 kind,
210 input,
211 output,
212 source_file: path.to_path_buf(),
213 docs,
214 timeout_ms,
215 idempotent,
216 })
217}
218
219fn detect_rpc_kind(attrs: &[Attribute]) -> Option<ProcedureKind> {
221 for attr in attrs {
222 if attr.path().is_ident(RPC_QUERY_ATTR) {
223 return Some(ProcedureKind::Query);
224 }
225 if attr.path().is_ident(RPC_MUTATION_ATTR) {
226 return Some(ProcedureKind::Mutation);
227 }
228 }
229 None
230}
231
232fn extract_generic_param_names(generics: &syn::Generics) -> Vec<String> {
236 generics
237 .params
238 .iter()
239 .filter_map(|p| match p {
240 syn::GenericParam::Type(t) => Some(t.ident.to_string()),
241 _ => None,
242 })
243 .collect()
244}
245
246fn extract_enum_variants(item_enum: &syn::ItemEnum) -> Vec<EnumVariant> {
248 item_enum
249 .variants
250 .iter()
251 .map(|v| {
252 let name = v.ident.to_string();
253 let rename = serde_attr::parse_rename(&v.attrs);
254 let kind = match &v.fields {
255 syn::Fields::Unit => VariantKind::Unit,
256 syn::Fields::Unnamed(fields) => {
257 let types = fields
258 .unnamed
259 .iter()
260 .map(|f| extract_rust_type(&f.ty))
261 .collect();
262 VariantKind::Tuple(types)
263 }
264 syn::Fields::Named(_) => {
265 let fields = extract_struct_fields(&v.fields);
266 VariantKind::Struct(fields)
267 }
268 };
269 EnumVariant { name, kind, rename }
270 })
271 .collect()
272}
273
274fn is_headers_type(ty: &syn::Type) -> bool {
279 if let syn::Type::Path(type_path) = ty
280 && let Some(segment) = type_path.path.segments.last()
281 {
282 return segment.ident == "Headers";
283 }
284 false
285}
286
287fn extract_timeout_ms(attrs: &[Attribute]) -> Option<u64> {
292 for attr in attrs {
293 if !attr.path().is_ident(RPC_QUERY_ATTR) && !attr.path().is_ident(RPC_MUTATION_ATTR) {
294 continue;
295 }
296 let Ok(parsed) = attr.parse_args_with(
297 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
298 ) else {
299 continue;
300 };
301 for meta in &parsed {
302 if let syn::Meta::NameValue(nv) = meta
303 && nv.path.is_ident("timeout")
304 && let syn::Expr::Lit(syn::ExprLit {
305 lit: syn::Lit::Str(s),
306 ..
307 }) = &nv.value
308 {
309 return parse_duration_to_ms(&s.value());
310 }
311 }
312 }
313 None
314}
315
316fn parse_duration_to_ms(s: &str) -> Option<u64> {
320 let (num_str, multiplier) = if let Some(n) = s.strip_suffix('s') {
321 (n, 1_000)
322 } else if let Some(n) = s.strip_suffix('m') {
323 (n, 60_000)
324 } else if let Some(n) = s.strip_suffix('h') {
325 (n, 3_600_000)
326 } else if let Some(n) = s.strip_suffix('d') {
327 (n, 86_400_000)
328 } else {
329 return None;
330 };
331 let num: u64 = num_str.parse().ok()?;
332 if num == 0 {
333 return None;
334 }
335 Some(num * multiplier)
336}
337
338fn extract_idempotent(attrs: &[Attribute]) -> bool {
343 for attr in attrs {
344 if !attr.path().is_ident(RPC_MUTATION_ATTR) {
345 continue;
346 }
347 let Ok(parsed) = attr.parse_args_with(
348 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
349 ) else {
350 continue;
351 };
352 for meta in &parsed {
353 if let syn::Meta::Path(path) = meta
354 && path.is_ident("idempotent")
355 {
356 return true;
357 }
358 }
359 }
360 false
361}
362
363fn has_serde_derive(attrs: &[Attribute]) -> bool {
365 attrs.iter().any(|attr| {
366 if !attr.path().is_ident("derive") {
367 return false;
368 }
369 attr.parse_args_with(
370 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
371 )
372 .is_ok_and(|nested| {
373 nested.iter().any(|path| {
374 path.is_ident("Serialize")
375 || path.segments.last().is_some_and(|s| s.ident == "Serialize")
376 })
377 })
378 })
379}