vrl 0.37.0

Vector Remap Language
Documentation
use convert_case::{Boundary, Case, Casing};

use crate::compiler::function::EnumVariant;
use crate::prelude::*;
use crate::value;

cfg_if::cfg_if! {
    if #[cfg(feature = "stdlib-base")] {
        pub(crate) mod camelcase;
        pub(crate) mod pascalcase;
        pub(crate) mod snakecase;
        pub(crate) mod screamingsnakecase;
        pub(crate) mod kebabcase;
    }
}

pub(crate) static ORIGINAL_CASE: Parameter = Parameter::optional(
    "original_case",
    kind::BYTES,
    "Optional hint on the original case type. Must be one of: kebab-case, camelCase, PascalCase, SCREAMING_SNAKE, snake_case",
)
.enum_variants(&[
    EnumVariant {
        value: "kebab-case",
        description: "[kebab-case](https://en.wikipedia.org/wiki/Letter_case#Kebab_case)",
    },
    EnumVariant {
        value: "camelCase",
        description: "[camelCase](https://en.wikipedia.org/wiki/Camel_case)",
    },
    EnumVariant {
        value: "PascalCase",
        description: "[PascalCase](https://en.wikipedia.org/wiki/Camel_case)",
    },
    EnumVariant {
        value: "SCREAMING_SNAKE",
        description: "[SCREAMING_SNAKE](https://en.wikipedia.org/wiki/Snake_case)",
    },
    EnumVariant {
        value: "snake_case",
        description: "[snake_case](https://en.wikipedia.org/wiki/Snake_case)",
    },
]);

pub(crate) fn variants() -> Vec<Value> {
    vec![
        value!("camelCase"),
        value!("PascalCase"),
        value!("SCREAMING_SNAKE"),
        value!("snake_case"),
        value!("kebab-case"),
    ]
}

pub(crate) fn boundaries() -> Vec<Value> {
    vec![
        value!("lower_upper"), // Splits "camelCase" into "camel" and "Case"
        value!("upper_lower"), // Rarely used, splits "CamelCase" at "Camel" and "Case"
        value!("acronym"),     // Splits "XMLHttpRequest" into "XML" and "HttpRequest"
        value!("lower_digit"), // Splits "version2Release" into "version" and "2Release"
        value!("upper_digit"), // Splits "Version2Release" into "Version" and "2Release"
        value!("digit_lower"), // Splits "v2release" into "v2" and "release"
        value!("digit_upper"), // Splits "v2Release" into "v2" and "Release"
    ]
}

fn join_string_values(values: &[Value]) -> String {
    values
        .iter()
        .filter_map(Value::as_string)
        .map(AsRef::<str>::as_ref)
        .collect::<Vec<_>>()
        .join(", ")
}

pub(crate) fn boundaries_msg() -> String {
    join_string_values(&boundaries())
}

pub(crate) fn variants_msg() -> String {
    join_string_values(&variants())
}

pub(crate) fn into_case(s: &str) -> Result<Case<'static>, Box<dyn DiagnosticMessage>> {
    match s {
        "camelCase" => Ok(Case::Camel),
        "PascalCase" => Ok(Case::Pascal),
        "SCREAMING_SNAKE" => Ok(Case::Constant),
        "snake_case" => Ok(Case::Snake),
        "kebab-case" => Ok(Case::Kebab),
        _ => Err(Box::new(ExpressionError::from(format!(
            "case must match one of: {}",
            variants_msg()
        ))) as Box<dyn DiagnosticMessage>),
    }
}

pub(crate) fn into_boundary(s: &str) -> Result<convert_case::Boundary, Box<dyn DiagnosticMessage>> {
    match s {
        "lower_upper" => Ok(convert_case::Boundary::LowerUpper),
        "upper_lower" => Ok(convert_case::Boundary::UpperLower),
        "acronym" => Ok(convert_case::Boundary::Acronym),
        "lower_digit" => Ok(convert_case::Boundary::LowerDigit),
        "upper_digit" => Ok(convert_case::Boundary::UpperDigit),
        "digit_lower" => Ok(convert_case::Boundary::DigitLower),
        "digit_upper" => Ok(convert_case::Boundary::DigitUpper),
        _ => Err(Box::new(ExpressionError::from(format!(
            "boundary must match one of: {}",
            boundaries_msg()
        ))) as Box<dyn DiagnosticMessage>),
    }
}

#[inline]
pub(crate) fn convert_case(
    value: &Value,
    to_case: Case<'static>,
    from_case: Option<Case<'static>>,
) -> Resolved {
    let string_value = value.try_bytes_utf8_lossy()?;

    match from_case {
        Some(case) => Ok(string_value
            .from_case(case)
            .remove_empty()
            .to_case(to_case)
            .into()),
        None => Ok(string_value.remove_empty().to_case(to_case).into()),
    }
}

#[inline]
pub(crate) fn convert_case_with_excluded_boundaries(
    string_value: &str,
    to_case: Case<'static>,
    from_case: Option<Case<'static>>,
    excluded_boundaries: &[Boundary],
) -> Value {
    match from_case {
        Some(case) => string_value
            .from_case(case)
            .remove_boundaries(excluded_boundaries)
            .remove_empty()
            .to_case(to_case)
            .into(),
        None => string_value
            .remove_boundaries(excluded_boundaries)
            .remove_empty()
            .to_case(to_case)
            .into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_var_msg() {
        assert_eq!(
            "camelCase, PascalCase, SCREAMING_SNAKE, snake_case, kebab-case",
            variants_msg()
        );
    }
}