vercel_rpc_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::types::{extract_rust_type, extract_struct_fields};
10use crate::config::InputConfig;
11use crate::model::{
12 EnumDef, EnumVariant, Manifest, Procedure, ProcedureKind, StructDef, VariantKind,
13};
14
15const RPC_QUERY_ATTR: &str = "rpc_query";
17const RPC_MUTATION_ATTR: &str = "rpc_mutation";
18
19fn build_glob_set(patterns: &[String]) -> Result<GlobSet> {
21 let mut builder = GlobSetBuilder::new();
22 for pattern in patterns {
23 let glob = GlobBuilder::new(pattern)
24 .literal_separator(false)
25 .build()
26 .with_context(|| format!("Invalid glob pattern: {pattern}"))?;
27 builder.add(glob);
28 }
29 builder.build().context("Failed to build glob set")
30}
31
32pub fn scan_directory(input: &InputConfig) -> Result<Manifest> {
38 let mut manifest = Manifest::default();
39
40 let include_set = build_glob_set(&input.include)?;
41 let exclude_set = build_glob_set(&input.exclude)?;
42
43 let entries: Vec<_> = WalkDir::new(&input.dir)
44 .into_iter()
45 .filter_map(|e| e.ok())
48 .filter(|e| {
49 if e.path().extension().is_none_or(|ext| ext != "rs") {
50 return false;
51 }
52 let rel = e.path().strip_prefix(&input.dir).unwrap_or(e.path());
53 include_set.is_match(rel) && !exclude_set.is_match(rel)
54 })
55 .collect();
56
57 if entries.is_empty() {
58 anyhow::bail!("No .rs files found in {}", input.dir.display());
59 }
60
61 for entry in entries {
62 let path = entry.path();
63 let file_manifest =
64 parse_file(path).with_context(|| format!("Failed to parse {}", path.display()))?;
65
66 manifest.procedures.extend(file_manifest.procedures);
67 manifest.structs.extend(file_manifest.structs);
68 manifest.enums.extend(file_manifest.enums);
69 }
70
71 manifest.procedures.sort_by(|a, b| a.name.cmp(&b.name));
73 manifest.structs.sort_by(|a, b| a.name.cmp(&b.name));
74 manifest.enums.sort_by(|a, b| a.name.cmp(&b.name));
75
76 Ok(manifest)
77}
78
79pub fn parse_file(path: &Path) -> Result<Manifest> {
81 let source =
82 fs::read_to_string(path).with_context(|| format!("Cannot read {}", path.display()))?;
83
84 let syntax: File =
85 syn::parse_file(&source).with_context(|| format!("Syntax error in {}", path.display()))?;
86
87 let mut manifest = Manifest::default();
88
89 for item in &syntax.items {
90 match item {
91 Item::Fn(func) => {
92 if let Some(procedure) = try_extract_procedure(func, path) {
93 manifest.procedures.push(procedure);
94 }
95 }
96 Item::Struct(item_struct) => {
97 if has_serde_derive(&item_struct.attrs) {
98 let fields = extract_struct_fields(&item_struct.fields);
99 let docs = extract_docs(&item_struct.attrs);
100 manifest.structs.push(StructDef {
101 name: item_struct.ident.to_string(),
102 fields,
103 source_file: path.to_path_buf(),
104 docs,
105 });
106 }
107 }
108 Item::Enum(item_enum) => {
109 if has_serde_derive(&item_enum.attrs) {
110 let variants = extract_enum_variants(item_enum);
111 let docs = extract_docs(&item_enum.attrs);
112 manifest.enums.push(EnumDef {
113 name: item_enum.ident.to_string(),
114 variants,
115 source_file: path.to_path_buf(),
116 docs,
117 });
118 }
119 }
120 _ => {}
121 }
122 }
123
124 Ok(manifest)
125}
126
127fn extract_docs(attrs: &[Attribute]) -> Option<String> {
131 let lines: Vec<String> = attrs
132 .iter()
133 .filter_map(|attr| {
134 if !attr.path().is_ident("doc") {
135 return None;
136 }
137 if let syn::Meta::NameValue(nv) = &attr.meta
138 && let syn::Expr::Lit(syn::ExprLit {
139 lit: syn::Lit::Str(s),
140 ..
141 }) = &nv.value
142 {
143 let text = s.value();
144 return Some(text.strip_prefix(' ').unwrap_or(&text).to_string());
146 }
147 None
148 })
149 .collect();
150
151 if lines.is_empty() {
152 None
153 } else {
154 Some(lines.join("\n"))
155 }
156}
157
158fn try_extract_procedure(func: &ItemFn, path: &Path) -> Option<Procedure> {
161 let kind = detect_rpc_kind(&func.attrs)?;
162 let name = func.sig.ident.to_string();
163 let docs = extract_docs(&func.attrs);
164
165 let input = func.sig.inputs.iter().find_map(|arg| {
166 let FnArg::Typed(pat) = arg else { return None };
167 Some(extract_rust_type(&pat.ty))
168 });
169
170 let output = match &func.sig.output {
171 ReturnType::Default => None,
172 ReturnType::Type(_, ty) => {
173 let rust_type = extract_rust_type(ty);
174 if rust_type.name == "Result" && !rust_type.generics.is_empty() {
176 Some(rust_type.generics[0].clone())
177 } else {
178 Some(rust_type)
179 }
180 }
181 };
182
183 Some(Procedure {
184 name,
185 kind,
186 input,
187 output,
188 source_file: path.to_path_buf(),
189 docs,
190 })
191}
192
193fn detect_rpc_kind(attrs: &[Attribute]) -> Option<ProcedureKind> {
195 for attr in attrs {
196 if attr.path().is_ident(RPC_QUERY_ATTR) {
197 return Some(ProcedureKind::Query);
198 }
199 if attr.path().is_ident(RPC_MUTATION_ATTR) {
200 return Some(ProcedureKind::Mutation);
201 }
202 }
203 None
204}
205
206fn extract_enum_variants(item_enum: &syn::ItemEnum) -> Vec<EnumVariant> {
208 item_enum
209 .variants
210 .iter()
211 .map(|v| {
212 let name = v.ident.to_string();
213 let kind = match &v.fields {
214 syn::Fields::Unit => VariantKind::Unit,
215 syn::Fields::Unnamed(fields) => {
216 let types = fields
217 .unnamed
218 .iter()
219 .map(|f| extract_rust_type(&f.ty))
220 .collect();
221 VariantKind::Tuple(types)
222 }
223 syn::Fields::Named(fields) => {
224 let named = fields
225 .named
226 .iter()
227 .filter_map(|f| {
228 let field_name = f.ident.as_ref()?.to_string();
229 let ty = extract_rust_type(&f.ty);
230 Some((field_name, ty))
231 })
232 .collect();
233 VariantKind::Struct(named)
234 }
235 };
236 EnumVariant { name, kind }
237 })
238 .collect()
239}
240
241fn has_serde_derive(attrs: &[Attribute]) -> bool {
243 attrs.iter().any(|attr| {
244 if !attr.path().is_ident("derive") {
245 return false;
246 }
247 attr.parse_args_with(
248 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
249 )
250 .is_ok_and(|nested| {
251 nested.iter().any(|path| {
252 path.is_ident("Serialize")
253 || path.segments.last().is_some_and(|s| s.ident == "Serialize")
254 })
255 })
256 })
257}