-
Notifications
You must be signed in to change notification settings - Fork 34
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
Changes from all commits
6c384d8
9ac1c74
894eab6
523cf82
2dbb21f
f48ec3e
7bae08a
3051136
b94a2b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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` |
This file was deleted.
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 | ||||||
/// | ||||||
/// | ||||||
/// ## Example: | ||||||
/// ```rust | ||||||
/// | ||||||
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||||||
/// | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please derive |
||||||
/// 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a nicer way to express this function would be 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be named 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"); | ||||||
} |
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. | ||||||||||||||||||
/// | ||||||||||||||||||
|
@@ -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 { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the proposed changes to the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the proposed changes to the
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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), | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
@@ -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); | ||
} | ||
|
There was a problem hiding this comment.
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.