musli_macros/internals/
name.rs

1use core::fmt;
2use core::mem::take;
3
4use crate::expander::{NameMethod, UnsizedMethod};
5
6#[derive(Default, Debug, Clone, Copy)]
7#[allow(clippy::enum_variant_names)]
8pub(crate) enum NameAll {
9    /// Fields are named by index.
10    #[default]
11    Index,
12    /// Fields are named by their original name.
13    Name,
14    PascalCase,
15    CamelCase,
16    SnakeCase,
17    ScreamingSnakeCase,
18    KebabCase,
19    ScreamingKebabCase,
20}
21
22impl NameAll {
23    pub(crate) const ALL: &'static [Self] = &[
24        Self::Index,
25        Self::Name,
26        Self::PascalCase,
27        Self::CamelCase,
28        Self::SnakeCase,
29        Self::ScreamingSnakeCase,
30        Self::KebabCase,
31        Self::ScreamingKebabCase,
32    ];
33
34    pub(crate) fn ty(&self) -> syn::Type {
35        match self {
36            NameAll::Index => syn::parse_quote! { usize },
37            _ => syn::parse_quote! { str },
38        }
39    }
40
41    pub(crate) fn name_method(&self) -> NameMethod {
42        match self {
43            NameAll::Index => NameMethod::Value,
44            _ => NameMethod::Unsized(UnsizedMethod::Default),
45        }
46    }
47
48    pub(crate) fn parse(input: &str) -> Option<Self> {
49        match input {
50            "index" => Some(Self::Index),
51            "name" => Some(Self::Name),
52            "PascalCase" => Some(Self::PascalCase),
53            "camelCase" => Some(Self::CamelCase),
54            "snake_case" => Some(Self::SnakeCase),
55            "SCREAMING_SNAKE_CASE" => Some(Self::ScreamingSnakeCase),
56            "kebab-case" => Some(Self::KebabCase),
57            "SCREAMING-KEBAB-CASE" => Some(Self::ScreamingKebabCase),
58            _ => None,
59        }
60    }
61
62    /// Apply the given rename to the input string.
63    pub(crate) fn apply(&self, input: &str) -> Option<String> {
64        let feed: fn(output: &mut String, open: bool, count: usize, c: char) = match self {
65            Self::Index => return None,
66            Self::Name => return Some(input.to_string()),
67            Self::PascalCase => |output, open, _, c| {
68                if open {
69                    output.extend(c.to_uppercase());
70                } else {
71                    output.extend(c.to_lowercase());
72                }
73            },
74            Self::CamelCase => |output, open, count, c| {
75                if open && count > 0 {
76                    output.extend(c.to_uppercase());
77                } else {
78                    output.extend(c.to_lowercase());
79                }
80            },
81            Self::SnakeCase => |output, _, _, c| {
82                output.extend(c.to_lowercase());
83            },
84            Self::ScreamingSnakeCase => |output, _, _, c| {
85                output.extend(c.to_uppercase());
86            },
87            Self::KebabCase => |output, _, _, c| {
88                output.extend(c.to_lowercase());
89            },
90            Self::ScreamingKebabCase => |output, _, _, c| {
91                output.extend(c.to_uppercase());
92            },
93        };
94
95        let prefix = match self {
96            Self::SnakeCase => Some('_'),
97            Self::ScreamingSnakeCase => Some('_'),
98            Self::KebabCase => Some('-'),
99            Self::ScreamingKebabCase => Some('-'),
100            _ => None,
101        };
102
103        let mut output = String::new();
104        let mut count = 0;
105        let mut group = true;
106
107        for c in input.chars() {
108            if matches!(c, '_') {
109                if !group {
110                    count += 1;
111                }
112
113                group = true;
114                continue;
115            }
116
117            if char::is_uppercase(c) && !group {
118                group = true;
119                count += 1;
120            }
121
122            let group = take(&mut group);
123
124            if group && count > 0 {
125                output.extend(prefix);
126            }
127
128            feed(&mut output, group, count, c);
129        }
130
131        Some(output)
132    }
133}
134
135impl fmt::Display for NameAll {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        match self {
138            Self::Index => write!(f, "index"),
139            Self::Name => write!(f, "name"),
140            Self::PascalCase => write!(f, "PascalCase"),
141            Self::CamelCase => write!(f, "camelCase"),
142            Self::SnakeCase => write!(f, "snake_case"),
143            Self::ScreamingSnakeCase => write!(f, "SCREAMING_SNAKE_CASE"),
144            Self::KebabCase => write!(f, "kebab-case"),
145            Self::ScreamingKebabCase => write!(f, "SCREAMING-KEBAB-CASE"),
146        }
147    }
148}
149
150#[test]
151fn test_rename() {
152    #[track_caller]
153    fn test(input: &str, rename: &str, expected: &str) {
154        let rename = NameAll::parse(rename).unwrap();
155        assert_eq!(rename.apply(input).unwrap(), expected);
156    }
157
158    test("hello_world", "PascalCase", "HelloWorld");
159    test("__hello__world__", "PascalCase", "HelloWorld");
160    test("hello_world", "camelCase", "helloWorld");
161    test("__hello__world__", "camelCase", "helloWorld");
162    test("hello_world", "snake_case", "hello_world");
163    test("__hello__world__", "snake_case", "hello_world");
164    test("hello_world", "SCREAMING_SNAKE_CASE", "HELLO_WORLD");
165    test("__hello__world__", "SCREAMING_SNAKE_CASE", "HELLO_WORLD");
166    test("hello_world", "kebab-case", "hello-world");
167    test("__hello__world__", "kebab-case", "hello-world");
168    test("hello_world", "SCREAMING-KEBAB-CASE", "HELLO-WORLD");
169    test("__hello__world__", "SCREAMING-KEBAB-CASE", "HELLO-WORLD");
170
171    test("HelloWorld", "PascalCase", "HelloWorld");
172    test("__Hello__World__", "PascalCase", "HelloWorld");
173    test("HelloWorld", "camelCase", "helloWorld");
174    test("__Hello__World__", "camelCase", "helloWorld");
175    test("HelloWorld", "snake_case", "hello_world");
176    test("__Hello__World__", "snake_case", "hello_world");
177    test("HelloWorld", "SCREAMING_SNAKE_CASE", "HELLO_WORLD");
178    test("__Hello__World__", "SCREAMING_SNAKE_CASE", "HELLO_WORLD");
179    test("HelloWorld", "kebab-case", "hello-world");
180    test("__Hello__World__", "kebab-case", "hello-world");
181    test("HelloWorld", "SCREAMING-KEBAB-CASE", "HELLO-WORLD");
182    test("__Hello__World__", "SCREAMING-KEBAB-CASE", "HELLO-WORLD");
183}