Skip to main content

vercel_rpc_cli/
model.rs

1use std::fmt;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8/// The kind of RPC procedure, determined by the macro attribute.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum ProcedureKind {
12    Query,
13    Mutation,
14}
15
16/// Serde `rename_all` naming convention.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum RenameRule {
19    #[serde(rename = "camelCase")]
20    CamelCase,
21    #[serde(rename = "snake_case")]
22    SnakeCase,
23    #[serde(rename = "PascalCase")]
24    PascalCase,
25    #[serde(rename = "SCREAMING_SNAKE_CASE")]
26    ScreamingSnakeCase,
27    #[serde(rename = "kebab-case")]
28    KebabCase,
29    #[serde(rename = "SCREAMING-KEBAB-CASE")]
30    ScreamingKebabCase,
31    #[serde(rename = "lowercase")]
32    Lowercase,
33    #[serde(rename = "UPPERCASE")]
34    Uppercase,
35}
36
37impl RenameRule {
38    /// Transforms a name according to this rename rule.
39    pub fn apply(&self, input: &str) -> String {
40        if input.is_empty() {
41            return String::new();
42        }
43        let words = split_words(input);
44        match self {
45            RenameRule::CamelCase => {
46                let mut result = String::new();
47                for (i, word) in words.iter().enumerate() {
48                    if i == 0 {
49                        result.push_str(&word.to_lowercase());
50                    } else {
51                        capitalize_into(word, &mut result);
52                    }
53                }
54                result
55            }
56            RenameRule::PascalCase => {
57                let mut result = String::new();
58                for word in &words {
59                    capitalize_into(word, &mut result);
60                }
61                result
62            }
63            RenameRule::SnakeCase => join_mapped(&words, "_", str::to_lowercase),
64            RenameRule::ScreamingSnakeCase => join_mapped(&words, "_", str::to_uppercase),
65            RenameRule::KebabCase => join_mapped(&words, "-", str::to_lowercase),
66            RenameRule::ScreamingKebabCase => join_mapped(&words, "-", str::to_uppercase),
67            RenameRule::Lowercase => join_mapped(&words, "", str::to_lowercase),
68            RenameRule::Uppercase => join_mapped(&words, "", str::to_uppercase),
69        }
70    }
71}
72
73/// Joins words with a separator, applying a transform to each word without intermediate allocation.
74fn join_mapped(words: &[String], sep: &str, f: fn(&str) -> String) -> String {
75    let mut out = String::new();
76    for (i, w) in words.iter().enumerate() {
77        if i > 0 {
78            out.push_str(sep);
79        }
80        out.push_str(&f(w));
81    }
82    out
83}
84
85/// Pushes a word capitalized (first char uppercase, rest lowercase) into `out`.
86fn capitalize_into(word: &str, out: &mut String) {
87    let mut chars = word.chars();
88    if let Some(first) = chars.next() {
89        out.extend(first.to_uppercase());
90        out.push_str(&chars.as_str().to_lowercase());
91    }
92}
93
94/// Splits a name into words, handling snake_case, PascalCase, and acronyms.
95///
96/// Examples:
97/// - `"first_name"` → `["first", "name"]`
98/// - `"MyVariant"` → `["My", "Variant"]`
99/// - `"HTTPSPort"` → `["HTTPS", "Port"]`
100/// - `"IOError"` → `["IO", "Error"]`
101fn split_words(input: &str) -> Vec<String> {
102    let mut words = Vec::new();
103    for segment in input.split('_') {
104        if segment.is_empty() {
105            continue;
106        }
107        let chars: Vec<char> = segment.chars().collect();
108        let mut current = String::new();
109        for i in 0..chars.len() {
110            let ch = chars[i];
111            if ch.is_uppercase() && !current.is_empty() {
112                let prev_lower = current.chars().last().is_some_and(|c| c.is_lowercase());
113                let next_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());
114                // Split when: previous char was lowercase (camelCase boundary),
115                // or next char is lowercase (end of acronym, e.g. "S" in "HTTPSPort")
116                if prev_lower || next_lower {
117                    words.push(current);
118                    current = String::new();
119                }
120            }
121            current.push(ch);
122        }
123        if !current.is_empty() {
124            words.push(current);
125        }
126    }
127    words
128}
129
130/// Error returned when parsing an unknown `rename_all` rule string.
131#[derive(Debug, Error)]
132#[error("unknown rename_all rule: `{0}`")]
133pub struct UnknownRenameRule(String);
134
135impl FromStr for RenameRule {
136    type Err = UnknownRenameRule;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        match s {
140            "camelCase" => Ok(RenameRule::CamelCase),
141            "snake_case" => Ok(RenameRule::SnakeCase),
142            "PascalCase" => Ok(RenameRule::PascalCase),
143            "SCREAMING_SNAKE_CASE" => Ok(RenameRule::ScreamingSnakeCase),
144            "kebab-case" => Ok(RenameRule::KebabCase),
145            "SCREAMING-KEBAB-CASE" => Ok(RenameRule::ScreamingKebabCase),
146            "lowercase" => Ok(RenameRule::Lowercase),
147            "UPPERCASE" => Ok(RenameRule::Uppercase),
148            _ => Err(UnknownRenameRule(s.to_owned())),
149        }
150    }
151}
152
153/// A single Rust type reference extracted from source code.
154///
155/// Preserves the full path as written (e.g. `Vec<String>`, `MyStruct`).
156/// Generic parameters are stored recursively for accurate TS mapping.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct RustType {
159    /// The base type name (e.g. "Vec", "String", "MyStruct")
160    pub name: String,
161    /// Generic type parameters, if any (e.g. `Vec<String>` → [RustType("String")])
162    pub generics: Vec<RustType>,
163}
164
165impl RustType {
166    /// Creates a simple type with no generic parameters (e.g. `String`, `i32`).
167    pub fn simple(name: impl Into<String>) -> Self {
168        Self {
169            name: name.into(),
170            generics: vec![],
171        }
172    }
173
174    /// Creates a generic type with the given type parameters (e.g. `Vec<String>`).
175    pub fn with_generics(name: impl Into<String>, generics: Vec<RustType>) -> Self {
176        Self {
177            name: name.into(),
178            generics,
179        }
180    }
181}
182
183impl fmt::Display for RustType {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(f, "{}", self.name)?;
186        if !self.generics.is_empty() {
187            write!(f, "<")?;
188            for (i, g) in self.generics.iter().enumerate() {
189                if i > 0 {
190                    write!(f, ", ")?;
191                }
192                write!(f, "{g}")?;
193            }
194            write!(f, ">")?;
195        }
196        Ok(())
197    }
198}
199
200/// A single field in a struct or struct variant.
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct FieldDef {
203    pub name: String,
204    pub ty: RustType,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub rename: Option<String>,
207    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
208    pub skip: bool,
209    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
210    pub has_default: bool,
211}
212
213/// Metadata for a single RPC procedure extracted from a source file.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Procedure {
216    /// Procedure name derived from the function name
217    pub name: String,
218    /// Query or Mutation
219    pub kind: ProcedureKind,
220    /// Input parameter type; `None` means no input (unit type)
221    pub input: Option<RustType>,
222    /// Return type; `None` means unit return
223    pub output: Option<RustType>,
224    /// Source file this procedure was extracted from
225    pub source_file: PathBuf,
226    /// Doc comment extracted from `///` lines
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub docs: Option<String>,
229}
230
231/// All user-defined struct types found in the scanned source files.
232/// Needed for generating corresponding TypeScript interfaces.
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct StructDef {
235    /// Struct name
236    pub name: String,
237    /// Named fields with their types
238    pub fields: Vec<FieldDef>,
239    /// Source file this struct was defined in
240    pub source_file: PathBuf,
241    /// Doc comment extracted from `///` lines
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub docs: Option<String>,
244    /// Container-level `#[serde(rename_all = "...")]`
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub rename_all: Option<RenameRule>,
247}
248
249/// A single variant of a Rust enum.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct EnumVariant {
252    /// Variant name (e.g. `Active`, `Error`)
253    pub name: String,
254    /// Variant kind determines TypeScript representation
255    pub kind: VariantKind,
256    /// Field-level `#[serde(rename = "...")]`
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub rename: Option<String>,
259}
260
261/// The shape of an enum variant's data.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub enum VariantKind {
264    /// Unit variant: `Active` → string literal `"Active"`
265    Unit,
266    /// Tuple variant with a single unnamed field: `Error(String)` → `{ Error: string }`
267    Tuple(Vec<RustType>),
268    /// Struct variant with named fields: `User { name: String }` → `{ User: { name: string } }`
269    Struct(Vec<FieldDef>),
270}
271
272/// All user-defined enum types found in the scanned source files.
273/// Needed for generating corresponding TypeScript union types.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct EnumDef {
276    /// Enum name
277    pub name: String,
278    /// Variants of the enum
279    pub variants: Vec<EnumVariant>,
280    /// Source file this enum was defined in
281    pub source_file: PathBuf,
282    /// Doc comment extracted from `///` lines
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub docs: Option<String>,
285    /// Container-level `#[serde(rename_all = "...")]`
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub rename_all: Option<RenameRule>,
288}
289
290/// Complete manifest of all discovered RPC metadata from a scan.
291#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292pub struct Manifest {
293    pub procedures: Vec<Procedure>,
294    pub structs: Vec<StructDef>,
295    pub enums: Vec<EnumDef>,
296}