odata_client_codegen 0.1.0

Strongly-typed OData client code generation
Documentation
//! Functionality to generate consistent, unique Rust idents for entity model items.

use bumpalo::Bump;
use lazy_static::lazy_static;
use std::collections::{HashMap, HashSet};

lazy_static! {
    // https://doc.rust-lang.org/reference/keywords.html
    static ref STRICT_KEYWORDS: HashSet<&'static str> = (&[
        "abstract", "as", "async", "await", "become", "break", "box", "const", "continue", "crate",
        "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in",
        "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
        "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "typeof",
        "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
    ])
    .into_iter()
    .map(|&s| s)
    .collect();
}

/// Provides unique field names for a single derived entity model type
pub struct FieldNameSource {
    used: HashSet<String>,
}

impl FieldNameSource {
    pub fn new() -> Self {
        FieldNameSource {
            used: HashSet::new(),
        }
    }

    pub fn get_struct_field_name<'a>(&'a mut self, entity_model_name: &str) -> &'a str {
        self.get_field_name(entity_model_name, to_snake_case)
    }

    pub fn get_enum_variant_name<'a>(&'a mut self, entity_model_name: &str) -> &'a str {
        self.get_field_name(entity_model_name, to_pascal_case)
    }

    fn get_field_name<'a>(
        &'a mut self,
        entity_model_name: &str,
        convert_case: impl Fn(&str) -> String,
    ) -> &'a str {
        let mut cased = convert_case(entity_model_name);

        while STRICT_KEYWORDS.contains(cased.as_str()) || self.used.contains(&cased) {
            cased.push('_');
        }

        // TODO: use HashSet::get_or_insert, currently nightly only:
        // https://github.com/rust-lang/rust/issues/60896
        let cloned = cased.clone();
        self.used.insert(cased);
        self.used.get(&cloned).unwrap()
    }
}

/// Provides unique names for direct child items of a single submodule.
pub struct ModChildNameSource<'ar> {
    /// All idents used in the module so far
    used: HashSet<&'ar str>,

    /// Lookup of entity model types to their assigned source code ident
    type_lookup: HashMap<&'ar str, &'ar str>,

    /// Lookup of entity sets/singletons to the assigned source code ident
    const_lookup: HashMap<&'ar str, &'ar str>,

    arena: &'ar Bump,
}

impl<'ar> ModChildNameSource<'ar> {
    pub fn new(arena: &'ar Bump) -> Self {
        ModChildNameSource {
            used: HashSet::new(),
            type_lookup: HashMap::new(),
            const_lookup: HashMap::new(),
            arena,
        }
    }

    pub fn get_const_item_name(&mut self, entity_model_name: &'ar str) -> &'ar str {
        Self::get_item_name(
            entity_model_name,
            to_screaming_snake_case,
            self.arena,
            &mut self.used,
            &mut self.const_lookup,
        )
    }

    pub fn get_type_name(&mut self, entity_model_name: &'ar str) -> &'ar str {
        Self::get_item_name(
            entity_model_name,
            to_pascal_case,
            self.arena,
            &mut self.used,
            &mut self.type_lookup,
        )
    }

    fn get_item_name(
        entity_model_name: &'ar str,
        convert_case: impl Fn(&str) -> String,
        arena: &'ar Bump,
        used: &mut HashSet<&'ar str>,
        lookup: &mut HashMap<&'ar str, &'ar str>,
    ) -> &'ar str {
        if let Some(name) = lookup.get(entity_model_name) {
            return name;
        }

        let cased = arena.alloc(convert_case(entity_model_name));

        while STRICT_KEYWORDS.contains(cased.as_str()) || used.contains(cased.as_str()) {
            cased.push('_');
        }

        used.insert(cased.as_str());
        lookup.insert(entity_model_name, cased.as_str());

        cased.as_str()
    }
}

/// Replaces capitals with underscore followed by lower case (with no leading underscore).
// TODO: compiler emits warning on snake_case idents with multiple consecutive underscores in the
// middle; we could remove consecutive underscores. This doesn't seem to apply to
// SCREAMING_SNAKE_CASE.
pub fn to_snake_case(s: &str) -> String {
    let mut out = String::new();
    let mut first = true;

    for c in s.chars() {
        if c.is_uppercase() && !first {
            out.push('_');
        }

        out.extend(c.to_lowercase());

        first = false;
    }

    out
}

/// Inserts underscores (if not present) before upper-case letters which are preceeded by lower-case
/// letters, and capitalises all letters.
pub fn to_screaming_snake_case(s: &str) -> String {
    let mut out = String::new();
    let mut prev_char_lowercase = false;

    for c in s.chars() {
        if c.is_uppercase() && prev_char_lowercase {
            out.push('_');
        }

        prev_char_lowercase = c.is_lowercase();

        out.extend(c.to_uppercase());
    }

    out
}

/// Capitalises lower-case letters which aren't preceeded by some other letter, and removes
/// underscores.
pub fn to_pascal_case(s: &str) -> String {
    let mut out = String::new();
    let mut prev_char_was_letter = false;

    for c in s.chars() {
        if c == '_' {
            prev_char_was_letter = false;
        } else if !c.is_alphabetic() {
            prev_char_was_letter = false;
            out.push(c);
        } else {
            if prev_char_was_letter {
                out.push(c);
            } else {
                out.extend(c.to_uppercase());
            }
            prev_char_was_letter = true;
        }
    }

    out
}