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 entries: Vec<_> = WalkDir::new(&input.dir)
45 .into_iter()
46 .filter_map(|e| e.ok())
49 .filter(|e| {
50 if e.path().extension().is_none_or(|ext| ext != "rs") {
51 return false;
52 }
53 let rel = e.path().strip_prefix(&input.dir).unwrap_or(e.path());
54 include_set.is_match(rel) && !exclude_set.is_match(rel)
55 })
56 .collect();
57
58 if entries.is_empty() {
59 anyhow::bail!("No .rs files found in {}", input.dir.display());
60 }
61
62 for entry in entries {
63 let path = entry.path();
64 let file_manifest =
65 parse_file(path).with_context(|| format!("Failed to parse {}", path.display()))?;
66
67 manifest.procedures.extend(file_manifest.procedures);
68 manifest.structs.extend(file_manifest.structs);
69 manifest.enums.extend(file_manifest.enums);
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 Some(extract_rust_type(&pat.ty))
173 });
174
175 let output = match &func.sig.output {
176 ReturnType::Default => None,
177 ReturnType::Type(_, ty) => {
178 let rust_type = extract_rust_type(ty);
179 if rust_type.name == "Result" && !rust_type.generics.is_empty() {
181 Some(rust_type.generics[0].clone())
182 } else {
183 Some(rust_type)
184 }
185 }
186 };
187
188 Some(Procedure {
189 name,
190 kind,
191 input,
192 output,
193 source_file: path.to_path_buf(),
194 docs,
195 })
196}
197
198fn detect_rpc_kind(attrs: &[Attribute]) -> Option<ProcedureKind> {
200 for attr in attrs {
201 if attr.path().is_ident(RPC_QUERY_ATTR) {
202 return Some(ProcedureKind::Query);
203 }
204 if attr.path().is_ident(RPC_MUTATION_ATTR) {
205 return Some(ProcedureKind::Mutation);
206 }
207 }
208 None
209}
210
211fn extract_enum_variants(item_enum: &syn::ItemEnum) -> Vec<EnumVariant> {
213 item_enum
214 .variants
215 .iter()
216 .map(|v| {
217 let name = v.ident.to_string();
218 let rename = serde_attr::parse_rename(&v.attrs);
219 let kind = match &v.fields {
220 syn::Fields::Unit => VariantKind::Unit,
221 syn::Fields::Unnamed(fields) => {
222 let types = fields
223 .unnamed
224 .iter()
225 .map(|f| extract_rust_type(&f.ty))
226 .collect();
227 VariantKind::Tuple(types)
228 }
229 syn::Fields::Named(_) => {
230 let fields = extract_struct_fields(&v.fields);
231 VariantKind::Struct(fields)
232 }
233 };
234 EnumVariant { name, kind, rename }
235 })
236 .collect()
237}
238
239fn has_serde_derive(attrs: &[Attribute]) -> bool {
241 attrs.iter().any(|attr| {
242 if !attr.path().is_ident("derive") {
243 return false;
244 }
245 attr.parse_args_with(
246 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
247 )
248 .is_ok_and(|nested| {
249 nested.iter().any(|path| {
250 path.is_ident("Serialize")
251 || path.segments.last().is_some_and(|s| s.ident == "Serialize")
252 })
253 })
254 })
255}