From f6ff6ab6e49252de4018470a4adfa3379445656a Mon Sep 17 00:00:00 2001 From: Rick Richardson Date: Mon, 17 Aug 2020 13:18:23 -0700 Subject: [PATCH] =?UTF-8?q?added=20various=20case=20conversion=20commands?= =?UTF-8?q?=20for=20str.=20Added=20the=20inflection=20=E2=80=A6=20(#2363)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added various case conversion commands for str. Added the inflection crate as a dependency * lighten the restriction on the inflector dependency * publishing the case commands * fix typo * fix kebab case test * formatting --- crates/nu-cli/Cargo.toml | 1 + crates/nu-cli/src/cli.rs | 5 ++ crates/nu-cli/src/commands.rs | 5 +- .../src/commands/str_/case/camel_case.rs | 74 +++++++++++++++++ .../src/commands/str_/case/kebab_case.rs | 74 +++++++++++++++++ crates/nu-cli/src/commands/str_/case/mod.rs | 79 +++++++++++++++++++ .../src/commands/str_/case/pascal_case.rs | 74 +++++++++++++++++ .../str_/case/screaming_snake_case.rs | 74 +++++++++++++++++ .../src/commands/str_/case/snake_case.rs | 74 +++++++++++++++++ crates/nu-cli/src/commands/str_/mod.rs | 6 ++ crates/nu-cli/tests/commands/str_/mod.rs | 20 +++++ 11 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 crates/nu-cli/src/commands/str_/case/camel_case.rs create mode 100644 crates/nu-cli/src/commands/str_/case/kebab_case.rs create mode 100644 crates/nu-cli/src/commands/str_/case/mod.rs create mode 100644 crates/nu-cli/src/commands/str_/case/pascal_case.rs create mode 100644 crates/nu-cli/src/commands/str_/case/screaming_snake_case.rs create mode 100644 crates/nu-cli/src/commands/str_/case/snake_case.rs diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 0607d0d42..d669f5392 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -97,6 +97,7 @@ quick-xml = "0.18.1" rayon = "1.3.1" trash = {version = "1.0.1", optional = true} url = {version = "2.1.1"} +Inflector = "0.11" [target.'cfg(unix)'.dependencies] users = "0.10.0" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index af5c3ab0d..f5a22278d 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -320,6 +320,11 @@ pub fn create_default_context( whole_stream_command(StrCollect), whole_stream_command(StrLength), whole_stream_command(StrReverse), + whole_stream_command(StrCamelCase), + whole_stream_command(StrPascalCase), + whole_stream_command(StrKebabCase), + whole_stream_command(StrSnakeCase), + whole_stream_command(StrScreamingSnakeCase), whole_stream_command(BuildString), whole_stream_command(Ansi), whole_stream_command(Char), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index ab38c80b1..1e4acf302 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -231,8 +231,9 @@ pub(crate) use sort_by::SortBy; pub(crate) use split::{Split, SplitChars, SplitColumn, SplitRow}; pub(crate) use split_by::SplitBy; pub(crate) use str_::{ - Str, StrCapitalize, StrCollect, StrContains, StrDowncase, StrEndsWith, StrFindReplace, StrFrom, - StrIndexOf, StrLength, StrReverse, StrSet, StrStartsWith, StrSubstring, StrToDatetime, + Str, StrCamelCase, StrCapitalize, StrCollect, StrContains, StrDowncase, StrEndsWith, + StrFindReplace, StrFrom, StrIndexOf, StrKebabCase, StrLength, StrPascalCase, StrReverse, + StrScreamingSnakeCase, StrSet, StrSnakeCase, StrStartsWith, StrSubstring, StrToDatetime, StrToDecimal, StrToInteger, StrTrim, StrTrimLeft, StrTrimRight, StrUpcase, }; pub(crate) use table::Table; diff --git a/crates/nu-cli/src/commands/str_/case/camel_case.rs b/crates/nu-cli/src/commands/str_/case/camel_case.rs new file mode 100644 index 000000000..30440d81e --- /dev/null +++ b/crates/nu-cli/src/commands/str_/case/camel_case.rs @@ -0,0 +1,74 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use inflector::cases::camelcase::to_camel_case; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str camel-case" + } + + fn signature(&self) -> Signature { + Signature::build("str camel-case").rest( + SyntaxShape::ColumnPath, + "optionally convert text to camelCase by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to camelCase" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &to_camel_case).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "convert a string to camelCase", + example: "echo 'NuShell' | str camel-case", + result: Some(vec![Value::from("nuShell")]), + }] + } +} + +#[cfg(test)] +mod tests { + use super::{to_camel_case, SubCommand}; + use crate::commands::str_::case::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn camel_case_from_kebab() { + let word = string("this-is-the-first-case"); + let expected = string("thisIsTheFirstCase"); + + let actual = action(&word, Tag::unknown(), &to_camel_case).unwrap(); + assert_eq!(actual, expected); + } + #[test] + fn camel_case_from_snake() { + let word = string("this_is_the_second_case"); + let expected = string("thisIsTheSecondCase"); + + let actual = action(&word, Tag::unknown(), &to_camel_case).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/case/kebab_case.rs b/crates/nu-cli/src/commands/str_/case/kebab_case.rs new file mode 100644 index 000000000..c43b5266a --- /dev/null +++ b/crates/nu-cli/src/commands/str_/case/kebab_case.rs @@ -0,0 +1,74 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use inflector::cases::kebabcase::to_kebab_case; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str kebab-case" + } + + fn signature(&self) -> Signature { + Signature::build("str kebab-case").rest( + SyntaxShape::ColumnPath, + "optionally convert text to kebab-case by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to kebab-case" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &to_kebab_case).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "convert a string to kebab-case", + example: "echo 'NuShell' | str kebab-case", + result: Some(vec![Value::from("nu-shell")]), + }] + } +} + +#[cfg(test)] +mod tests { + use super::{to_kebab_case, SubCommand}; + use crate::commands::str_::case::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn kebab_case_from_camel() { + let word = string("thisIsTheFirstCase"); + let expected = string("this-is-the-first-case"); + + let actual = action(&word, Tag::unknown(), &to_kebab_case).unwrap(); + assert_eq!(actual, expected); + } + #[test] + fn kebab_case_from_screaming_snake() { + let word = string("THIS_IS_THE_SECOND_CASE"); + let expected = string("this-is-the-second-case"); + + let actual = action(&word, Tag::unknown(), &to_kebab_case).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/case/mod.rs b/crates/nu-cli/src/commands/str_/case/mod.rs new file mode 100644 index 000000000..74054d891 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/case/mod.rs @@ -0,0 +1,79 @@ +pub mod camel_case; +pub mod kebab_case; +pub mod pascal_case; +pub mod screaming_snake_case; +pub mod snake_case; + +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::ShellTypeName; +use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, UntaggedValue, Value}; +use nu_source::Tag; +use nu_value_ext::ValueExt; + +pub use camel_case::SubCommand as CamelCase; +pub use pascal_case::SubCommand as PascalCase; +pub use screaming_snake_case::SubCommand as ScreamingSnakeCase; +pub use snake_case::SubCommand as SnakeCase; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub async fn operate( + args: CommandArgs, + registry: &CommandRegistry, + case_operation: &'static F, +) -> Result +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + let registry = registry.clone(); + + let (Arguments { rest }, input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest; + Ok(input + .map(move |v| { + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, v.tag(), &case_operation)?) + } else { + let mut ret = v; + + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, old.tag(), &case_operation)), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +pub fn action( + input: &Value, + tag: impl Into, + case_operation: &F, +) -> Result +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + Ok(UntaggedValue::string(case_operation(s)).into_value(tag)) + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not string", + got, + tag.into().span, + )) + } + } +} diff --git a/crates/nu-cli/src/commands/str_/case/pascal_case.rs b/crates/nu-cli/src/commands/str_/case/pascal_case.rs new file mode 100644 index 000000000..113b77ba1 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/case/pascal_case.rs @@ -0,0 +1,74 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use inflector::cases::pascalcase::to_pascal_case; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str pascal-case" + } + + fn signature(&self) -> Signature { + Signature::build("str pascal-case").rest( + SyntaxShape::ColumnPath, + "optionally convert text to PascalCase by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to PascalCase" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &to_pascal_case).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "convert a string to PascalCase", + example: "echo 'nu-shell' | str pascal-case", + result: Some(vec![Value::from("NuShell")]), + }] + } +} + +#[cfg(test)] +mod tests { + use super::{to_pascal_case, SubCommand}; + use crate::commands::str_::case::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn pascal_case_from_kebab() { + let word = string("this-is-the-first-case"); + let expected = string("ThisIsTheFirstCase"); + + let actual = action(&word, Tag::unknown(), &to_pascal_case).unwrap(); + assert_eq!(actual, expected); + } + #[test] + fn pascal_case_from_snake() { + let word = string("this_is_the_second_case"); + let expected = string("ThisIsTheSecondCase"); + + let actual = action(&word, Tag::unknown(), &to_pascal_case).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/case/screaming_snake_case.rs b/crates/nu-cli/src/commands/str_/case/screaming_snake_case.rs new file mode 100644 index 000000000..726c92b7c --- /dev/null +++ b/crates/nu-cli/src/commands/str_/case/screaming_snake_case.rs @@ -0,0 +1,74 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use inflector::cases::screamingsnakecase::to_screaming_snake_case; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str screaming-snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str screaming-snake-case").rest( + SyntaxShape::ColumnPath, + "optionally convert text to SCREAMING_SNAKE_CASE by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to SCREAMING_SNAKE_CASE" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &to_screaming_snake_case).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "convert a string to SCREAMING_SNAKE_CASE", + example: "echo 'NuShell' | str screaming-snake-case", + result: Some(vec![Value::from("NU_SHELL")]), + }] + } +} + +#[cfg(test)] +mod tests { + use super::{to_screaming_snake_case, SubCommand}; + use crate::commands::str_::case::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn snake_case_from_kebab() { + let word = string("this-is-the-first-case"); + let expected = string("THIS_IS_THE_FIRST_CASE"); + + let actual = action(&word, Tag::unknown(), &to_screaming_snake_case).unwrap(); + assert_eq!(actual, expected); + } + #[test] + fn snake_case_from_snake() { + let word = string("this_is_the_second_case"); + let expected = string("THIS_IS_THE_SECOND_CASE"); + + let actual = action(&word, Tag::unknown(), &to_screaming_snake_case).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/case/snake_case.rs b/crates/nu-cli/src/commands/str_/case/snake_case.rs new file mode 100644 index 000000000..cb6bfcf42 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/case/snake_case.rs @@ -0,0 +1,74 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use inflector::cases::snakecase::to_snake_case; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str snake-case").rest( + SyntaxShape::ColumnPath, + "optionally convert text to snake_case by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to snake_case" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &to_snake_case).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "convert a string to snake_case", + example: "echo 'NuShell' | str snake-case", + result: Some(vec![Value::from("nu_shell")]), + }] + } +} + +#[cfg(test)] +mod tests { + use super::{to_snake_case, SubCommand}; + use crate::commands::str_::case::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn snake_case_from_kebab() { + let word = string("this-is-the-first-case"); + let expected = string("this_is_the_first_case"); + + let actual = action(&word, Tag::unknown(), &to_snake_case).unwrap(); + assert_eq!(actual, expected); + } + #[test] + fn snake_case_from_camel() { + let word = string("thisIsTheSecondCase"); + let expected = string("this_is_the_second_case"); + + let actual = action(&word, Tag::unknown(), &to_snake_case).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/mod.rs b/crates/nu-cli/src/commands/str_/mod.rs index 6926bd2e6..67e761558 100644 --- a/crates/nu-cli/src/commands/str_/mod.rs +++ b/crates/nu-cli/src/commands/str_/mod.rs @@ -1,4 +1,5 @@ mod capitalize; +mod case; mod collect; mod command; mod contains; @@ -19,6 +20,11 @@ mod trim; mod upcase; pub use capitalize::SubCommand as StrCapitalize; +pub use case::camel_case::SubCommand as StrCamelCase; +pub use case::kebab_case::SubCommand as StrKebabCase; +pub use case::pascal_case::SubCommand as StrPascalCase; +pub use case::screaming_snake_case::SubCommand as StrScreamingSnakeCase; +pub use case::snake_case::SubCommand as StrSnakeCase; pub use collect::SubCommand as StrCollect; pub use command::Command as Str; pub use contains::SubCommand as StrContains; diff --git a/crates/nu-cli/tests/commands/str_/mod.rs b/crates/nu-cli/tests/commands/str_/mod.rs index 66e293fb3..720a00656 100644 --- a/crates/nu-cli/tests/commands/str_/mod.rs +++ b/crates/nu-cli/tests/commands/str_/mod.rs @@ -97,6 +97,26 @@ fn upcases() { }) } +#[test] +fn camelcases() { + Playground::setup("str_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "THIS_IS_A_TEST" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str camel-case dependency.name | get dependency.name | echo $it" + ); + + assert_eq!(actual.out, "thisIsATest"); + }) +} + #[test] fn converts_to_int() { let actual = nu!(