Skip to main content

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::{
25    CamelCase, KebabCase, LowerCase, None, PascalCase, ScreamingKebabCase, ScreamingSnakeCase,
26    SnakeCase, UpperCase,
27};
28use std::fmt::{self, Debug, Display};
29
30/// The different possible ways to change case of fields in a struct, or variants in an enum.
31#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
32pub enum RenameRule {
33    /// Don't apply a default rename rule.
34    #[default]
35    None,
36    /// Rename direct children to `lowercase` style.
37    LowerCase,
38    /// Rename direct children to `UPPERCASE` style.
39    UpperCase,
40    /// Rename direct children to `PascalCase` style, as typically used for
41    /// enum variants.
42    PascalCase,
43    /// Rename direct children to `camelCase` style.
44    CamelCase,
45    /// Rename direct children to `snake_case` style, as commonly used for
46    /// fields.
47    SnakeCase,
48    /// Rename direct children to `SCREAMING_SNAKE_CASE` style, as commonly
49    /// used for constants.
50    ScreamingSnakeCase,
51    /// Rename direct children to `kebab-case` style.
52    KebabCase,
53    /// Rename direct children to `SCREAMING-KEBAB-CASE` style.
54    ScreamingKebabCase,
55}
56
57impl FromMeta for RenameRule {
58    fn from_value(value: &Lit) -> darling::Result<Self> {
59        let Lit::Str(litstr) = value else {
60            return Err(darling::Error::unexpected_lit_type(value));
61        };
62        Self::from_str(&litstr.value()).map_err(darling::Error::custom)
63    }
64}
65
66static RENAME_RULES: &[(&str, RenameRule)] = &[
67    ("lowercase", LowerCase),
68    ("UPPERCASE", UpperCase),
69    ("PascalCase", PascalCase),
70    ("camelCase", CamelCase),
71    ("snake_case", SnakeCase),
72    ("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
73    ("kebab-case", KebabCase),
74    ("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
75];
76
77impl RenameRule {
78    pub fn from_str(rename_all_str: &str) -> Result<Self, ParseError> {
79        RENAME_RULES
80            .iter()
81            .find(|(name, _)| *name == rename_all_str)
82            .map(|(_, rule)| *rule)
83            .ok_or_else(|| ParseError {
84                unknown: rename_all_str.to_string(),
85            })
86    }
87
88    /// Apply a renaming rule to an enum variant, returning the version expected in the source.
89    pub fn apply_to_variant(self, variant: &str) -> String {
90        match self {
91            None | PascalCase => variant.to_owned(),
92            LowerCase => variant.to_ascii_lowercase(),
93            UpperCase => variant.to_ascii_uppercase(),
94            CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
95            SnakeCase => {
96                let mut snake = String::new();
97                for (i, ch) in variant.char_indices() {
98                    if i > 0 && ch.is_uppercase() {
99                        snake.push('_');
100                    }
101                    snake.push(ch.to_ascii_lowercase());
102                }
103                snake
104            }
105            ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
106            KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
107            ScreamingKebabCase => ScreamingSnakeCase
108                .apply_to_variant(variant)
109                .replace('_', "-"),
110        }
111    }
112
113    /// Apply a renaming rule to a struct field, returning the version expected in the source.
114    pub fn apply_to_field(self, field: &str) -> String {
115        match self {
116            None | LowerCase | SnakeCase => field.to_owned(),
117            UpperCase | ScreamingSnakeCase => field.to_ascii_uppercase(),
118            PascalCase => {
119                let mut pascal = String::new();
120                let mut capitalize = true;
121                for ch in field.chars() {
122                    if ch == '_' {
123                        capitalize = true;
124                    } else if capitalize {
125                        pascal.push(ch.to_ascii_uppercase());
126                        capitalize = false;
127                    } else {
128                        pascal.push(ch);
129                    }
130                }
131                pascal
132            }
133            CamelCase => {
134                let pascal = PascalCase.apply_to_field(field);
135                pascal[..1].to_ascii_lowercase() + &pascal[1..]
136            }
137            KebabCase => field.replace('_', "-"),
138            ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"),
139        }
140    }
141}
142
143pub struct ParseError {
144    unknown: String,
145}
146
147impl Display for ParseError {
148    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
149        f.write_str("unknown rename rule `rename_all = ")?;
150        Debug::fmt(&self.unknown, f)?;
151        f.write_str("`, expected one of ")?;
152        for (i, (name, _rule)) in RENAME_RULES.iter().enumerate() {
153            if i > 0 {
154                f.write_str(", ")?;
155            }
156            Debug::fmt(name, f)?;
157        }
158        Ok(())
159    }
160}
161
162#[test]
163fn rename_variants() {
164    for &(original, lower, upper, camel, snake, screaming, kebab, screaming_kebab) in &[
165        (
166            "Outcome", "outcome", "OUTCOME", "outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
167        ),
168        (
169            "VeryTasty",
170            "verytasty",
171            "VERYTASTY",
172            "veryTasty",
173            "very_tasty",
174            "VERY_TASTY",
175            "very-tasty",
176            "VERY-TASTY",
177        ),
178        ("A", "a", "A", "a", "a", "A", "a", "A"),
179        ("Z42", "z42", "Z42", "z42", "z42", "Z42", "z42", "Z42"),
180    ] {
181        assert_eq!(None.apply_to_variant(original), original);
182        assert_eq!(LowerCase.apply_to_variant(original), lower);
183        assert_eq!(UpperCase.apply_to_variant(original), upper);
184        assert_eq!(PascalCase.apply_to_variant(original), original);
185        assert_eq!(CamelCase.apply_to_variant(original), camel);
186        assert_eq!(SnakeCase.apply_to_variant(original), snake);
187        assert_eq!(ScreamingSnakeCase.apply_to_variant(original), screaming);
188        assert_eq!(KebabCase.apply_to_variant(original), kebab);
189        assert_eq!(
190            ScreamingKebabCase.apply_to_variant(original),
191            screaming_kebab
192        );
193    }
194}
195
196#[test]
197fn rename_fields() {
198    for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[
199        (
200            "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME",
201        ),
202        (
203            "very_tasty",
204            "VERY_TASTY",
205            "VeryTasty",
206            "veryTasty",
207            "VERY_TASTY",
208            "very-tasty",
209            "VERY-TASTY",
210        ),
211        ("a", "A", "A", "a", "A", "a", "A"),
212        ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"),
213    ] {
214        assert_eq!(None.apply_to_field(original), original);
215        assert_eq!(UpperCase.apply_to_field(original), upper);
216        assert_eq!(PascalCase.apply_to_field(original), pascal);
217        assert_eq!(CamelCase.apply_to_field(original), camel);
218        assert_eq!(SnakeCase.apply_to_field(original), original);
219        assert_eq!(ScreamingSnakeCase.apply_to_field(original), screaming);
220        assert_eq!(KebabCase.apply_to_field(original), kebab);
221        assert_eq!(ScreamingKebabCase.apply_to_field(original), screaming_kebab);
222    }
223}