Skip to main content

ts_rs_macros/attr/
mod.rs

1use std::collections::HashMap;
2
3pub use field::*;
4use proc_macro2::TokenTree;
5use quote::quote;
6pub use r#enum::*;
7pub use r#struct::*;
8use syn::{
9    parenthesized,
10    parse::{Parse, ParseStream},
11    punctuated::Punctuated,
12    Error, Expr, Ident, Lit, Path, Result, Token, WherePredicate,
13};
14pub use variant::*;
15mod r#enum;
16mod field;
17mod r#struct;
18mod variant;
19
20#[derive(Copy, Clone, Debug)]
21pub enum Inflection {
22    Lower,
23    Upper,
24    Camel,
25    Snake,
26    Pascal,
27    ScreamingSnake,
28    Kebab,
29    ScreamingKebab,
30}
31
32pub(super) trait Attr: Default {
33    type Item;
34
35    fn merge(self, other: Self) -> Self;
36    fn assert_validity(&self, item: &Self::Item) -> Result<()>;
37}
38
39pub(super) trait ContainerAttr: Attr {
40    fn crate_rename(&self) -> Path;
41}
42
43#[derive(Default)]
44pub(super) struct Serde<T>(pub T)
45where
46    T: Attr;
47
48impl<T> Serde<T>
49where
50    T: Attr,
51{
52    pub fn merge(self, other: Self) -> Self {
53        Self(self.0.merge(other.0))
54    }
55}
56
57impl Inflection {
58    pub fn apply(self, string: &str) -> String {
59        match self {
60            Inflection::Lower => string.to_lowercase(),
61            Inflection::Upper => string.to_uppercase(),
62            Inflection::Camel => {
63                let pascal = Inflection::apply(Inflection::Pascal, string);
64                pascal[..1].to_ascii_lowercase() + &pascal[1..]
65            }
66            Inflection::Snake => {
67                let mut s = String::new();
68
69                for (i, ch) in string.char_indices() {
70                    if ch.is_uppercase() && i != 0 {
71                        s.push('_');
72                    }
73                    s.push(ch.to_ascii_lowercase());
74                }
75
76                s
77            }
78            Inflection::Pascal => {
79                let mut s = String::with_capacity(string.len());
80
81                let mut capitalize = true;
82                for c in string.chars() {
83                    if c == '_' {
84                        capitalize = true;
85                        continue;
86                    } else if capitalize {
87                        s.push(c.to_ascii_uppercase());
88                        capitalize = false;
89                    } else {
90                        s.push(c)
91                    }
92                }
93
94                s
95            }
96            Inflection::ScreamingSnake => Self::Snake.apply(string).to_ascii_uppercase(),
97            Inflection::Kebab => Self::Snake.apply(string).replace('_', "-"),
98            Inflection::ScreamingKebab => Self::Kebab.apply(string).to_ascii_uppercase(),
99        }
100    }
101}
102
103fn skip_until_next_comma(input: ParseStream) -> proc_macro2::TokenStream {
104    input
105        .step(|cursor| {
106            let mut stuff = quote!();
107            let mut rest = *cursor;
108
109            if let Some((TokenTree::Punct(ref punct), _)) = cursor.token_tree() {
110                if punct.as_char() == ',' {
111                    return Ok((stuff, rest));
112                }
113            }
114
115            while let Some((tt, next)) = rest.token_tree() {
116                if let Some((TokenTree::Punct(punct), _)) = next.token_tree() {
117                    if punct.as_char() == ',' {
118                        return Ok((stuff, next));
119                    }
120                }
121
122                rest = next;
123                stuff = quote!(#stuff #tt)
124            }
125
126            Ok((stuff, rest))
127        })
128        .unwrap()
129}
130
131fn parse_assign_expr(input: ParseStream) -> Result<Expr> {
132    input.parse::<Token![=]>()?;
133    Expr::parse(input)
134}
135
136fn parse_assign_str(input: ParseStream) -> Result<String> {
137    input.parse::<Token![=]>()?;
138    match Lit::parse(input)? {
139        Lit::Str(string) => Ok(string.value()),
140        other => Err(Error::new(other.span(), "expected string")),
141    }
142}
143
144fn parse_optional_assign_str(input: ParseStream) -> Result<Option<String>> {
145    if input.peek(Token![=]) {
146        Some(parse_assign_str(input))
147    } else {
148        None
149    }
150    .transpose()
151}
152
153fn parse_concrete(input: ParseStream) -> Result<HashMap<syn::Ident, syn::Type>> {
154    struct Concrete {
155        ident: syn::Ident,
156        _equal_token: Token![=],
157        ty: syn::Type,
158    }
159
160    impl Parse for Concrete {
161        fn parse(input: ParseStream) -> Result<Self> {
162            Ok(Self {
163                ident: input.parse()?,
164                _equal_token: input.parse()?,
165                ty: input.parse()?,
166            })
167        }
168    }
169
170    let content;
171    parenthesized!(content in input);
172
173    Ok(
174        Punctuated::<Concrete, Token![,]>::parse_terminated(&content)?
175            .into_iter()
176            .map(|concrete| (concrete.ident, concrete.ty))
177            .collect(),
178    )
179}
180
181fn parse_assign_inflection(input: ParseStream) -> Result<Inflection> {
182    input.parse::<Token![=]>()?;
183
184    match Lit::parse(input)? {
185        Lit::Str(string) => Ok(match &*string.value() {
186            "lowercase" => Inflection::Lower,
187            "UPPERCASE" => Inflection::Upper,
188            "camelCase" => Inflection::Camel,
189            "snake_case" => Inflection::Snake,
190            "PascalCase" => Inflection::Pascal,
191            "SCREAMING_SNAKE_CASE" => Inflection::ScreamingSnake,
192            "kebab-case" => Inflection::Kebab,
193            "SCREAMING-KEBAB-CASE" => Inflection::ScreamingKebab,
194            other => {
195                syn_err!(
196                    string.span();
197                    r#"Value "{other}" is not valid for "rename_all". Accepted values are: "lowercase", "UPPERCASE", "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", "kebab-case" and "SCREAMING-KEBAB-CASE""#
198                )
199            }
200        }),
201        other => Err(Error::new(other.span(), "expected string")),
202    }
203}
204
205fn parse_assign_from_str<T>(input: ParseStream) -> Result<T>
206where
207    T: Parse,
208{
209    input.parse::<Token![=]>()?;
210    match Lit::parse(input)? {
211        Lit::Str(string) => string.parse(),
212        other => Err(Error::new(other.span(), "expected string")),
213    }
214}
215
216fn parse_bound(input: ParseStream) -> Result<Vec<WherePredicate>> {
217    input.parse::<Token![=]>()?;
218    match Lit::parse(input)? {
219        Lit::Str(string) => {
220            let parser = Punctuated::<WherePredicate, Token![,]>::parse_terminated;
221
222            Ok(string.parse_with(parser)?.into_iter().collect())
223        }
224        other => Err(Error::new(other.span(), "expected string")),
225    }
226}
227
228fn parse_repr(input: ParseStream) -> Result<Repr> {
229    let content;
230    parenthesized!(content in input);
231
232    content.parse::<Token![enum]>()?;
233
234    if content.is_empty() {
235        return Ok(Repr::Int);
236    }
237
238    content.parse::<Token![=]>()?;
239
240    let span = content.span();
241    let ident = content.parse::<Ident>()?;
242    match ident.to_string().as_str() {
243        "name" => Ok(Repr::Name),
244        _ => syn_err!(span; "expected `name`"),
245    }
246}