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