From cee2f5207ed409456ccb460971aaf3b82df0f6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 11 Aug 2019 12:46:14 -0500 Subject: [PATCH] Str plugin: replace & find-replace. --- src/plugins/str.rs | 391 ++++++++++++++++++++++++++++++++++---- tests/filter_str_tests.rs | 108 ++++++++--- 2 files changed, 434 insertions(+), 65 deletions(-) diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 14bc30e4d..3c9201174 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -1,17 +1,27 @@ use indexmap::IndexMap; use nu::{ - serve_plugin, CallInfo, NamedType, Plugin, Primitive, ReturnSuccess, - ReturnValue, ShellError, Signature, Tagged, Value, + serve_plugin, CallInfo, NamedType, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, + Signature, Tagged, Value, }; +use regex::Regex; +#[derive(Debug, Eq, PartialEq)] enum Action { Downcase, Upcase, ToInteger, + Replace(ReplaceAction), +} + +#[derive(Debug, Eq, PartialEq)] +enum ReplaceAction { + Direct, + FindAndReplace, } struct Str { field: Option, + params: Option>, error: Option, action: Option, } @@ -20,25 +30,58 @@ impl Str { fn new() -> Str { Str { field: None, + params: Some(Vec::::new()), error: None, action: None, } } fn apply(&self, input: &str) -> Value { - match self.action { - Some(Action::Downcase) => Value::string(input.to_ascii_lowercase()), - Some(Action::Upcase) => Value::string(input.to_ascii_uppercase()), - Some(Action::ToInteger) => match input.trim().parse::() { + if self.action.is_none() { + return Value::string(input.to_string()); + } + + match self.action.as_ref().unwrap() { + Action::Downcase => Value::string(input.to_ascii_lowercase()), + Action::Upcase => Value::string(input.to_ascii_uppercase()), + Action::ToInteger => match input.trim().parse::() { Ok(v) => Value::int(v), Err(_) => Value::string(input), }, - None => Value::string(input.to_string()), + Action::Replace(ref mode) => match mode { + ReplaceAction::Direct => Value::string(self.first_param()), + ReplaceAction::FindAndReplace => { + let regex = Regex::new(self.first_param()); + + match regex { + Ok(re) => Value::string(re.replace(input, self.second_param()).to_owned()), + Err(_) => Value::string(input), + } + } + }, } } - fn for_input(&mut self, field: String) { - self.field = Some(field); + fn did_supply_field(&self) -> bool { + self.field.is_some() + } + + fn first_param(&self) -> &str { + let idx = if self.did_supply_field() { 1 } else { 0 }; + self.get_param(idx) + } + + fn second_param(&self) -> &str { + let idx = if self.did_supply_field() { 2 } else { 1 }; + self.get_param(idx) + } + + fn get_param(&self, idx: usize) -> &str { + self.params.as_ref().unwrap().get(idx).unwrap().as_str() + } + + fn for_field(&mut self, field: &str) { + self.field = Some(String::from(field)); } fn permit(&mut self) -> bool { @@ -49,6 +92,14 @@ impl Str { self.error = Some(message.to_string()); } + fn for_replace(&mut self, mode: ReplaceAction) { + if self.permit() { + self.action = Some(Action::Replace(mode)); + } else { + self.log_error("can only apply one"); + } + } + fn for_to_int(&mut self) { if self.permit() { self.action = Some(Action::ToInteger); @@ -74,7 +125,7 @@ impl Str { } fn usage(&self) -> &'static str { - "Usage: str field [--downcase|--upcase|--to-int]" + "Usage: str field [--downcase|--upcase|--to-int|--replace|--find-replace]" } } @@ -122,6 +173,8 @@ impl Plugin for Str { named.insert("downcase".to_string(), NamedType::Switch); named.insert("upcase".to_string(), NamedType::Switch); named.insert("to-int".to_string(), NamedType::Switch); + named.insert("replace".to_string(), NamedType::Switch); + named.insert("find-replace".to_string(), NamedType::Switch); Ok(Signature { name: "str".to_string(), @@ -133,37 +186,66 @@ impl Plugin for Str { } fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { - if call_info.args.has("downcase") { + let args = call_info.args; + + if args.has("downcase") { self.for_downcase(); } - - if call_info.args.has("upcase") { + if args.has("upcase") { self.for_upcase(); } - - if call_info.args.has("to-int") { + if args.has("to-int") { self.for_to_int(); } + if args.has("replace") { + self.for_replace(ReplaceAction::Direct); + } + if args.has("find-replace") { + self.for_replace(ReplaceAction::FindAndReplace); + } - if let Some(args) = call_info.args.positional { - for arg in args { - match arg { - Tagged { - item: Value::Primitive(Primitive::String(s)), - .. - } => { - self.for_input(s); + if let Some(possible_field) = args.nth(0) { + match possible_field { + Tagged { + item: Value::Primitive(Primitive::String(s)), + .. + } => match self.action { + Some(Action::Replace(ReplaceAction::Direct)) => { + if args.len() == 2 { + self.for_field(&s); + } } - _ => { - return Err(ShellError::string(format!( - "Unrecognized type in params: {:?}", - arg - ))) + Some(Action::Replace(ReplaceAction::FindAndReplace)) => { + if args.len() == 3 { + self.for_field(&s); + } } + Some(Action::Downcase) + | Some(Action::Upcase) + | Some(Action::ToInteger) + | None => { + self.for_field(&s); + } + }, + _ => { + return Err(ShellError::string(format!( + "Unrecognized type in params: {:?}", + possible_field + ))) } } } + for param in args.positional_iter() { + match param { + Tagged { + item: Value::Primitive(Primitive::String(s)), + .. + } => self.params.as_mut().unwrap().push(String::from(s)), + _ => {} + } + } + match &self.error { Some(reason) => { return Err(ShellError::string(format!("{}: {}", reason, self.usage()))) @@ -184,13 +266,23 @@ fn main() { #[cfg(test)] mod tests { - use super::Str; + use super::{Action, ReplaceAction, Str}; use indexmap::IndexMap; use nu::{ - CallInfo, EvaluatedArgs, Plugin, ReturnSuccess, SourceMap, Span, Tag, Tagged, + CallInfo, EvaluatedArgs, Plugin, Primitive, ReturnSuccess, SourceMap, Span, Tag, Tagged, TaggedDictBuilder, TaggedItem, Value, }; + impl Str { + fn replace_with(&mut self, value: &str) { + self.params.as_mut().unwrap().push(value.to_string()); + } + + fn find_with(&mut self, search: &str) { + self.params.as_mut().unwrap().push(search.to_string()); + } + } + struct CallStub { positionals: Vec>, flags: IndexMap>, @@ -227,19 +319,23 @@ mod tests { } } - fn sample_record(key: &str, value: &str) -> Tagged { + fn structured_sample_record(key: &str, value: &str) -> Tagged { let mut record = TaggedDictBuilder::new(Tag::unknown()); record.insert(key.clone(), Value::string(value)); record.into_tagged_value() } + fn unstructured_sample_record(value: &str) -> Tagged { + Tagged::from_item(Value::string(value), Tag::unknown()) + } + #[test] fn str_plugin_configuration_flags_wired() { let mut plugin = Str::new(); let configured = plugin.config().unwrap(); - for action_flag in &["downcase", "upcase", "to-int"] { + for action_flag in &["downcase", "upcase", "to-int", "replace", "find-replace"] { assert!(configured.named.get(*action_flag).is_some()); } } @@ -251,7 +347,7 @@ mod tests { assert!(plugin .begin_filter(CallStub::new().with_long_flag("downcase").create()) .is_ok()); - assert!(plugin.action.is_some()); + assert_eq!(plugin.action.unwrap(), Action::Downcase); } #[test] @@ -261,7 +357,7 @@ mod tests { assert!(plugin .begin_filter(CallStub::new().with_long_flag("upcase").create()) .is_ok()); - assert!(plugin.action.is_some()); + assert_eq!(plugin.action.unwrap(), Action::Upcase); } #[test] @@ -271,7 +367,33 @@ mod tests { assert!(plugin .begin_filter(CallStub::new().with_long_flag("to-int").create()) .is_ok()); - assert!(plugin.action.is_some()); + assert_eq!(plugin.action.unwrap(), Action::ToInteger); + } + + #[test] + fn str_plugin_accepts_replace() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("replace").create()) + .is_ok()); + assert_eq!( + plugin.action.unwrap(), + Action::Replace(ReplaceAction::Direct) + ); + } + + #[test] + fn str_plugin_accepts_find_replace() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("find-replace").create()) + .is_ok()); + assert_eq!( + plugin.action.unwrap(), + Action::Replace(ReplaceAction::FindAndReplace) + ); } #[test] @@ -327,7 +449,24 @@ mod tests { } #[test] - fn str_plugin_applies_upcase() { + fn str_replace() { + let mut strutils = Str::new(); + strutils.for_replace(ReplaceAction::Direct); + strutils.replace_with("robalino"); + assert_eq!(strutils.apply("andres"), Value::string("robalino")); + } + + #[test] + fn str_find_replace() { + let mut strutils = Str::new(); + strutils.for_replace(ReplaceAction::FindAndReplace); + strutils.find_with(r"kittens"); + strutils.replace_with("jotandrehuda"); + assert_eq!(strutils.apply("wykittens"), Value::string("wyjotandrehuda")); + } + + #[test] + fn str_plugin_applies_upcase_with_field() { let mut plugin = Str::new(); assert!(plugin @@ -339,7 +478,7 @@ mod tests { ) .is_ok()); - let subject = sample_record("name", "jotandrehuda"); + let subject = structured_sample_record("name", "jotandrehuda"); let output = plugin.filter(subject).unwrap(); match output[0].as_ref().unwrap() { @@ -355,7 +494,27 @@ mod tests { } #[test] - fn str_plugin_applies_downcase() { + fn str_plugin_applies_upcase_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("upcase").create()) + .is_ok()); + + let subject = unstructured_sample_record("jotandrehuda"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("JOTANDREHUDA")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_downcase_with_field() { let mut plugin = Str::new(); assert!(plugin @@ -367,7 +526,7 @@ mod tests { ) .is_ok()); - let subject = sample_record("name", "JOTANDREHUDA"); + let subject = structured_sample_record("name", "JOTANDREHUDA"); let output = plugin.filter(subject).unwrap(); match output[0].as_ref().unwrap() { @@ -383,7 +542,27 @@ mod tests { } #[test] - fn str_plugin_applies_to_int() { + fn str_plugin_applies_downcase_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("downcase").create()) + .is_ok()); + + let subject = unstructured_sample_record("JOTANDREHUDA"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("jotandrehuda")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_to_int_with_field() { let mut plugin = Str::new(); assert!(plugin @@ -395,7 +574,7 @@ mod tests { ) .is_ok()); - let subject = sample_record("Nu_birthday", "10"); + let subject = structured_sample_record("Nu_birthday", "10"); let output = plugin.filter(subject).unwrap(); match output[0].as_ref().unwrap() { @@ -409,4 +588,134 @@ mod tests { _ => {} } } + + #[test] + fn str_plugin_applies_to_int_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("to-int").create()) + .is_ok()); + + let subject = unstructured_sample_record("10"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Primitive(Primitive::Int(i)), + .. + }) => assert_eq!(*i, 10), + _ => {} + } + } + + #[test] + fn str_plugin_applies_replace_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("rustconf") + .with_parameter("22nd August 2019") + .with_long_flag("replace") + .create() + ) + .is_ok()); + + let subject = structured_sample_record("rustconf", "1st January 1970"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Object(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("rustconf")).borrow(), + Value::string(String::from("22nd August 2019")) + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_replace_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("22nd August 2019") + .with_long_flag("replace") + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("1st January 1970"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("22nd August 2019")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_find_replace_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("staff") + .with_parameter("kittens") + .with_parameter("jotandrehuda") + .with_long_flag("find-replace") + .create() + ) + .is_ok()); + + let subject = structured_sample_record("staff", "wykittens"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Object(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("staff")).borrow(), + Value::string(String::from("wyjotandrehuda")) + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_find_replace_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("kittens") + .with_parameter("jotandrehuda") + .with_long_flag("find-replace") + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("wykittens"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Tagged { + item: Value::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("wyjotandrehuda")), + _ => {} + } + } } diff --git a/tests/filter_str_tests.rs b/tests/filter_str_tests.rs index 9a4ab553a..5d96bddec 100644 --- a/tests/filter_str_tests.rs +++ b/tests/filter_str_tests.rs @@ -11,24 +11,27 @@ fn can_only_apply_one() { "open caco3_plastics.csv | first 1 | str origin --downcase --upcase" ); - assert!(output.contains("Usage: str field [--downcase|--upcase|--to-int]")); + assert!( + output.contains("Usage: str field [--downcase|--upcase|--to-int|--replace|--find-replace]") + ); } #[test] fn acts_without_passing_field() { - Playground::setup_for("plugin_str_test_without_passing_field") - .with_files(vec![FileWithContent( + Playground::setup_for("plugin_str_acts_without_passing_field_test").with_files(vec![ + FileWithContent( "sample.yml", r#" environment: global: PROJECT_NAME: nushell "#, - )]); + ), + ]); nu!( output, - cwd("tests/fixtures/nuplayground/plugin_str_test_without_passing_field"), + cwd("tests/fixtures/nuplayground/plugin_str_acts_without_passing_field_test"), "open sample.yml | get environment.global.PROJECT_NAME | str --upcase | echo $it" ); @@ -37,18 +40,17 @@ fn acts_without_passing_field() { #[test] fn downcases() { - Playground::setup_for("plugin_str_test_downcases") - .with_files(vec![FileWithContent( - "sample.toml", - r#" - [dependency] - name = "LIGHT" - "#, - )]); + Playground::setup_for("plugin_str_downcases_test").with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "LIGHT" + "#, + )]); nu!( output, - cwd("tests/fixtures/nuplayground/plugin_str_test_downcases"), + cwd("tests/fixtures/nuplayground/plugin_str_downcases_test"), "open sample.toml | str dependency.name --downcase | get dependency.name | echo $it" ); @@ -57,18 +59,17 @@ fn downcases() { #[test] fn upcases() { - Playground::setup_for("plugin_str_test_upcases") - .with_files(vec![FileWithContent( - "sample.toml", - r#" - [package] - name = "nushell" - "#, - )]); + Playground::setup_for("plugin_str_upcases_test").with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nushell" + "#, + )]); nu!( output, - cwd("tests/fixtures/nuplayground/plugin_str_test_upcases"), + cwd("tests/fixtures/nuplayground/plugin_str_upcases_test"), "open sample.toml | str package.name --upcase | get package.name | echo $it" ); @@ -84,4 +85,63 @@ fn converts_to_int() { ); assert_eq!(output, "2509000000"); -} \ No newline at end of file +} + +#[test] +fn replaces() { + Playground::setup_for("plugin_str_replaces_test").with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nushell" + "#, + )]); + + nu!( + output, + cwd("tests/fixtures/nuplayground/plugin_str_replaces_test"), + "open sample.toml | str package.name --replace wykittenshell | get package.name | echo $it" + ); + + assert_eq!(output, "wykittenshell"); +} + +#[test] +fn find_and_replaces() { + Playground::setup_for("plugin_str_find_and_replaces_test").with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800 KATZ" + "#, + )]); + + nu!( + output, + cwd("tests/fixtures/nuplayground/plugin_str_find_and_replaces_test"), + "open sample.toml | str fortune.teller.phone --find-replace KATZ \"5289\" | get fortune.teller.phone | echo $it" + ); + + assert_eq!(output, "1-800 5289"); +} + +#[test] +fn find_and_replaces_without_passing_field() { + Playground::setup_for("plugin_str_find_and_replaces_without_passing_field_test").with_files( + vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800 KATZ" + "#, + )], + ); + + nu!( + output, + cwd("tests/fixtures/nuplayground/plugin_str_find_and_replaces_without_passing_field_test"), + "open sample.toml | get fortune.teller.phone | str --find-replace KATZ \"5289\" | echo $it" + ); + + assert_eq!(output, "1-800 5289"); +}