diff --git a/src/camel.rs b/src/camel.rs index 74bd741..713d221 100644 --- a/src/camel.rs +++ b/src/camel.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// This trait defines a camel case conversion. /// /// In CamelCase, word boundaries are indicated by capital letters, including @@ -22,7 +24,28 @@ pub trait CamelCase: ToOwned { impl CamelCase for str { fn to_camel_case(&self) -> String { - ::transform(self, ::capitalize, |_| {}) + AsCamelCase(self).to_string() + } +} + +/// This wrapper performs a camel case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// +/// ``` +/// extern crate heck; +/// fn main() { +/// use heck::AsCamelCase; +/// +/// let sentence = "We are not in the least afraid of ruins."; +/// assert_eq!(format!("{}", AsCamelCase(sentence)), "WeAreNotInTheLeastAfraidOfRuins"); +/// } +/// ``` +pub struct AsCamelCase>(pub T); + +impl> fmt::Display for AsCamelCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ::transform(self.0.as_ref(), ::capitalize, |_| Ok(()), f) } } diff --git a/src/kebab.rs b/src/kebab.rs index f81ba92..f807770 100644 --- a/src/kebab.rs +++ b/src/kebab.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// This trait defines a kebab case conversion. /// /// In kebab-case, word boundaries are indicated by hyphens. @@ -21,7 +23,28 @@ pub trait KebabCase: ToOwned { impl KebabCase for str { fn to_kebab_case(&self) -> Self::Owned { - ::transform(self, ::lowercase, |s| s.push('-')) + AsKebabCase(self).to_string() + } +} + +/// This wrapper performs a kebab case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// +/// ``` +/// extern crate heck; +/// fn main() { +/// use heck::AsKebabCase; +/// +/// let sentence = "We are going to inherit the earth."; +/// assert_eq!(format!("{}", AsKebabCase(sentence)), "we-are-going-to-inherit-the-earth"); +/// } +/// ``` +pub struct AsKebabCase>(pub T); + +impl> fmt::Display for AsKebabCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ::transform(self.0.as_ref(), ::lowercase, |f| write!(f, "-"), f) } } diff --git a/src/lib.rs b/src/lib.rs index c35ba34..4133175 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,19 +44,21 @@ mod shouty_snake; mod snake; mod title; -pub use camel::CamelCase; -pub use kebab::KebabCase; -pub use mixed::MixedCase; -pub use shouty_snake::{ShoutySnakeCase, ShoutySnekCase}; -pub use snake::{SnakeCase, SnekCase}; -pub use title::TitleCase; +pub use camel::{AsCamelCase, CamelCase}; +pub use kebab::{AsKebabCase, KebabCase}; +pub use mixed::{AsMixedCase, MixedCase}; +pub use shouty_snake::{AsShoutySnakeCase, AsShoutySnekCase, ShoutySnakeCase, ShoutySnekCase}; +pub use snake::{AsSnakeCase, AsSnekCase, SnakeCase, SnekCase}; +pub use title::{AsTitleCase, TitleCase}; + +use std::fmt; use unicode_segmentation::UnicodeSegmentation; -fn transform(s: &str, with_word: F, boundary: G) -> String +fn transform(s: &str, mut with_word: F, mut boundary: G, f: &mut fmt::Formatter) -> fmt::Result where - F: Fn(&str, &mut String), - G: Fn(&mut String) + F: FnMut(&str, &mut fmt::Formatter) -> fmt::Result, + G: FnMut(&mut fmt::Formatter) -> fmt::Result { /// Tracks the current 'mode' of the transformation algorithm as it scans the input string. @@ -75,7 +77,6 @@ where Uppercase, } - let mut out = String::new(); let mut first_word = true; for word in s.unicode_words() { @@ -105,8 +106,8 @@ where // 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()) { - if !first_word { boundary(&mut out); } - with_word(&word[init..next_i], &mut out); + if !first_word { boundary(&mut *f)?; } + with_word(&word[init..next_i], &mut *f)?; first_word = false; init = next_i; mode = WordMode::Boundary; @@ -114,9 +115,9 @@ where // Otherwise if current and previous are uppercase and next // is lowercase, word boundary before } else if mode == WordMode::Uppercase && c.is_uppercase() && next.is_lowercase() { - if !first_word { boundary(&mut out); } + if !first_word { boundary(&mut *f)?; } else { first_word = false; } - with_word(&word[init..i], &mut out); + with_word(&word[init..i], &mut *f)?; init = i; mode = WordMode::Boundary; @@ -126,40 +127,46 @@ where } } else { // Collect trailing characters as a word - if !first_word { boundary(&mut out); } + if !first_word { boundary(&mut *f)?; } else { first_word = false; } - with_word(&word[init..], &mut out); + with_word(&word[init..], &mut *f)?; break; } } } - out + Ok(()) } -fn lowercase(s: &str, out: &mut String) { +fn lowercase(s: &str, f: &mut fmt::Formatter) -> fmt::Result { let mut chars = s.chars().peekable(); while let Some(c) = chars.next() { if c == 'Σ' && chars.peek().is_none() { - out.push('ς'); + write!(f, "ς")?; } else { - out.extend(c.to_lowercase()); + write!(f, "{}", c.to_lowercase())?; } } + + Ok(()) } -fn uppercase(s: &str, out: &mut String ) { +fn uppercase(s: &str, f: &mut fmt::Formatter) -> fmt::Result { for c in s.chars() { - out.extend(c.to_uppercase()) + write!(f, "{}", c.to_uppercase())?; } + + Ok(()) } -fn capitalize(s: &str, out: &mut String) { +fn capitalize(s: &str, f: &mut fmt::Formatter) -> fmt::Result { let mut char_indices = s.char_indices(); if let Some((_, c)) = char_indices.next() { - out.extend(c.to_uppercase()); + write!(f, "{}", c.to_uppercase())?; if let Some((i, _)) = char_indices.next() { - lowercase(&s[i..], out); + lowercase(&s[i..], &mut *f)?; } } + + Ok(()) } diff --git a/src/mixed.rs b/src/mixed.rs index 7736684..39374cc 100644 --- a/src/mixed.rs +++ b/src/mixed.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// This trait defines a mixed case conversion. /// /// In mixedCase, word boundaries are indicated by capital letters, excepting @@ -22,10 +24,34 @@ pub trait MixedCase: ToOwned { impl MixedCase for str { fn to_mixed_case(&self) -> String { - ::transform(self, |s, out| { - if out.is_empty() { ::lowercase(s, out); } - else { ::capitalize(s, out) } - }, |_| {}) + AsMixedCase(self).to_string() + } +} + +/// This wrapper performs a mixed case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// +/// ``` +/// extern crate heck; +/// fn main() { +/// use heck::AsMixedCase; +/// +/// let sentence = "It is we who built these palaces and cities."; +/// assert_eq!(format!("{}", AsMixedCase(sentence)), "itIsWeWhoBuiltThesePalacesAndCities"); +/// } +/// ``` +pub struct AsMixedCase>(pub T); + +impl> fmt::Display for AsMixedCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut first = true; + ::transform(self.0.as_ref(), |s, out| { + if first { + first = false; + ::lowercase(s, out) + } else { ::capitalize(s, out) } + }, |_| Ok(()), f) } } diff --git a/src/shouty_snake.rs b/src/shouty_snake.rs index 0f846c3..175bf84 100644 --- a/src/shouty_snake.rs +++ b/src/shouty_snake.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// This trait defines a shouty snake case conversion. /// /// In SHOUTY_SNAKE_CASE, word boundaries are indicated by underscores and all @@ -37,10 +39,34 @@ impl ShoutySnekCase for T { impl ShoutySnakeCase for str { fn to_shouty_snake_case(&self) -> Self::Owned { - ::transform(self, ::uppercase, |s| s.push('_')) + AsShoutySnakeCase(self).to_string() + } +} + +/// This wrapper performs a camel case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// +/// ``` +/// extern crate heck; +/// fn main() { +/// use heck::AsShoutySnakeCase; +/// +/// let sentence = "That world is growing in this minute."; +/// assert_eq!(format!("{}", AsShoutySnakeCase(sentence)), "THAT_WORLD_IS_GROWING_IN_THIS_MINUTE"); +/// } +/// ``` +pub struct AsShoutySnakeCase>(pub T); + +impl> fmt::Display for AsShoutySnakeCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ::transform(self.0.as_ref(), ::uppercase, |f| write!(f, "_"), f) } } +/// Oh heck, AsShoutySnekCase is an alias for AsShoutySnakeCase. See AsShoutySnakeCase for more documentation. +pub use AsShoutySnakeCase as AsShoutySnekCase; + #[cfg(test)] mod tests { use super::ShoutySnakeCase; diff --git a/src/snake.rs b/src/snake.rs index 86c3756..7bc88df 100644 --- a/src/snake.rs +++ b/src/snake.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// This trait defines a camel case conversion. /// /// In snake_case, word boundaries are indicated by underscores. @@ -34,10 +36,34 @@ impl SnekCase for T { impl SnakeCase for str { fn to_snake_case(&self) -> String { - ::transform(self, ::lowercase, |s| s.push('_')) + AsSnakeCase(self).to_string() } } +/// This wrapper performs a snake case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// +/// ``` +/// extern crate heck; +/// fn main() { +/// use heck::AsSnakeCase; +/// +/// let sentence = "We carry a new world here, in our hearts."; +/// assert_eq!(format!("{}", AsSnakeCase(sentence)), "we_carry_a_new_world_here_in_our_hearts"); +/// } +/// ``` +pub struct AsSnakeCase>(pub T); + +impl> fmt::Display for AsSnakeCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ::transform(self.0.as_ref(), ::lowercase, |f| write!(f, "_"), f) + } +} + +/// Oh heck, AsSnekCase is an alias for AsSnakeCase. See AsSnakeCase for more documentation. +pub use AsSnakeCase as AsSnekCase; + #[cfg(test)] mod tests { use super::SnakeCase; diff --git a/src/title.rs b/src/title.rs index cb48302..31a4b1b 100644 --- a/src/title.rs +++ b/src/title.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// This trait defines a title case conversion. /// /// In Title Case, word boundaries are indicated by spaces, and every word is @@ -22,7 +24,28 @@ pub trait TitleCase: ToOwned { impl TitleCase for str { fn to_title_case(&self) -> String { - ::transform(self, ::capitalize, |s| s.push(' ')) + AsTitleCase(self).to_string() + } +} + +/// This wrapper performs a title case conversion in [`fmt::Display`]. +/// +/// ## Example: +/// +/// ``` +/// extern crate heck; +/// fn main() { +/// use heck::AsTitleCase; +/// +/// let sentence = "We have always lived in slums and holes in the wall."; +/// assert_eq!(format!("{}", AsTitleCase(sentence)), "We Have Always Lived In Slums And Holes In The Wall"); +/// } +/// ``` +pub struct AsTitleCase>(pub T); + +impl> fmt::Display for AsTitleCase { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + ::transform(self.0.as_ref(), ::capitalize, |f| write!(f, " "), f) } }