Skip to main content

morphir_projection/
naming.rs

1//! Shared camelCase identifier helpers.
2//!
3//! These derive synthetic and semantic identifiers (record fields, synthetic
4//! argument names, namespace segments) from Morphir source names. Every
5//! backend extension that projects [`crate::ProjectionPackage`] into a
6//! target schema language is expected to call these directly (or, for Avro,
7//! re-export them) rather than reimplement the transform: normalization and
8//! rendering must agree byte-for-byte on the derived name for a given
9//! backend, so this is the single source of truth both sides depend on
10//! producing identical output for.
11
12/// Convert `source` to `UpperCamelCase`, treating any run of non-alphanumeric
13/// characters as a word boundary.
14pub fn upper_camel(source: &str) -> String {
15    let words = words(source);
16    let result = words
17        .iter()
18        .map(|word| {
19            let mut chars = word.chars();
20            chars
21                .next()
22                .map(|first| first.to_uppercase().chain(chars).collect::<String>())
23                .unwrap_or_default()
24        })
25        .collect::<String>();
26    valid_identifier(result)
27}
28
29/// Convert `source` to `lowerCamelCase`, treating any run of non-alphanumeric
30/// characters as a word boundary.
31pub fn lower_camel(source: &str) -> String {
32    let upper = upper_camel(source);
33    let mut chars = upper.chars();
34    chars
35        .next()
36        .map(|first| first.to_lowercase().chain(chars).collect::<String>())
37        .unwrap_or_else(|| "_".to_owned())
38}
39
40fn words(source: &str) -> Vec<String> {
41    let mut words = Vec::new();
42    let mut current = String::new();
43    let mut previous_was_lowercase_or_digit = false;
44    for character in source.chars() {
45        if !character.is_ascii_alphanumeric() {
46            if !current.is_empty() {
47                words.push(std::mem::take(&mut current));
48            }
49            previous_was_lowercase_or_digit = false;
50            continue;
51        }
52        if character.is_ascii_uppercase() && previous_was_lowercase_or_digit && !current.is_empty()
53        {
54            words.push(std::mem::take(&mut current));
55        }
56        previous_was_lowercase_or_digit =
57            character.is_ascii_lowercase() || character.is_ascii_digit();
58        current.push(character.to_ascii_lowercase());
59    }
60    if !current.is_empty() {
61        words.push(current);
62    }
63    words
64}
65
66fn valid_identifier(mut value: String) -> String {
67    if value.is_empty() {
68        value.push('_');
69    }
70    if value.as_bytes()[0].is_ascii_digit() {
71        value.insert(0, '_');
72    }
73    value
74}