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}
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    /// Returns the base name (last path segment) of this type.
183    ///
184    /// For simple names like `"String"` this returns `"String"`.
185    /// For qualified paths like `"chrono::DateTime"` this returns `"DateTime"`.
186    pub fn base_name(&self) -> &str {
187        self.name.rsplit("::").next().unwrap_or(&self.name)
188    }
189}
190
191impl fmt::Display for RustType {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "{}", self.name)?;
194        if !self.generics.is_empty() {
195            write!(f, "<")?;
196            for (i, g) in self.generics.iter().enumerate() {
197                if i > 0 {
198                    write!(f, ", ")?;
199                }
200                write!(f, "{g}")?;
201            }
202            write!(f, ">")?;
203        }
204        Ok(())
205    }
206}
207
208/// A single field in a struct or struct variant.
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct FieldDef {
211    pub name: String,
212    pub ty: RustType,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub rename: Option<String>,
215    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
216    pub skip: bool,
217    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
218    pub has_default: bool,
219    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
220    pub flatten: bool,
221}
222
223/// Metadata for a single RPC procedure extracted from a source file.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct Procedure {
226    /// Procedure name derived from the function name
227    pub name: String,
228    /// Query or Mutation
229    pub kind: ProcedureKind,
230    /// Input parameter type; `None` means no input (unit type)
231    pub input: Option<RustType>,
232    /// Return type; `None` means unit return
233    pub output: Option<RustType>,
234    /// Source file this procedure was extracted from
235    pub source_file: PathBuf,
236    /// Doc comment extracted from `///` lines
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub docs: Option<String>,
239    /// Per-procedure timeout in milliseconds (from `timeout = "..."` attribute)
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub timeout_ms: Option<u64>,
242    /// Whether this mutation is marked as idempotent (safe to retry)
243    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
244    pub idempotent: bool,
245}
246
247/// All user-defined struct types found in the scanned source files.
248/// Needed for generating corresponding TypeScript interfaces.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct StructDef {
251    /// Struct name
252    pub name: String,
253    /// Generic type parameter names (e.g. `["T"]`, `["A", "B"]`)
254    #[serde(default, skip_serializing_if = "Vec::is_empty")]
255    pub generics: Vec<String>,
256    /// Named fields with their types
257    pub fields: Vec<FieldDef>,
258    /// Unnamed fields for tuple structs (e.g. `struct UserId(String)`)
259    #[serde(default, skip_serializing_if = "Vec::is_empty")]
260    pub tuple_fields: Vec<RustType>,
261    /// Source file this struct was defined in
262    pub source_file: PathBuf,
263    /// Doc comment extracted from `///` lines
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub docs: Option<String>,
266    /// Container-level `#[serde(rename_all = "...")]`
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub rename_all: Option<RenameRule>,
269}
270
271/// A single variant of a Rust enum.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct EnumVariant {
274    /// Variant name (e.g. `Active`, `Error`)
275    pub name: String,
276    /// Variant kind determines TypeScript representation
277    pub kind: VariantKind,
278    /// Field-level `#[serde(rename = "...")]`
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub rename: Option<String>,
281}
282
283/// The shape of an enum variant's data.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub enum VariantKind {
286    /// Unit variant: `Active` → string literal `"Active"`
287    Unit,
288    /// Tuple variant with a single unnamed field: `Error(String)` → `{ Error: string }`
289    Tuple(Vec<RustType>),
290    /// Struct variant with named fields: `User { name: String }` → `{ User: { name: string } }`
291    Struct(Vec<FieldDef>),
292}
293
294/// Serde enum tagging strategy.
295///
296/// Corresponds to the four representations serde supports:
297/// - `External` (default): `{ "Variant": data }`
298/// - `Internal { tag }`: `{ "tag": "Variant", ...data }`
299/// - `Adjacent { tag, content }`: `{ "tag": "Variant", "content": data }`
300/// - `Untagged`: `data` (no wrapping)
301#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
302pub enum EnumTagging {
303    #[default]
304    External,
305    Internal {
306        tag: String,
307    },
308    Adjacent {
309        tag: String,
310        content: String,
311    },
312    Untagged,
313}
314
315/// All user-defined enum types found in the scanned source files.
316/// Needed for generating corresponding TypeScript union types.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct EnumDef {
319    /// Enum name
320    pub name: String,
321    /// Generic type parameter names (e.g. `["T"]`, `["A", "B"]`)
322    #[serde(default, skip_serializing_if = "Vec::is_empty")]
323    pub generics: Vec<String>,
324    /// Variants of the enum
325    pub variants: Vec<EnumVariant>,
326    /// Source file this enum was defined in
327    pub source_file: PathBuf,
328    /// Doc comment extracted from `///` lines
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub docs: Option<String>,
331    /// Container-level `#[serde(rename_all = "...")]`
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub rename_all: Option<RenameRule>,
334    /// Serde enum tagging strategy
335    #[serde(default)]
336    pub tagging: EnumTagging,
337}
338
339/// Complete manifest of all discovered RPC metadata from a scan.
340#[derive(Debug, Clone, Default, Serialize, Deserialize)]
341pub struct Manifest {
342    pub procedures: Vec<Procedure>,
343    pub structs: Vec<StructDef>,
344    pub enums: Vec<EnumDef>,
345}