apache_avro_derive/
case.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Code to convert the Rust-styled field/variant (e.g. `my_field`, `MyType`) to the
19//! case of the source (e.g. `my-field`, `MY_FIELD`).
20//! Code copied from serde <https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/case.rs>
21use darling::FromMeta;
22use syn::Lit;
23
24use self::RenameRule::*;
25use std::fmt::{self, Debug, Display};
26
27/// The different possible ways to change case of fields in a struct, or variants in an enum.
28#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
29pub enum RenameRule {
30    /// Don't apply a default rename rule.
31    #[default]
32    None,
33    /// Rename direct children to "lowercase" style.
34    LowerCase,
35    /// Rename direct children to "UPPERCASE" style.
36    UpperCase,
37    /// Rename direct children to "PascalCase" style, as typically used for
38    /// enum variants.
39    PascalCase,
40    /// Rename direct children to "camelCase" style.
41    CamelCase,
42    /// Rename direct children to "snake_case" style, as commonly used for
43    /// fields.
44    SnakeCase,
45    /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
46    /// used for constants.
47    ScreamingSnakeCase,
48    /// Rename direct children to "kebab-case" style.
49    KebabCase,
50    /// Rename direct children to "SCREAMING-KEBAB-CASE" style.
51    ScreamingKebabCase,
52}
53
54impl FromMeta for RenameRule {
55    fn from_value(value: &Lit) -> darling::Result<Self> {
56        let Lit::Str(litstr) = value else {
57            return Err(darling::Error::unexpected_lit_type(value));
58        };
59        Self::from_str(&litstr.value()).map_err(darling::Error::custom)
60    }
61}
62
63static RENAME_RULES: &[(&str, RenameRule)] = &[
64    ("lowercase", LowerCase),
65    ("UPPERCASE", UpperCase),
66    ("PascalCase", PascalCase),
67    ("camelCase", CamelCase),
68    ("snake_case", SnakeCase),
69    ("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
70    ("kebab-case", KebabCase),
71    ("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
72];
73
74impl RenameRule {
75    pub fn from_str(rename_all_str: &str) -> Result<Self, ParseError> {
76        RENAME_RULES
77            .iter()
78            .find(|(name, _)| *name == rename_all_str)
79            .map(|(_, rule)| *rule)
80            .ok_or_else(|| ParseError {
81                unknown: rename_all_str.to_string(),
82            })
83    }
84
85    /// Apply a renaming rule to an enum variant, returning the version expected in the source.
86    pub fn apply_to_variant(self, variant: &str) -> String {
87        match self {
88            None | PascalCase => variant.to_owned(),
89            LowerCase => variant.to_ascii_lowercase(),
90            UpperCase => variant.to_ascii_uppercase(),
91            CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
92            SnakeCase => {
93                let mut snake = String::new();
94                for (i, ch) in variant.char_indices() {
95                    if i > 0 && ch.is_uppercase() {
96                        snake.push('_');
97                    }
98                    snake.push(ch.to_ascii_lowercase());
99                }
100                snake
101            }
102            ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
103            KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
104            ScreamingKebabCase => ScreamingSnakeCase
105                .apply_to_variant(variant)
106                .replace('_', "-"),
107        }
108    }
109
110    /// Apply a renaming rule to a struct field, returning the version expected in the source.
111    pub fn apply_to_field(self, field: &str) -> String {
112        match self {
113            None | LowerCase | SnakeCase => field.to_owned(),
114            UpperCase => field.to_ascii_uppercase(),
115            PascalCase => {
116                let mut pascal = String::new();
117                let mut capitalize = true;
118                for ch in field.chars() {
119                    if ch == '_' {
120                        capitalize = true;
121                    } else if capitalize {
122                        pascal.push(ch.to_ascii_uppercase());
123                        capitalize = false;
124                    } else {
125                        pascal.push(ch);
126                    }
127                }
128                pascal
129            }
130            CamelCase => {
131                let pascal = PascalCase.apply_to_field(field);
132                pascal[..1].to_ascii_lowercase() + &pascal[1..]
133            }
134            ScreamingSnakeCase => field.to_ascii_uppercase(),
135            KebabCase => field.replace('_', "-"),
136            ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
137        }
138    }
139}
140
141pub struct ParseError {
142    unknown: String,
143}
144
145impl Display for ParseError {
146    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
147        f.write_str("unknown rename rule `rename_all = ")?;
148        Debug::fmt(&self.unknown, f)?;
149        f.write_str("`, expected one of ")?;
150        for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() {
151            if i > 0 {
152                f.write_str(", ")?;
153            }
154            Debug::fmt(name, f)?;
155        }
156        Ok(())
157    }
158}
159
160#[test]
161fn rename_variants() {
162    for &(original, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[
163        (
164            "Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
165        ),
166        (
167            "VeryTasty",
168            "verytasty",
169            "VERYTASTY",
170            "veryTasty",
171            "very_tasty",
172            "VERY_TASTY",
173            "very-tasty",
174            "VERY-TASTY",
175        ),
176        ("A", "a", "A", "a", "a", "A", "a", "A"),
177        ("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"),
178    ] {
179        assert_eq!(None.apply_to_variant(original), original);
180        assert_eq!(LowerCase.apply_to_variant(original), lower);
181        assert_eq!(UpperCase.apply_to_variant(original), upper);
182        assert_eq!(PascalCase.apply_to_variant(original), original);
183        assert_eq!(CamelCase.apply_to_variant(original), camel);
184        assert_eq!(SnakeCase.apply_to_variant(original), snake);
185        assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming);
186        assert_eq!(KebabCase.apply_to_variant(original), kebab);
187        assert_eq!(
188            ScreamingKebabCase.apply_to_variant(original),
189            screaming_kebab
190        );
191    }
192}
193
194#[test]
195fn rename_fields() {
196    for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[
197        (
198            "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
199        ),
200        (
201            "very_tasty",
202            "VERY_TASTY",
203            "VeryTasty",
204            "veryTasty",
205            "VERY_TASTY",
206            "very-tasty",
207            "VERY-TASTY",
208        ),
209        ("a", "A", "A", "a", "A", "a", "A"),
210        ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"),
211    ] {
212        assert_eq!(None.apply_to_field(original), original);
213        assert_eq!(UpperCase.apply_to_field(original), upper);
214        assert_eq!(PascalCase.apply_to_field(original), pascal);
215        assert_eq!(CamelCase.apply_to_field(original), camel);
216        assert_eq!(SnakeCase.apply_to_field(original), original);
217        assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming);
218        assert_eq!(KebabCase.apply_to_field(original), kebab);
219        assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
220    }
221}