metrique-macro 0.1.2

Library for working with unit of work metrics - #[metrics] macro
Documentation
use darling::FromMeta;

use crate::{MetricsField, MetricsFieldKind, MetricsVariant, RootAttributes};

#[allow(clippy::enum_variant_names)] // "Case" is part of the name...
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromMeta)]
pub(crate) enum NameStyle {
    #[darling(rename = "PascalCase")]
    PascalCase,
    #[darling(rename = "snake_case")]
    SnakeCase,
    #[darling(rename = "kebab-case")]
    KebabCase,
    #[default]
    Preserve,
}

impl NameStyle {
    pub(crate) fn apply(self, name: &str) -> String {
        use inflector::Inflector;
        match self {
            NameStyle::PascalCase => name.to_pascal_case(),
            NameStyle::SnakeCase => name.to_snake_case(),
            NameStyle::Preserve => name.to_string(),
            NameStyle::KebabCase => name.to_kebab_case(),
        }
    }

    pub(crate) fn apply_prefix(self, name: &str) -> String {
        use inflector::Inflector;
        match self {
            NameStyle::PascalCase => name.to_pascal_case(),
            NameStyle::SnakeCase => {
                let mut res = name.to_snake_case();
                if !res.ends_with("_") {
                    res.push('_');
                }
                res
            }
            NameStyle::Preserve => name.to_string(),
            NameStyle::KebabCase => {
                let mut res = name.to_kebab_case();
                if !res.ends_with("-") {
                    res.push('-');
                }
                res
            }
        }
    }

    pub(crate) fn to_word(self) -> &'static str {
        match self {
            NameStyle::PascalCase => "Pascal",
            NameStyle::SnakeCase => "Snake",
            NameStyle::Preserve => "Preserve",
            NameStyle::KebabCase => "Kebab",
        }
    }
}

pub fn metric_name(
    root_attrs: &RootAttributes,
    name_style: NameStyle,
    field: &impl HasInflectableName,
) -> String {
    let prefix = root_attrs.prefix.as_deref().unwrap_or_default();

    if let Some(name_override) = field.name_override() {
        return name_override.to_owned();
    };
    let base = field.name();
    let prefixed_base = format!("{prefix}{base}");

    name_style.apply(&prefixed_base)
}

pub trait HasInflectableName {
    fn name_override(&self) -> Option<&str>;
    fn name(&self) -> String;
}

impl HasInflectableName for MetricsField {
    fn name_override(&self) -> Option<&str> {
        if let MetricsFieldKind::Field {
            name: Some(name), ..
        } = &self.attrs.kind
        {
            Some(name)
        } else {
            None
        }
    }

    fn name(&self) -> String {
        self.name.clone().expect("name must be set here")
    }
}

impl HasInflectableName for MetricsVariant {
    fn name_override(&self) -> Option<&str> {
        self.attrs.name.as_deref()
    }

    fn name(&self) -> String {
        self.ident.to_string()
    }
}

#[cfg(test)]
mod test {
    use crate::NameStyle;

    #[test]
    fn test_inflect_prefix() {
        let kebab = NameStyle::KebabCase;
        let snake = NameStyle::SnakeCase;
        let pascal = NameStyle::PascalCase;

        assert_eq!(kebab.apply_prefix("Foo"), "foo-");
        assert_eq!(kebab.apply_prefix("foo"), "foo-");
        assert_eq!(kebab.apply_prefix("foo_"), "foo-");
        assert_eq!(kebab.apply_prefix("foo-"), "foo-");

        assert_eq!(snake.apply_prefix("Foo"), "foo_");
        assert_eq!(snake.apply_prefix("foo"), "foo_");
        assert_eq!(snake.apply_prefix("foo_"), "foo_");
        assert_eq!(snake.apply_prefix("foo-"), "foo_");

        assert_eq!(pascal.apply_prefix("Foo"), "Foo");
        assert_eq!(pascal.apply_prefix("foo"), "Foo");
        assert_eq!(pascal.apply_prefix("foo_"), "Foo");
        assert_eq!(pascal.apply_prefix("foo-"), "Foo");
    }
}