1use std::collections::HashMap;
2use std::fmt::{Display, Formatter};
3use std::rc::Rc;
4
5use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase};
6use lazy_static::lazy_static;
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct Name {
29    validated: Rc<String>,
30}
31
32impl From<Name> for String {
33    fn from(x: Name) -> Self {
34        x.validated.to_string()
35    }
36}
37
38impl std::ops::Deref for Name {
39    type Target = str;
40
41    fn deref(&self) -> &Self::Target {
42        self.validated.as_str()
43    }
44}
45
46impl Display for Name {
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.validated.as_str())
49    }
50}
51
52impl PartialEq<&str> for Name {
53    fn eq(&self, other: &&str) -> bool {
54        &self.as_ref() == other
55    }
56}
57
58impl PartialEq<String> for Name {
59    fn eq(&self, other: &String) -> bool {
60        self.as_ref() == other
61    }
62}
63
64impl PartialEq<str> for Name {
65    fn eq(&self, other: &str) -> bool {
66        self.as_ref() == other
67    }
68}
69
70pub trait IntoName {
71    fn into_name(self) -> Result<Name, BadName>;
72}
73
74impl<T> IntoName for T
75where
76    T: AsRef<str>,
77{
78    fn into_name(self) -> Result<Name, BadName> {
79        Name::create(self.as_ref())
80    }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct BadName {
85    pub(crate) name: String,
86    pub(crate) error: NameError,
87}
88
89impl BadName {
90    fn new(name: String, error: NameError) -> Self {
91        Self { name, error }
92    }
93}
94
95impl std::error::Error for BadName {}
96
97impl Display for BadName {
98    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99        write!(f, "name: {} error: {}", self.name, self.error)
100    }
101}
102
103#[non_exhaustive]
104#[derive(Error, Debug, Copy, Clone, PartialEq, Eq)]
105pub(crate) enum NameError {
106    #[error("Name is an empty string")]
107    IsEmpty,
108    #[error("Name contains invalid character '{}'", c)]
109    CharacterNeverAllowed { c: char },
110    #[error("Name must start with lowercase ascii but first character is '{}'", c)]
111    FirstCharacterNotLowercaseAscii { c: char },
112    #[error(
113        "'{}' is a reserved identifier in backend language '{}' ",
114        id,
115        language
116    )]
117    ReservedIdentifier {
118        id: &'static str,
119        language: &'static str,
120    },
121    #[error("'{}' is a sub-phrase reserved by oo-bindgen itself", phrase)]
122    BindgenConflict { phrase: &'static str },
123    #[error("Names cannot contain double underscores")]
124    ContainsDoubleUnderscore,
125    #[error("Names cannot end in underscores")]
126    LastCharacterIsUnderscore,
127}
128
129impl NameError {
130    fn character_never_allowed(c: char) -> NameError {
131        NameError::CharacterNeverAllowed { c }
132    }
133
134    fn first_character_not_lower_case_ascii(c: char) -> NameError {
135        NameError::FirstCharacterNotLowercaseAscii { c }
136    }
137
138    fn reserved_identifier(id: &'static str, language: &'static str) -> NameError {
139        NameError::ReservedIdentifier { id, language }
140    }
141}
142
143impl AsRef<str> for Name {
144    fn as_ref(&self) -> &str {
145        self.validated.as_str()
146    }
147}
148
149impl Name {
150    pub(crate) fn camel_case(&self) -> String {
152        self.validated.to_upper_camel_case()
153    }
154
155    pub(crate) fn capital_snake_case(&self) -> String {
157        self.validated.to_shouty_snake_case()
158    }
159
160    pub(crate) fn mixed_case(&self) -> String {
162        self.validated.to_lower_camel_case()
163    }
164
165    pub(crate) fn kebab_case(&self) -> String {
167        self.validated.to_kebab_case()
168    }
169
170    pub fn create<S: AsRef<str>>(value: S) -> Result<Self, BadName> {
172        Self::create_impl(value.as_ref())
173    }
174
175    #[must_use]
177    pub(crate) fn append(&self, other: &Name) -> Self {
178        Self {
179            validated: Rc::new(format!(
180                "{}_{}",
181                self.validated.as_str(),
182                other.validated.as_str()
183            )),
184        }
185    }
186
187    fn is_allowed(c: char) -> bool {
188        c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'
189    }
190
191    fn create_impl(value: &str) -> Result<Self, BadName> {
192        if let Some((keyword, lang)) = KEYWORD_MAP.get(value) {
193            return Err(BadName::new(
194                value.to_string(),
195                NameError::reserved_identifier(keyword, lang),
196            ));
197        }
198
199        match value.chars().next() {
200            Some(c) => {
201                if !c.is_ascii_lowercase() {
202                    return Err(BadName::new(
203                        value.to_string(),
204                        NameError::first_character_not_lower_case_ascii(c),
205                    ));
206                }
207            }
208            None => return Err(BadName::new(value.to_string(), NameError::IsEmpty)),
209        }
210
211        if let Some(bad_character) = value.chars().find(|c| !Self::is_allowed(*c)) {
212            return Err(BadName::new(
213                value.to_string(),
214                NameError::character_never_allowed(bad_character),
215            ));
216        }
217
218        if value.contains("__") {
219            return Err(BadName::new(
220                value.to_string(),
221                NameError::ContainsDoubleUnderscore,
222            ));
223        }
224
225        if let Some('_') = value.chars().last() {
226            return Err(BadName::new(
227                value.to_string(),
228                NameError::LastCharacterIsUnderscore,
229            ));
230        }
231
232        for phrase in OO_BINDGEN_RESERVED_PHRASES {
233            if value.contains(phrase) {
234                return Err(BadName::new(
235                    value.to_string(),
236                    NameError::BindgenConflict { phrase },
237                ));
238            }
239        }
240
241        Ok(Name {
242            validated: Rc::new(value.to_string()),
243        })
244    }
245}
246
247lazy_static! {
248    static ref KEYWORD_MAP: KeywordMap = KeywordMap::new();
249}
250
251const RUST_KEYWORDS: &[&str] = &[
252    "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate",
253    "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in",
254    "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
255    "return", "self", "Self", "static", "struct", "super", "trait", "true", "try", "type",
256    "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
257];
258
259const C_KEYWORDS: &[&str] = &[
260    "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else",
261    "enum", "extern", "float", "for", "goto", "if", "int", "long", "register", "return", "short",
262    "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void",
263    "volatile", "while",
264];
265
266const CPP_KEYWORDS: &[&str] = &[
267    "alignas",
268    "alignof",
269    "and",
270    "and_eq",
271    "asm",
272    "auto",
273    "bitand",
274    "bitor",
275    "bool",
276    "break",
277    "case",
278    "catch",
279    "char",
280    "char16_t",
281    "char32_t",
282    "char8_t",
283    "class",
284    "co_await",
285    "co_return",
286    "co_yield",
287    "compl",
288    "concept",
289    "const",
290    "const_cast",
291    "consteval",
292    "constexpr",
293    "constinit",
294    "continue",
295    "declaration",
296    "decltype",
297    "default",
298    "delete",
299    "directive",
300    "do",
301    "double",
302    "dynamic_cast",
303    "else",
304    "enum",
305    "explicit",
306    "export",
307    "extern",
308    "false",
309    "float",
310    "for",
311    "friend",
312    "goto",
313    "if",
314    "inline",
315    "int",
316    "long",
317    "mutable",
318    "namespace",
319    "new",
320    "noexcept",
321    "not",
322    "not_eq",
323    "nullptr",
324    "operator",
325    "or",
326    "or_eq",
327    "private",
328    "protected",
329    "public",
330    "register",
331    "reinterpret_cast",
332    "requires",
333    "return",
334    "short",
335    "signed",
336    "sizeof",
337    "static",
338    "static_assert",
339    "static_cast",
340    "struct",
341    "switch",
342    "template",
343    "this",
344    "thread_local",
345    "throw",
346    "true",
347    "try",
348    "typedef",
349    "typeid",
350    "typename",
351    "union",
352    "unsigned",
353    "using",
354    "using",
355    "virtual",
356    "void",
357    "volatile",
358    "wchar_t",
359    "while",
360    "xor",
361    "xor_eq",
362];
363
364const JAVA_KEYWORDS: &[&str] = &[
365    "abstract",
366    "assert",
367    "boolean",
368    "break",
369    "byte",
370    "case",
371    "catch",
372    "char",
373    "class",
374    "const",
375    "continue",
376    "default",
377    "do",
378    "double",
379    "else",
380    "enum",
381    "extends",
382    "final",
383    "finally",
384    "float",
385    "for",
386    "goto",
387    "if",
388    "implements",
389    "import",
390    "instanceof",
391    "int",
392    "interface",
393    "long",
394    "native",
395    "new",
396    "package",
397    "private",
398    "protected",
399    "public",
400    "return",
401    "short",
402    "static",
403    "strictfp",
404    "super",
405    "switch",
406    "synchronized",
407    "this",
408    "throw",
409    "throws",
410    "transient",
411    "try",
412    "void",
413    "volatile",
414    "while",
415];
416
417const CSHARP_KEYWORDS: &[&str] = &[
418    "abstract",
419    "as",
420    "base",
421    "bool",
422    "break",
423    "byte",
424    "case",
425    "catch",
426    "char",
427    "checked",
428    "class",
429    "const",
430    "continue",
431    "decimal",
432    "default",
433    "delegate",
434    "do",
435    "double",
436    "else",
437    "enum",
438    "event",
439    "explicit",
440    "extern",
441    "false",
442    "finally",
443    "fixed",
444    "float",
445    "for",
446    "foreach",
447    "goto",
448    "if",
449    "implicit",
450    "in",
451    "int",
452    "interface",
453    "internal",
454    "is",
455    "lock",
456    "long",
457    "namespace",
458    "new",
459    "null",
460    "object",
461    "operator",
462    "out",
463    "override",
464    "params",
465    "private",
466    "protected",
467    "public",
468    "readonly",
469    "ref",
470    "return",
471    "sbyte",
472    "sealed",
473    "short",
474    "sizeof",
475    "stackalloc",
476    "static",
477    "string",
478    "struct",
479    "switch",
480    "this",
481    "throw",
482    "true",
483    "try",
484    "typeof",
485    "uint",
486    "ulong",
487    "unchecked",
488    "unsafe",
489    "ushort",
490    "using",
491    "virtual",
492    "void",
493    "volatile",
494    "while",
495];
496
497const OO_BINDGEN_RESERVED_PHRASES: &[&str] = &[
503    "oo_bindgen",
504    "backend_library_loader",
506];
507
508type KeyWordMap = HashMap<&'static str, (&'static str, &'static str)>;
509
510struct KeywordMap {
511    map: KeyWordMap,
512}
513
514impl KeywordMap {
515    fn new() -> Self {
516        let mut map = KeyWordMap::new();
517
518        for x in RUST_KEYWORDS {
519            map.insert(x, (x, "Rust"));
520        }
521
522        for x in C_KEYWORDS {
523            map.insert(x, (x, "C"));
524        }
525
526        for x in CPP_KEYWORDS {
527            map.insert(x, (x, "C++"));
528        }
529
530        for x in JAVA_KEYWORDS {
531            map.insert(x, (x, "Java"));
532        }
533
534        for x in CSHARP_KEYWORDS {
535            map.insert(x, (x, "C#"));
536        }
537
538        Self { map }
539    }
540
541    fn get(&self, x: &str) -> Option<&(&'static str, &'static str)> {
542        self.map.get(x)
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn allows_valid_names() {
552        assert!(Name::create("a1").is_ok());
553        assert!(Name::create("abc_def").is_ok());
554        assert!(Name::create("a1_d_2").is_ok());
555    }
556
557    #[test]
558    fn cannot_equal_reserved_identifiers() {
559        assert_eq!(
560            Name::create("alignas").err().unwrap().error,
561            NameError::reserved_identifier("alignas", "C++")
562        );
563        assert_eq!(
564            Name::create("implements").err().unwrap().error,
565            NameError::reserved_identifier("implements", "Java")
566        );
567        assert_eq!(
568            Name::create("delegate").err().unwrap().error,
569            NameError::reserved_identifier("delegate", "C#")
570        );
571        assert_eq!(
572            Name::create("box").err().unwrap().error,
573            NameError::reserved_identifier("box", "Rust")
574        );
575    }
576
577    #[test]
578    fn cannot_be_empty() {
579        assert_eq!(Name::create("").err().unwrap().error, NameError::IsEmpty);
580    }
581
582    #[test]
583    fn cannot_contain_uppercase() {
584        assert_eq!(
585            Name::create("aBc").err().unwrap().error,
586            NameError::character_never_allowed('B')
587        );
588    }
589
590    #[test]
591    fn cannot_lead_with_underscores_or_numbers() {
592        assert_eq!(
593            Name::create("_abc").err().unwrap().error,
594            NameError::first_character_not_lower_case_ascii('_')
595        );
596        assert_eq!(
597            Name::create("1abc").err().unwrap().error,
598            NameError::first_character_not_lower_case_ascii('1')
599        );
600    }
601
602    #[test]
603    fn last_character_cannot_be_underscore() {
604        assert_eq!(
605            Name::create("abc_").err().unwrap().error,
606            NameError::LastCharacterIsUnderscore
607        );
608    }
609
610    #[test]
611    fn cannot_contain_double_underscore() {
612        assert_eq!(
613            Name::create("abc_def__ghi").err().unwrap().error,
614            NameError::ContainsDoubleUnderscore
615        );
616    }
617
618    #[test]
619    fn cannot_contain_a_sub_phrase_reserved_by_oo_bindgen() {
620        assert_eq!(
621            Name::create("blah_blah_oo_bindgen_blad")
622                .err()
623                .unwrap()
624                .error,
625            NameError::BindgenConflict {
626                phrase: "oo_bindgen"
627            }
628        );
629    }
630
631    #[test]
632    fn can_append_string() {
633        assert_eq!(
634            Name::create("abc")
635                .unwrap()
636                .append(&Name::create("def").unwrap())
637                .as_ref(),
638            "abc_def"
639        );
640    }
641
642    #[test]
643    fn names_are_compared_by_inner_value() {
644        assert_eq!(Name::create("abc"), Name::create("abc"));
645    }
646
647    #[test]
648    fn name_not_equal_works_as_expected() {
649        assert_ne!(Name::create("abc"), Name::create("def"));
650    }
651}