From 5c2746e0db0658224ab5f58aad429c28a4c3ea4f Mon Sep 17 00:00:00 2001 From: Wyatt Herkamp Date: Sat, 18 Nov 2023 15:53:54 -0500 Subject: [PATCH 1/4] Cases Enum --- .gitignore | 1 + Cargo.toml | 9 ++ src/cases.rs | 362 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 +- src/shouty_snake.rs | 1 + 5 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 src/cases.rs diff --git a/.gitignore b/.gitignore index a9d37c5..965ad77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +.vscode/settings.json diff --git a/Cargo.toml b/Cargo.toml index e4ca4c9..d337eb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,12 @@ repository = "https://github.com/withoutboats/heck" keywords = ["string", "case", "camel", "snake", "unicode"] categories = ["no-std"] include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] + +[dependencies] +# Optional Dependency if you want to increase the performance of Case from_str +phf = { version = "0.11", default-features = false, features = [ + "macros", +], optional = true } + +[features] +std = ["phf?/std"] diff --git a/src/cases.rs b/src/cases.rs new file mode 100644 index 0000000..0f80cea --- /dev/null +++ b/src/cases.rs @@ -0,0 +1,362 @@ +use core::fmt::Display; + +use alloc::{borrow::ToOwned, fmt, string::String}; + +use crate::{ + AsKebabCase, AsLowerCamelCase, AsShoutyKebabCase, AsShoutySnekCase, AsSnakeCase, AsTitleCase, + AsTrainCase, AsUpperCamelCase, ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, + ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToTrainCase, ToUpperCamelCase, +}; +/// Error returned when a case is not found +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CaseNotFound(String); + +impl> From for CaseNotFound { + fn from(s: T) -> Self { + Self(s.into()) + } +} +impl Display for CaseNotFound { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Case not found: {}", self.0) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CaseNotFound {} + +/// The case to convert to +/// +/// This is a way to specify case conversion "dynamicly" +/// # Example: +/// ``` +/// +/// use heck::Case; +/// use heck::ToCase; +/// let original = "We are going to inherit the earth"; +/// let cases = vec![ +/// ("camelCase", "weAreGoingToInheritTheEarth"), +/// ("UpperCamelCase", "WeAreGoingToInheritTheEarth"), +/// ("PascalCase", "WeAreGoingToInheritTheEarth"), +/// ("snake_case", "we_are_going_to_inherit_the_earth"), +/// ("UPPER_SNAKE_CASE", "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), +/// ("kebab-case", "we-are-going-to-inherit-the-earth"), +/// ("UPPER-KEBAB-CASE", "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), +/// ("Title Case", "We Are Going To Inherit The Earth"), +/// ("Train-Case", "We-Are-Going-To-Inherit-The-Earth"), +/// ("UPPERCASE", "WE ARE GOING TO INHERIT THE EARTH"), +/// ("lowercase", "we are going to inherit the earth"), +/// ]; +/// for (case, expected) in cases { +/// let case = case.parse().expect("Failed to parse case"); +/// assert_eq!(original.to_case(case), expected); +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Case { + /// `camelCase` is primary name + /// + /// Other accepted names are `lowerCamelCase` + /// + /// [See Also](ToLowerCamelCase) + LowerCamelCase, + /// `UpperCamelCase` is primary name + /// + /// [See Also](ToUpperCamelCase) + UpperCamelCase, + /// `PascalCase` is primary name + /// + /// [See Also](ToPascalCase) + Pascal, + /// `snake_case` is primary name + /// + /// Other accepted names are `lower_snake_case` + /// + /// [See Also](ToSnakeCase) + Snake, + /// `UPPER_SNAKE_CASE` is primary name + /// + /// Other accepted names are `SCREAMING_SNAKE_CASE` + /// + /// [See Also](ToShoutySnakeCase) + ScreamingSnake, + /// `kebab-case` is primary name + /// + /// Other accepted names are `lower-kebab-case` + /// + /// [See Also](ToKebabCase) + Kebab, + /// `SCREAMING-KEBAB-CASE` is primary name + /// + /// Other accepted names are `UPPER-KEBAB-CASE` + /// + /// [See Also](ToShoutyKebabCase) + ScreamingKebab, + /// `Title Case` is the primary name + /// + /// Other accepted names are `TitleCase` + /// + /// [See Also](ToTitleCase) + TitleCase, + /// `Train-Case` is the primary name + /// + /// [See Also](ToTrainCase) + TrainCase, + /// `UPPERCASE` is the primary name + /// + /// This corresponds to the to_uppercase method in [String] + UpperCase, + /// `lowercase` is the primary name + /// + /// This corresponds to the to_lowercase method in [String] + LowerCase, +} +impl AsRef for Case { + fn as_ref(&self) -> &str { + match self { + Case::LowerCamelCase => "lowerCamelCase", + Case::UpperCamelCase => "UpperCamelCase", + Case::Pascal => "PascalCase", + Case::Snake => "snake_case", + Case::ScreamingSnake => "UPPER_SNAKE_CASE", + Case::Kebab => "kebab-case", + Case::ScreamingKebab => "UPPER-KEBAB-CASE", + Case::TitleCase => "Title Case", + Case::TrainCase => "Train-Case", + Case::UpperCase => "UPPERCASE", + Case::LowerCase => "lowercase", + } + } +} +impl Case { + /// Creates a [AsCase] wrapper for the specified case + pub fn as_case>(&self, value: T) -> AsCase { + match self { + Case::LowerCamelCase => AsCase::LowerCamelCase(AsLowerCamelCase(value)), + Case::UpperCamelCase => AsCase::UpperCamelCase(AsUpperCamelCase(value)), + Case::Pascal => AsCase::Pascal(AsUpperCamelCase(value)), + Case::Snake => AsCase::Snake(AsSnakeCase(value)), + Case::ScreamingSnake => AsCase::ShoutySnekCase(AsShoutySnekCase(value)), + Case::Kebab => AsCase::Kebab(AsKebabCase(value)), + Case::ScreamingKebab => AsCase::ShoutyKebab(AsShoutyKebabCase(value)), + Case::TitleCase => AsCase::TitleCase(AsTitleCase(value)), + Case::TrainCase => AsCase::TrainCase(AsTrainCase(value)), + Case::UpperCase => AsCase::UpperCase(value), + Case::LowerCase => AsCase::LowerCase(value), + } + } +} +impl Display for Case { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_ref()) + } +} +/// Implements [FromStr] for [Case] using [phf](https://crates.io/crates/phf) +/// +/// This is only available when the `phf` feature is enabled +/// +/// This can be useful if you are super worried about performance +#[cfg(feature = "phf")] +mod phf_lookup { + use core::str::FromStr; + + use crate::CaseNotFound; + + use super::Case; + use phf::phf_map; + static CASES: phf::Map<&'static str, Case> = phf_map! { + "camelCase" => Case::LowerCamelCase, + "lowerCamelCase" => Case::LowerCamelCase, + "UpperCamelCase" => Case::UpperCamelCase, + "PascalCase" => Case::Pascal, + "lower_snake_case" => Case::Snake, + "snake_case" => Case::Snake, + "snek_case" => Case::Snake, + "UPPER_SNAKE_CASE" => Case::ScreamingSnake, + "SCREAMING_SNAKE_CASE" => Case::ScreamingSnake, + "SHOUTY_SNEK_CASE" => Case::ScreamingSnake, + "lower-kebab-case" => Case::Kebab, + "kebab-case" => Case::Kebab, + "UPPER-KEBAB-CASE" => Case::ScreamingKebab, + "SCREAMING-KEBAB-CASE" => Case::ScreamingKebab, + "TitleCase" => Case::TitleCase, + "Title Case" => Case::TitleCase, + "Train-Case" => Case::TrainCase, + "UPPERCASE" => Case::UpperCase, + "lowercase" => Case::LowerCase, + }; + impl FromStr for Case { + type Err = CaseNotFound; + + fn from_str(s: &str) -> Result { + CASES.get(s).cloned().ok_or_else(|| s.into()) + } + } +} +#[cfg(not(feature = "phf"))] +impl core::str::FromStr for Case { + type Err = CaseNotFound; + + fn from_str(s: &str) -> Result { + match s { + "camelCase" | "lowerCamelCase" => Ok(Case::LowerCamelCase), + "UpperCamelCase" => Ok(Case::UpperCamelCase), + "PascalCase" => Ok(Case::Pascal), + "lower_snake_case" | "snake_case" | "snek_case" => Ok(Case::Snake), + "UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" | "SHOUTY_SNEK_CASE" => { + Ok(Case::ScreamingSnake) + } + "lower-kebab-case" | "kebab-case" => Ok(Case::Kebab), + "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => Ok(Case::ScreamingKebab), + "TitleCase" | "Title Case" => Ok(Case::TitleCase), + "Train-Case" => Ok(Case::TrainCase), + "UPPERCASE" => Ok(Case::UpperCase), + "lowercase" => Ok(Case::LowerCase), + _ => return Result::Err(s.into()), + } + } +} + +impl TryFrom for Case { + type Error = CaseNotFound; + + #[inline(always)] + fn try_from(s: String) -> Result { + s.parse() + } +} + +/// The Trait that defines case conversion +/// This wrapper performs case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// ``` +/// use heck::ToCase; +/// use heck::Case; +/// let original = "We are going to inherit the earth"; +/// let cases = vec![ +/// (Case::LowerCamelCase, "weAreGoingToInheritTheEarth"), +/// (Case::UpperCamelCase, "WeAreGoingToInheritTheEarth"), +/// (Case::Pascal, "WeAreGoingToInheritTheEarth"), +/// (Case::Snake, "we_are_going_to_inherit_the_earth"), +/// (Case::ScreamingSnake, "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), +/// (Case::Kebab, "we-are-going-to-inherit-the-earth"), +/// (Case::ScreamingKebab, "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), +/// (Case::TitleCase, "We Are Going To Inherit The Earth"), +/// (Case::TrainCase, "We-Are-Going-To-Inherit-The-Earth"), +/// (Case::UpperCase, "WE ARE GOING TO INHERIT THE EARTH"), +/// (Case::LowerCase, "we are going to inherit the earth"), +/// ]; +/// +/// for (case, expected) in cases { +/// assert_eq!(original.to_case(case), expected); +/// } +/// ``` +pub trait ToCase: ToOwned { + /// Converts the case of the string to the specified case + + fn to_case(&self, case: Case) -> Self::Owned; + /// Converts the case of the type to the specified case + /// + /// If the case is None, it will return an owned version of the type + fn to_optional_case(&self, case: Option) -> Self::Owned { + match case { + Some(case) => self.to_case(case), + None => self.to_owned(), + } + } +} + +impl ToCase for str { + fn to_case(&self, case: Case) -> Self::Owned { + match case { + Case::LowerCamelCase => self.to_lower_camel_case(), + Case::UpperCamelCase => self.to_upper_camel_case(), + Case::Pascal => self.to_pascal_case(), + Case::Snake => self.to_snake_case(), + Case::ScreamingSnake => self.to_shouty_snake_case(), + Case::Kebab => self.to_kebab_case(), + Case::ScreamingKebab => self.to_shouty_kebab_case(), + Case::TitleCase => self.to_title_case(), + Case::TrainCase => self.to_train_case(), + Case::UpperCase => self.to_uppercase(), + Case::LowerCase => self.to_lowercase(), + } + } +} +/// This wrapper performs case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// ``` +/// use heck::AsCase; +/// use heck::Case; +/// let original = "We are going to inherit the earth"; +/// let cases = vec![ +/// (Case::LowerCamelCase, "weAreGoingToInheritTheEarth"), +/// (Case::UpperCamelCase, "WeAreGoingToInheritTheEarth"), +/// (Case::Pascal, "WeAreGoingToInheritTheEarth"), +/// (Case::Snake, "we_are_going_to_inherit_the_earth"), +/// (Case::ScreamingSnake, "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), +/// (Case::Kebab, "we-are-going-to-inherit-the-earth"), +/// (Case::ScreamingKebab, "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), +/// (Case::TitleCase, "We Are Going To Inherit The Earth"), +/// (Case::TrainCase, "We-Are-Going-To-Inherit-The-Earth"), +/// (Case::UpperCase, "WE ARE GOING TO INHERIT THE EARTH"), +/// (Case::LowerCase, "we are going to inherit the earth"), +/// ]; +/// +/// for (case, expected) in cases { +/// assert_eq!(format!("{}", AsCase::from((original, case))), expected); +/// } +/// ``` +pub enum AsCase> { + /// Wrapper Around [AsLowerCamelCase] + LowerCamelCase(AsLowerCamelCase), + /// Wrapper Around [AsUpperCamelCase] + UpperCamelCase(AsUpperCamelCase), + /// Wrapper Around [AsUpperCamelCase] + Pascal(AsUpperCamelCase), + /// Wrapper Around [AsSnakeCase] + Snake(AsSnakeCase), + /// Wrapper Around [AsShoutySnakeCase] + ShoutySnekCase(AsShoutySnekCase), + /// Wrapper Around [AsKebabCase] + Kebab(AsKebabCase), + /// Wrapper Around [AsShoutyKebabCase] + ShoutyKebab(AsShoutyKebabCase), + /// Wrapper Around [AsTitleCase] + TitleCase(AsTitleCase), + /// Wrapper Around [AsTrainCase] + TrainCase(AsTrainCase), + /// Just calls to_uppercase in [str] + UpperCase(T), + /// Just calls to_lowercase in [str] + LowerCase(T), +} +impl> From<(T, Case)> for AsCase { + fn from((s, case): (T, Case)) -> Self { + case.as_case(s) + } +} + +impl> fmt::Display for AsCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Currently, UpperCase and LowerCase do not have a write to the formatter method + // So we will just convert them to a string and write that + // This is not ideal, but it works + // This could be changed in the future. + match self { + AsCase::LowerCamelCase(s) => s.fmt(f), + AsCase::UpperCamelCase(s) => s.fmt(f), + AsCase::Pascal(s) => s.fmt(f), + AsCase::Snake(s) => s.fmt(f), + AsCase::ShoutySnekCase(s) => s.fmt(f), + AsCase::Kebab(s) => s.fmt(f), + AsCase::ShoutyKebab(s) => s.fmt(f), + AsCase::TitleCase(s) => s.fmt(f), + AsCase::TrainCase(s) => s.fmt(f), + AsCase::UpperCase(s) => write!(f, "{}", s.as_ref().to_uppercase()), + AsCase::LowerCase(s) => write!(f, "{}", s.as_ref().to_lowercase()), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index ab8a015..655f8e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,10 +38,11 @@ //! 8. Train-Case #![deny(missing_docs)] #![forbid(unsafe_code)] -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; +mod cases; mod kebab; mod lower_camel; mod shouty_kebab; @@ -50,7 +51,7 @@ mod snake; mod title; mod train; mod upper_camel; - +pub use cases::{AsCase, Case, CaseNotFound, ToCase}; pub use kebab::{AsKebabCase, ToKebabCase}; pub use lower_camel::{AsLowerCamelCase, ToLowerCamelCase}; pub use shouty_kebab::{AsShoutyKebabCase, ToShoutyKebabCase}; diff --git a/src/shouty_snake.rs b/src/shouty_snake.rs index 54a3556..aa1c9b5 100644 --- a/src/shouty_snake.rs +++ b/src/shouty_snake.rs @@ -31,6 +31,7 @@ pub trait ToShoutySnekCase: ToOwned { } impl ToShoutySnekCase for T { + #[allow(non_snake_case)] fn TO_SHOUTY_SNEK_CASE(&self) -> Self::Owned { self.to_shouty_snake_case() } From bc44b655db314745c1d04cd6b9c62bf5f4f32c34 Mon Sep 17 00:00:00 2001 From: Wyatt Herkamp Date: Sat, 18 Nov 2023 16:07:07 -0500 Subject: [PATCH 2/4] Doc Fix --- src/cases.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cases.rs b/src/cases.rs index 0f80cea..2d4419d 100644 --- a/src/cases.rs +++ b/src/cases.rs @@ -318,7 +318,7 @@ pub enum AsCase> { Pascal(AsUpperCamelCase), /// Wrapper Around [AsSnakeCase] Snake(AsSnakeCase), - /// Wrapper Around [AsShoutySnakeCase] + /// Wrapper Around [AsShoutySnekCase] ShoutySnekCase(AsShoutySnekCase), /// Wrapper Around [AsKebabCase] Kebab(AsKebabCase), From d9895614aeb4fc97df1c3a136ed878d6952419f2 Mon Sep 17 00:00:00 2001 From: Wyatt Herkamp Date: Sat, 18 Nov 2023 16:41:22 -0500 Subject: [PATCH 3/4] Remove PHF --- Cargo.toml | 7 ++----- src/cases.rs | 42 ------------------------------------------ 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d337eb8..0da0ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,7 @@ categories = ["no-std"] include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] [dependencies] -# Optional Dependency if you want to increase the performance of Case from_str -phf = { version = "0.11", default-features = false, features = [ - "macros", -], optional = true } + [features] -std = ["phf?/std"] +std = [] diff --git a/src/cases.rs b/src/cases.rs index 2d4419d..1e9bd00 100644 --- a/src/cases.rs +++ b/src/cases.rs @@ -151,49 +151,7 @@ impl Display for Case { f.write_str(self.as_ref()) } } -/// Implements [FromStr] for [Case] using [phf](https://crates.io/crates/phf) -/// -/// This is only available when the `phf` feature is enabled -/// -/// This can be useful if you are super worried about performance -#[cfg(feature = "phf")] -mod phf_lookup { - use core::str::FromStr; - - use crate::CaseNotFound; - - use super::Case; - use phf::phf_map; - static CASES: phf::Map<&'static str, Case> = phf_map! { - "camelCase" => Case::LowerCamelCase, - "lowerCamelCase" => Case::LowerCamelCase, - "UpperCamelCase" => Case::UpperCamelCase, - "PascalCase" => Case::Pascal, - "lower_snake_case" => Case::Snake, - "snake_case" => Case::Snake, - "snek_case" => Case::Snake, - "UPPER_SNAKE_CASE" => Case::ScreamingSnake, - "SCREAMING_SNAKE_CASE" => Case::ScreamingSnake, - "SHOUTY_SNEK_CASE" => Case::ScreamingSnake, - "lower-kebab-case" => Case::Kebab, - "kebab-case" => Case::Kebab, - "UPPER-KEBAB-CASE" => Case::ScreamingKebab, - "SCREAMING-KEBAB-CASE" => Case::ScreamingKebab, - "TitleCase" => Case::TitleCase, - "Title Case" => Case::TitleCase, - "Train-Case" => Case::TrainCase, - "UPPERCASE" => Case::UpperCase, - "lowercase" => Case::LowerCase, - }; - impl FromStr for Case { - type Err = CaseNotFound; - fn from_str(s: &str) -> Result { - CASES.get(s).cloned().ok_or_else(|| s.into()) - } - } -} -#[cfg(not(feature = "phf"))] impl core::str::FromStr for Case { type Err = CaseNotFound; From 63bde43d7eab29ae9b8c5377edc5f350a91ac8c2 Mon Sep 17 00:00:00 2001 From: Wyatt Herkamp Date: Sun, 19 Nov 2023 09:34:03 -0500 Subject: [PATCH 4/4] Better Error Message --- src/cases.rs | 184 ++++++++++++++++++++++++++++++++------------------- src/lib.rs | 2 +- 2 files changed, 117 insertions(+), 69 deletions(-) diff --git a/src/cases.rs b/src/cases.rs index 1e9bd00..7bd3dfc 100644 --- a/src/cases.rs +++ b/src/cases.rs @@ -1,9 +1,9 @@ -use core::fmt::Display; +use core::{fmt::Display, str::FromStr}; use alloc::{borrow::ToOwned, fmt, string::String}; use crate::{ - AsKebabCase, AsLowerCamelCase, AsShoutyKebabCase, AsShoutySnekCase, AsSnakeCase, AsTitleCase, + AsKebabCase, AsLowerCamelCase, AsShoutyKebabCase, AsShoutySnakeCase, AsSnakeCase, AsTitleCase, AsTrainCase, AsUpperCamelCase, ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToTrainCase, ToUpperCamelCase, }; @@ -18,10 +18,44 @@ impl> From for CaseNotFound { } impl Display for CaseNotFound { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Case not found: {}", self.0) + write!( + f, + "Invalid Case: {:?} expected one of {}", + self.0, EXPTECTED_CASES + ) } } - +/// Implements AsRef, Into, and Display for the specified case +/// Creates a static array of all the case names +macro_rules! variants { + ($($varient:ident => $name:literal),+) => { + #[doc = "Case variants for the `Case` enum"] + pub static CASES: &[&str] = &[$($name),+]; + #[doc = "A string of all the expected case names. Formatted as [`lowerCamelCase`, `upperCamelCase`, ...]"] + static EXPTECTED_CASES: &str = concat!("[", $("`", $name, "`, "),+,"]"); + impl AsRef for Case { + fn as_ref(&self) -> &str { + match self { + $(Case::$varient => $name),+ + } + } + } + impl From for String { + fn from(case: Case) -> Self { + match case { + $(Case::$varient => $name.into()),+ + } + } + } + impl Display for Case { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $(Case::$varient => f.write_str($name)),+ + } + } + } + }; +} #[cfg(feature = "std")] impl std::error::Error for CaseNotFound {} @@ -53,6 +87,7 @@ impl std::error::Error for CaseNotFound {} /// } /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum Case { /// `camelCase` is primary name /// @@ -67,31 +102,31 @@ pub enum Case { /// `PascalCase` is primary name /// /// [See Also](ToPascalCase) - Pascal, + PascalCase, /// `snake_case` is primary name /// /// Other accepted names are `lower_snake_case` /// /// [See Also](ToSnakeCase) - Snake, + SnakeCase, /// `UPPER_SNAKE_CASE` is primary name /// /// Other accepted names are `SCREAMING_SNAKE_CASE` /// /// [See Also](ToShoutySnakeCase) - ScreamingSnake, + ScreamingSnakeCase, /// `kebab-case` is primary name /// /// Other accepted names are `lower-kebab-case` /// /// [See Also](ToKebabCase) - Kebab, + KebabCase, /// `SCREAMING-KEBAB-CASE` is primary name /// /// Other accepted names are `UPPER-KEBAB-CASE` /// /// [See Also](ToShoutyKebabCase) - ScreamingKebab, + ScreamingKebabCase, /// `Title Case` is the primary name /// /// Other accepted names are `TitleCase` @@ -111,34 +146,31 @@ pub enum Case { /// This corresponds to the to_lowercase method in [String] LowerCase, } -impl AsRef for Case { - fn as_ref(&self) -> &str { - match self { - Case::LowerCamelCase => "lowerCamelCase", - Case::UpperCamelCase => "UpperCamelCase", - Case::Pascal => "PascalCase", - Case::Snake => "snake_case", - Case::ScreamingSnake => "UPPER_SNAKE_CASE", - Case::Kebab => "kebab-case", - Case::ScreamingKebab => "UPPER-KEBAB-CASE", - Case::TitleCase => "Title Case", - Case::TrainCase => "Train-Case", - Case::UpperCase => "UPPERCASE", - Case::LowerCase => "lowercase", - } - } -} +variants!( + LowerCamelCase => "lowerCamelCase", + UpperCamelCase => "UpperCamelCase", + PascalCase => "PascalCase", + SnakeCase => "snake_case", + ScreamingSnakeCase => "UPPER_SNAKE_CASE", + KebabCase => "kebab-case", + ScreamingKebabCase => "UPPER-KEBAB-CASE", + TitleCase => "Title Case", + TrainCase => "Train-Case", + UpperCase => "UPPERCASE", + LowerCase => "lowercase" +); + impl Case { /// Creates a [AsCase] wrapper for the specified case pub fn as_case>(&self, value: T) -> AsCase { match self { Case::LowerCamelCase => AsCase::LowerCamelCase(AsLowerCamelCase(value)), Case::UpperCamelCase => AsCase::UpperCamelCase(AsUpperCamelCase(value)), - Case::Pascal => AsCase::Pascal(AsUpperCamelCase(value)), - Case::Snake => AsCase::Snake(AsSnakeCase(value)), - Case::ScreamingSnake => AsCase::ShoutySnekCase(AsShoutySnekCase(value)), - Case::Kebab => AsCase::Kebab(AsKebabCase(value)), - Case::ScreamingKebab => AsCase::ShoutyKebab(AsShoutyKebabCase(value)), + Case::PascalCase => AsCase::PascalCase(AsUpperCamelCase(value)), + Case::SnakeCase => AsCase::SnakeCase(AsSnakeCase(value)), + Case::ScreamingSnakeCase => AsCase::ShoutySnakeCase(AsShoutySnakeCase(value)), + Case::KebabCase => AsCase::KebabCase(AsKebabCase(value)), + Case::ScreamingKebabCase => AsCase::ShoutyKebabCase(AsShoutyKebabCase(value)), Case::TitleCase => AsCase::TitleCase(AsTitleCase(value)), Case::TrainCase => AsCase::TrainCase(AsTrainCase(value)), Case::UpperCase => AsCase::UpperCase(value), @@ -146,31 +178,26 @@ impl Case { } } } -impl Display for Case { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} -impl core::str::FromStr for Case { +impl FromStr for Case { type Err = CaseNotFound; fn from_str(s: &str) -> Result { match s { "camelCase" | "lowerCamelCase" => Ok(Case::LowerCamelCase), "UpperCamelCase" => Ok(Case::UpperCamelCase), - "PascalCase" => Ok(Case::Pascal), - "lower_snake_case" | "snake_case" | "snek_case" => Ok(Case::Snake), + "PascalCase" => Ok(Case::PascalCase), + "lower_snake_case" | "snake_case" | "snek_case" => Ok(Case::SnakeCase), "UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" | "SHOUTY_SNEK_CASE" => { - Ok(Case::ScreamingSnake) + Ok(Case::ScreamingSnakeCase) } - "lower-kebab-case" | "kebab-case" => Ok(Case::Kebab), - "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => Ok(Case::ScreamingKebab), + "lower-kebab-case" | "kebab-case" => Ok(Case::KebabCase), + "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => Ok(Case::ScreamingKebabCase), "TitleCase" | "Title Case" => Ok(Case::TitleCase), "Train-Case" => Ok(Case::TrainCase), "UPPERCASE" => Ok(Case::UpperCase), "lowercase" => Ok(Case::LowerCase), - _ => return Result::Err(s.into()), + _ => Result::Err(s.into()), } } } @@ -183,6 +210,14 @@ impl TryFrom for Case { s.parse() } } +impl TryFrom<&str> for Case { + type Error = CaseNotFound; + + #[inline(always)] + fn try_from(s: &str) -> Result { + s.parse() + } +} /// The Trait that defines case conversion /// This wrapper performs case conversion in [`fmt::Display`]. @@ -195,11 +230,11 @@ impl TryFrom for Case { /// let cases = vec![ /// (Case::LowerCamelCase, "weAreGoingToInheritTheEarth"), /// (Case::UpperCamelCase, "WeAreGoingToInheritTheEarth"), -/// (Case::Pascal, "WeAreGoingToInheritTheEarth"), -/// (Case::Snake, "we_are_going_to_inherit_the_earth"), -/// (Case::ScreamingSnake, "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), -/// (Case::Kebab, "we-are-going-to-inherit-the-earth"), -/// (Case::ScreamingKebab, "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), +/// (Case::PascalCase, "WeAreGoingToInheritTheEarth"), +/// (Case::SnakeCase, "we_are_going_to_inherit_the_earth"), +/// (Case::ScreamingSnakeCase, "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), +/// (Case::KebabCase, "we-are-going-to-inherit-the-earth"), +/// (Case::ScreamingKebabCase, "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), /// (Case::TitleCase, "We Are Going To Inherit The Earth"), /// (Case::TrainCase, "We-Are-Going-To-Inherit-The-Earth"), /// (Case::UpperCase, "WE ARE GOING TO INHERIT THE EARTH"), @@ -230,11 +265,11 @@ impl ToCase for str { match case { Case::LowerCamelCase => self.to_lower_camel_case(), Case::UpperCamelCase => self.to_upper_camel_case(), - Case::Pascal => self.to_pascal_case(), - Case::Snake => self.to_snake_case(), - Case::ScreamingSnake => self.to_shouty_snake_case(), - Case::Kebab => self.to_kebab_case(), - Case::ScreamingKebab => self.to_shouty_kebab_case(), + Case::PascalCase => self.to_pascal_case(), + Case::SnakeCase => self.to_snake_case(), + Case::ScreamingSnakeCase => self.to_shouty_snake_case(), + Case::KebabCase => self.to_kebab_case(), + Case::ScreamingKebabCase => self.to_shouty_kebab_case(), Case::TitleCase => self.to_title_case(), Case::TrainCase => self.to_train_case(), Case::UpperCase => self.to_uppercase(), @@ -252,11 +287,11 @@ impl ToCase for str { /// let cases = vec![ /// (Case::LowerCamelCase, "weAreGoingToInheritTheEarth"), /// (Case::UpperCamelCase, "WeAreGoingToInheritTheEarth"), -/// (Case::Pascal, "WeAreGoingToInheritTheEarth"), -/// (Case::Snake, "we_are_going_to_inherit_the_earth"), -/// (Case::ScreamingSnake, "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), -/// (Case::Kebab, "we-are-going-to-inherit-the-earth"), -/// (Case::ScreamingKebab, "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), +/// (Case::PascalCase, "WeAreGoingToInheritTheEarth"), +/// (Case::SnakeCase, "we_are_going_to_inherit_the_earth"), +/// (Case::ScreamingSnakeCase, "WE_ARE_GOING_TO_INHERIT_THE_EARTH"), +/// (Case::KebabCase, "we-are-going-to-inherit-the-earth"), +/// (Case::ScreamingKebabCase, "WE-ARE-GOING-TO-INHERIT-THE-EARTH"), /// (Case::TitleCase, "We Are Going To Inherit The Earth"), /// (Case::TrainCase, "We-Are-Going-To-Inherit-The-Earth"), /// (Case::UpperCase, "WE ARE GOING TO INHERIT THE EARTH"), @@ -267,21 +302,22 @@ impl ToCase for str { /// assert_eq!(format!("{}", AsCase::from((original, case))), expected); /// } /// ``` +#[non_exhaustive] pub enum AsCase> { /// Wrapper Around [AsLowerCamelCase] LowerCamelCase(AsLowerCamelCase), /// Wrapper Around [AsUpperCamelCase] UpperCamelCase(AsUpperCamelCase), /// Wrapper Around [AsUpperCamelCase] - Pascal(AsUpperCamelCase), + PascalCase(AsUpperCamelCase), /// Wrapper Around [AsSnakeCase] - Snake(AsSnakeCase), + SnakeCase(AsSnakeCase), /// Wrapper Around [AsShoutySnekCase] - ShoutySnekCase(AsShoutySnekCase), + ShoutySnakeCase(AsShoutySnakeCase), /// Wrapper Around [AsKebabCase] - Kebab(AsKebabCase), + KebabCase(AsKebabCase), /// Wrapper Around [AsShoutyKebabCase] - ShoutyKebab(AsShoutyKebabCase), + ShoutyKebabCase(AsShoutyKebabCase), /// Wrapper Around [AsTitleCase] TitleCase(AsTitleCase), /// Wrapper Around [AsTrainCase] @@ -296,6 +332,18 @@ impl> From<(T, Case)> for AsCase { case.as_case(s) } } +/// Converts the case from the name and creates an AsCase wrapper +/// +/// # Arguments +/// - First argument is the string to convert +/// - Second argument is the name of the case +impl> TryFrom<(T, &str)> for AsCase { + type Error = CaseNotFound; + + fn try_from((s, case): (T, &str)) -> Result { + Case::from_str(case).map(|v| v.as_case(s)) + } +} impl> fmt::Display for AsCase { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -306,11 +354,11 @@ impl> fmt::Display for AsCase { match self { AsCase::LowerCamelCase(s) => s.fmt(f), AsCase::UpperCamelCase(s) => s.fmt(f), - AsCase::Pascal(s) => s.fmt(f), - AsCase::Snake(s) => s.fmt(f), - AsCase::ShoutySnekCase(s) => s.fmt(f), - AsCase::Kebab(s) => s.fmt(f), - AsCase::ShoutyKebab(s) => s.fmt(f), + AsCase::PascalCase(s) => s.fmt(f), + AsCase::SnakeCase(s) => s.fmt(f), + AsCase::ShoutySnakeCase(s) => s.fmt(f), + AsCase::KebabCase(s) => s.fmt(f), + AsCase::ShoutyKebabCase(s) => s.fmt(f), AsCase::TitleCase(s) => s.fmt(f), AsCase::TrainCase(s) => s.fmt(f), AsCase::UpperCase(s) => write!(f, "{}", s.as_ref().to_uppercase()), diff --git a/src/lib.rs b/src/lib.rs index 655f8e3..023beb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ mod snake; mod title; mod train; mod upper_camel; -pub use cases::{AsCase, Case, CaseNotFound, ToCase}; +pub use cases::{AsCase, Case, CaseNotFound, ToCase, CASES}; pub use kebab::{AsKebabCase, ToKebabCase}; pub use lower_camel::{AsLowerCamelCase, ToLowerCamelCase}; pub use shouty_kebab::{AsShoutyKebabCase, ToShoutyKebabCase};