Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConvertCase trait as a wrapper for other convert case functions #27

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
root = true

[*]
charset = utf-8

end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

indent_style = space
indent_size = 4

[*.md]
trim_trailing_whitespace = true

[*.yml]
indent_size = 2
11 changes: 9 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@ jobs:

steps:
- uses: actions/checkout@v2
# Use MSRV for the build job
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.32
default: true
profile: minimal
components: rustfmt, clippy
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
# Use stable for other jobs
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
profile: minimal
components: rustfmt, clippy
- name: Run tests
uses: actions-rs/cargo@v1
with:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 0.4.0 (unreleased)

Breaking changes:

* Rename all traits from `SomeCase` to `ToSomeCase`, matching `std`s convention
of beginning trait names with a verb (`ToOwned`, `AsRef`, …)
* Rename `ToMixedCase` to `ToLowerCamelCase`
* Rename `ToCamelCase` to `ToUpperCamelCase`
* Add `ToPascalCase` as an alias to `ToUpperCamelCase`
50 changes: 0 additions & 50 deletions src/camel.rs

This file was deleted.

101 changes: 101 additions & 0 deletions src/convert_case.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use crate::kebab::to_kebab;
use crate::lower_camel::to_lower_camel_case;
use crate::shouty_kebab::to_shouty_kebab_case;
use crate::shouty_snake::to_shouty_snake_case;
use crate::snake::to_snake_case;
use crate::title::to_title_case;
use crate::upper_camel::to_upper_camel_case;

/// This trait defines a wrapper function for other case-conversion functions
/// in that this can tweak the behaviour of those functions based on customization options
///
///
Comment on lines +11 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be one empty doc comment line.

/// ## Example:
/// ```rust
///
Comment on lines +14 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swap these two lines (there should not be an empty line at the start of the code block).

/// use heck::{ConvertCase, ConvertCaseOpt, Case};
///
/// let sentence = "Aes128";
/// assert_eq!(sentence.convert_case(ConvertCaseOpt { case: Case::ShoutyKebab, number_starts_word: true }),
/// "AES-128");
///
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code block should explicitly be terminated:

Suggested change
///
/// ```


pub trait ConvertCase: ToOwned {
/// Convert this type to supported cases with options
fn convert_case(&self, opt: ConvertCaseOpt) -> String;
}

/// Options to tweak how convert_case will behave
pub struct ConvertCaseOpt {
/// supported case
pub case: Case,
/// whether numbers should start a new word
pub number_starts_word: bool,
}

/// supported cases
pub enum Case {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please derive Clone, Copy and Debug.

/// kebab-case
Kebab,
/// lowerCamelCase
LowerCamel,
/// SHOUT-KEBAB-CASE
ShoutyKebab,
/// SHOUTY_SNAKE_CASE
ShoutySnake,
/// snake_case
Snake,
/// Title Case
Title,
/// UpperCamelCase
UpperCamel,
}

pub fn convert_case(s: &str, opt: ConvertCaseOpt) -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a separate free function? I think the body of this should just be moved into the trait implementation below.

match opt.case {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a nicer way to express this function would be transform(s, number_starts_word, case.with_word(), case.boundary()) where

impl Case {
    pub(crate) fn with_word(self) -> fn(&str, &mut String) { ... }
    pub(crate) fn boundary(self) -> fn(&mut String) { ... }
}

represent the actual differing logic that was previously in the individual trait implementations.

Case::Kebab => to_kebab(s, opt.number_starts_word),
Case::LowerCamel => to_lower_camel_case(s, opt.number_starts_word),
Case::ShoutyKebab => to_shouty_kebab_case(s, opt.number_starts_word),
Case::ShoutySnake => to_shouty_snake_case(s, opt.number_starts_word),
Case::Snake => to_snake_case(s, opt.number_starts_word),
Case::Title => to_title_case(s, opt.number_starts_word),
Case::UpperCamel => to_upper_camel_case(s, opt.number_starts_word),
}
}
impl ConvertCase for str {
fn convert_case(&self, opt: ConvertCaseOpt) -> Self::Owned {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be named convert_case_with, and convert_case should be

fn convert_case(&self, case: Case) -> Self::Owned {
    self.convert_case_with(ConvertCaseOpt { case, number_starts_word: false })
}

convert_case(self, opt)
}
}

#[cfg(test)]
mod tests {
use crate::{Case, ConvertCase, ConvertCaseOpt};

macro_rules! t {
($t:ident: $s1:expr, $c:ident, $n:ident => $s2:expr) => {
#[test]
fn $t() {
assert_eq!(
$s1.convert_case(ConvertCaseOpt {
case: Case::$c,
number_starts_word: $n
}),
$s2
)
}
};
}

t!(test1: "AES 128 bit key", LowerCamel, false => "aes128BitKey");
t!(test2: "AES 128 bit key", LowerCamel, true => "aes128BitKey");
t!(test3: "AES 128 bit key", Kebab, true => "aes-128-bit-key");
t!(test4: "99BOTTLES", Snake, false => "99bottles");
t!(test5: "99BOTTLES", Snake, true => "99_bottles");
t!(test6: "ABC123dEEf456FOO", Snake, false => "abc123d_e_ef456_foo");
t!(test7: "ABC123dEEf456FOO", Snake, true => "abc_123_d_e_ef_456_foo");
t!(test8: "XMLHttpRequest404", Title, false => "Xml Http Request404");
t!(test9: "XMLHttpRequest404", Title, true => "Xml Http Request 404");
t!(test10: "this-contains_ ALLKinds OfWord_Boundaries_1Also", Kebab, false => "this-contains-all-kinds-of-word-boundaries-1also");
t!(test11: "this-contains_ ALLKinds OfWord_Boundaries_1Also", Kebab, true => "this-contains-all-kinds-of-word-boundaries-1-also");
}
23 changes: 17 additions & 6 deletions src/kebab.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{lowercase, transform};
use crate::convert_case::convert_case;
use crate::{lowercase, transform, Case, ConvertCaseOpt};

/// This trait defines a kebab case conversion.
///
Expand All @@ -7,25 +8,35 @@ use crate::{lowercase, transform};
/// ## Example:
///
/// ```rust
/// use heck::KebabCase;
/// use heck::ToKebabCase;
///
/// let sentence = "We are going to inherit the earth.";
/// assert_eq!(sentence.to_kebab_case(), "we-are-going-to-inherit-the-earth");
/// ```
pub trait KebabCase: ToOwned {
pub trait ToKebabCase: ToOwned {
/// Convert this type to kebab case.
fn to_kebab_case(&self) -> Self::Owned;
}

impl KebabCase for str {
pub fn to_kebab(s: &str, number_starts_word: bool) -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the proposed changes to the convert_case module, this function doesn't need to exist.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the other modules.

transform(s, number_starts_word, lowercase, |s| s.push('-'))
}

impl ToKebabCase for str {
fn to_kebab_case(&self) -> Self::Owned {
transform(self, lowercase, |s| s.push('-'))
convert_case(
&self,
ConvertCaseOpt {
case: Case::Kebab,
number_starts_word: false,
},
)
Comment on lines +27 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the proposed changes to the convert_case module, this implementation simply becomes

Suggested change
convert_case(
&self,
ConvertCaseOpt {
case: Case::Kebab,
number_starts_word: false,
},
)
self.convert_case(Case::Kebab)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the other modules.

}
}

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

macro_rules! t {
($t:ident : $s1:expr => $s2:expr) => {
Expand Down
44 changes: 32 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,29 @@
//! 6. Title Case
//! 7. SHOUTY-KEBAB-CASE
#![deny(missing_docs)]
#![forbid(unsafe_code)]

mod camel;
mod convert_case;
mod kebab;
mod mixed;
mod lower_camel;
mod shouty_kebab;
mod shouty_snake;
mod snake;
mod title;
mod upper_camel;

pub use camel::CamelCase;
pub use kebab::KebabCase;
pub use mixed::MixedCase;
pub use shouty_kebab::ShoutyKebabCase;
pub use shouty_snake::{ShoutySnakeCase, ShoutySnekCase};
pub use snake::{SnakeCase, SnekCase};
pub use title::TitleCase;
pub use convert_case::{Case, ConvertCase, ConvertCaseOpt};
pub use kebab::ToKebabCase;
pub use lower_camel::ToLowerCamelCase;
pub use shouty_kebab::ToShoutyKebabCase;
pub use shouty_snake::{ToShoutySnakeCase, ToShoutySnekCase};
pub use snake::{ToSnakeCase, ToSnekCase};
pub use title::ToTitleCase;
pub use upper_camel::{ToPascalCase, ToUpperCamelCase};

use unicode_segmentation::UnicodeSegmentation;

fn transform<F, G>(s: &str, with_word: F, boundary: G) -> String
fn transform<F, G>(s: &str, number_starts_word: bool, with_word: F, boundary: G) -> String
where
F: Fn(&str, &mut String),
G: Fn(&mut String),
Expand All @@ -80,6 +83,8 @@ where
Lowercase,
/// The previous cased character in the current word is uppercase.
Uppercase,
/// The previous cased character in the current word is numeric
Numeric,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected the logic change in transform to look different, but I also haven't thought about it much. Will review this part in more detail later.

}

let mut out = String::new();
Expand All @@ -106,13 +111,28 @@ where
WordMode::Lowercase
} else if c.is_uppercase() {
WordMode::Uppercase

// set numeric only if number_starts_word
// so that it does not affect regular processing
// when number_starts_word is false
} else if number_starts_word && c.is_numeric() {
WordMode::Numeric
} else {
mode
};

// Word boundary after if next is underscore or current is
// When number_starts_word is false: Word boundary after if next is underscore or current is
// not uppercase and next is uppercase
if next == '_' || (next_mode == WordMode::Lowercase && next.is_uppercase()) {
// When number_starts_word is true: word boundary after when mode changes from alpha to numeric or numeric to alpha
if next == '_'
|| (next_mode == WordMode::Lowercase && next.is_uppercase())
|| (number_starts_word
&& next_mode == WordMode::Numeric
&& (next.is_uppercase() || next.is_lowercase()))
|| (number_starts_word
&& next.is_numeric()
&& (next_mode == WordMode::Lowercase || next_mode == WordMode::Uppercase))
{
if !first_word {
boundary(&mut out);
}
Expand Down
Loading