Skip to main content

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