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::serde as serde_attr;
10use super::types::{extract_rust_type, extract_struct_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 fields = extract_struct_fields(&item_struct.fields);
100 let docs = extract_docs(&item_struct.attrs);
101 let rename_all = serde_attr::parse_rename_all(&item_struct.attrs);
102 manifest.structs.push(StructDef {
103 name: item_struct.ident.to_string(),
104 fields,
105 source_file: path.to_path_buf(),
106 docs,
107 rename_all,
108 });
109 }
110 }
111 Item::Enum(item_enum) => {
112 if has_serde_derive(&item_enum.attrs) {
113 let rename_all = serde_attr::parse_rename_all(&item_enum.attrs);
114 let tagging = serde_attr::parse_enum_tagging(&item_enum.attrs);
115 let variants = extract_enum_variants(item_enum);
116 let docs = extract_docs(&item_enum.attrs);
117 manifest.enums.push(EnumDef {
118 name: item_enum.ident.to_string(),
119 variants,
120 source_file: path.to_path_buf(),
121 docs,
122 rename_all,
123 tagging,
124 });
125 }
126 }
127 _ => {}
128 }
129 }
130
131 Ok(manifest)
132}
133
134fn extract_docs(attrs: &[Attribute]) -> Option<String> {
138 let lines: Vec<String> = attrs
139 .iter()
140 .filter_map(|attr| {
141 if !attr.path().is_ident("doc") {
142 return None;
143 }
144 if let syn::Meta::NameValue(nv) = &attr.meta
145 && let syn::Expr::Lit(syn::ExprLit {
146 lit: syn::Lit::Str(s),
147 ..
148 }) = &nv.value
149 {
150 let text = s.value();
151 return Some(text.strip_prefix(' ').unwrap_or(&text).to_string());
153 }
154 None
155 })
156 .collect();
157
158 if lines.is_empty() {
159 None
160 } else {
161 Some(lines.join("\n"))
162 }
163}
164
165fn try_extract_procedure(func: &ItemFn, path: &Path) -> Option<Procedure> {
168 let kind = detect_rpc_kind(&func.attrs)?;
169 let name = func.sig.ident.to_string();
170 let docs = extract_docs(&func.attrs);
171
172 let input = func.sig.inputs.iter().find_map(|arg| {
173 let FnArg::Typed(pat) = arg else { return None };
174 if is_headers_type(&pat.ty) {
176 return None;
177 }
178 Some(extract_rust_type(&pat.ty))
179 });
180
181 let output = match &func.sig.output {
182 ReturnType::Default => None,
183 ReturnType::Type(_, ty) => {
184 let rust_type = extract_rust_type(ty);
185 if rust_type.name == "Result" && !rust_type.generics.is_empty() {
187 rust_type.generics.into_iter().next()
188 } else {
189 Some(rust_type)
190 }
191 }
192 };
193
194 Some(Procedure {
195 name,
196 kind,
197 input,
198 output,
199 source_file: path.to_path_buf(),
200 docs,
201 })
202}
203
204fn detect_rpc_kind(attrs: &[Attribute]) -> Option<ProcedureKind> {
206 for attr in attrs {
207 if attr.path().is_ident(RPC_QUERY_ATTR) {
208 return Some(ProcedureKind::Query);
209 }
210 if attr.path().is_ident(RPC_MUTATION_ATTR) {
211 return Some(ProcedureKind::Mutation);
212 }
213 }
214 None
215}
216
217fn extract_enum_variants(item_enum: &syn::ItemEnum) -> Vec<EnumVariant> {
219 item_enum
220 .variants
221 .iter()
222 .map(|v| {
223 let name = v.ident.to_string();
224 let rename = serde_attr::parse_rename(&v.attrs);
225 let kind = match &v.fields {
226 syn::Fields::Unit => VariantKind::Unit,
227 syn::Fields::Unnamed(fields) => {
228 let types = fields
229 .unnamed
230 .iter()
231 .map(|f| extract_rust_type(&f.ty))
232 .collect();
233 VariantKind::Tuple(types)
234 }
235 syn::Fields::Named(_) => {
236 let fields = extract_struct_fields(&v.fields);
237 VariantKind::Struct(fields)
238 }
239 };
240 EnumVariant { name, kind, rename }
241 })
242 .collect()
243}
244
245fn is_headers_type(ty: &syn::Type) -> bool {
250 if let syn::Type::Path(type_path) = ty
251 && let Some(segment) = type_path.path.segments.last()
252 {
253 return segment.ident == "Headers";
254 }
255 false
256}
257
258fn has_serde_derive(attrs: &[Attribute]) -> bool {
260 attrs.iter().any(|attr| {
261 if !attr.path().is_ident("derive") {
262 return false;
263 }
264 attr.parse_args_with(
265 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
266 )
267 .is_ok_and(|nested| {
268 nested.iter().any(|path| {
269 path.is_ident("Serialize")
270 || path.segments.last().is_some_and(|s| s.ident == "Serialize")
271 })
272 })
273 })
274}