diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 0bae95618..216b70195 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -4,11 +4,16 @@ use nu::{ ReturnSuccess, ReturnValue, ShellError, Tagged, Value, }; +enum Action { + Downcase, + Upcase, + ToInteger, +} + struct Str { field: Option, error: Option, - downcase: bool, - upcase: bool, + action: Option, } impl Str { @@ -16,53 +21,60 @@ impl Str { Str { field: None, error: None, - downcase: false, - upcase: false, + action: None, } } - fn is_valid(&self) -> bool { - (self.downcase && !self.upcase) || (!self.downcase && self.upcase) - } - - fn log_error(&mut self, message: &str) { - self.error = Some(message.to_string()); + 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::() { + Ok(v) => Value::int(v), + Err(_) => Value::string(input), + }, + None => Value::string(input.to_string()), + } } fn for_input(&mut self, field: String) { self.field = Some(field); } - fn for_downcase(&mut self) { - self.downcase = true; + fn permit(&mut self) -> bool { + self.action.is_none() + } - if !self.is_valid() { - self.log_error("can only apply one") + fn log_error(&mut self, message: &str) { + self.error = Some(message.to_string()); + } + + fn for_to_int(&mut self) { + if self.permit() { + self.action = Some(Action::ToInteger); + } else { + self.log_error("can only apply one"); + } + } + + fn for_downcase(&mut self) { + if self.permit() { + self.action = Some(Action::Downcase); + } else { + self.log_error("can only apply one"); } } fn for_upcase(&mut self) { - self.upcase = true; - - if !self.is_valid() { - self.log_error("can only apply one") + if self.permit() { + self.action = Some(Action::Upcase); + } else { + self.log_error("can only apply one"); } } - fn apply(&self, input: &str) -> String { - if self.downcase { - return input.to_ascii_lowercase(); - } - - if self.upcase { - return input.to_ascii_uppercase(); - } - - input.to_string() - } - fn usage(&self) -> &'static str { - "Usage: str [--downcase, --upcase]" + "Usage: str field [--downcase|--upcase|--to-int]" } } @@ -73,10 +85,9 @@ impl Str { field: &Option, ) -> Result, ShellError> { match value.item { - Value::Primitive(Primitive::String(ref s)) => Ok(Tagged::from_item( - Value::string(self.apply(&s)), - value.span(), - )), + Value::Primitive(Primitive::String(ref s)) => { + Ok(Tagged::from_item(self.apply(&s), value.span())) + } Value::Object(_) => match field { Some(f) => { let replacement = match value.item.get_data_by_path(value.span(), f) { @@ -95,9 +106,11 @@ impl Str { } } } - None => Err(ShellError::string( + None => Err(ShellError::string(format!( + "{}: {}", "str needs a field when applying it to a value in an object", - )), + self.usage() + ))), }, x => Err(ShellError::string(format!( "Unrecognized type in stream: {:?}", @@ -112,6 +125,7 @@ impl Plugin for Str { let mut named = IndexMap::new(); named.insert("downcase".to_string(), NamedType::Switch); named.insert("upcase".to_string(), NamedType::Switch); + named.insert("to-int".to_string(), NamedType::Switch); Ok(CommandConfig { name: "str".to_string(), @@ -132,6 +146,10 @@ impl Plugin for Str { self.for_upcase(); } + if call_info.args.has("to-int") { + self.for_to_int(); + } + if let Some(args) = call_info.args.positional { for arg in args { match arg { @@ -155,10 +173,8 @@ impl Plugin for Str { Some(reason) => { return Err(ShellError::string(format!("{}: {}", reason, self.usage()))) } - None => {} + None => Ok(vec![]), } - - Ok(vec![]) } fn filter(&mut self, input: Tagged) -> Result, ShellError> { @@ -171,3 +187,233 @@ impl Plugin for Str { fn main() { serve_plugin(&mut Str::new()); } + +#[cfg(test)] +mod tests { + + use super::Str; + use indexmap::IndexMap; + use nu::{ + Args, CallInfo, Plugin, ReturnSuccess, SourceMap, Span, Tagged, TaggedDictBuilder, + TaggedItem, Value, + }; + + struct CallStub { + positionals: Vec>, + flags: IndexMap>, + } + + impl CallStub { + fn new() -> CallStub { + CallStub { + positionals: vec![], + flags: indexmap::IndexMap::new(), + } + } + + fn with_long_flag(&mut self, name: &str) -> &mut Self { + self.flags.insert( + name.to_string(), + Value::boolean(true).tagged(Span::unknown()), + ); + self + } + + fn with_parameter(&mut self, name: &str) -> &mut Self { + self.positionals + .push(Value::string(name.to_string()).tagged(Span::unknown())); + self + } + + fn create(&self) -> CallInfo { + CallInfo { + args: Args::new(Some(self.positionals.clone()), Some(self.flags.clone())), + source_map: SourceMap::new(), + name_span: None, + } + } + } + + fn sample_record(key: &str, value: &str) -> Tagged { + let mut record = TaggedDictBuilder::new(Span::unknown()); + record.insert(key.clone(), Value::string(value)); + record.into_tagged_value() + } + + #[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"] { + assert!(configured.named.get(*action_flag).is_some()); + } + } + + #[test] + fn str_plugin_accepts_downcase() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("downcase").create()) + .is_ok()); + assert!(plugin.action.is_some()); + } + + #[test] + fn str_plugin_accepts_upcase() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("upcase").create()) + .is_ok()); + assert!(plugin.action.is_some()); + } + + #[test] + fn str_plugin_accepts_to_int() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("to-int").create()) + .is_ok()); + assert!(plugin.action.is_some()); + } + + #[test] + fn str_plugin_accepts_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("package.description") + .create() + ) + .is_ok()); + + assert_eq!(plugin.field, Some("package.description".to_string())); + } + + #[test] + fn str_plugin_accepts_only_one_action() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("upcase") + .with_long_flag("downcase") + .with_long_flag("to-int") + .create(), + ) + .is_err()); + assert_eq!(plugin.error, Some("can only apply one".to_string())); + } + + #[test] + fn str_downcases() { + let mut strutils = Str::new(); + strutils.for_downcase(); + assert_eq!(strutils.apply("ANDRES"), Value::string("andres")); + } + + #[test] + fn str_upcases() { + let mut strutils = Str::new(); + strutils.for_upcase(); + assert_eq!(strutils.apply("andres"), Value::string("ANDRES")); + } + + #[test] + fn str_to_int() { + let mut strutils = Str::new(); + strutils.for_to_int(); + assert_eq!(strutils.apply("9999"), Value::int(9999 as i64)); + } + + #[test] + fn str_plugin_applies_upcase() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("upcase") + .with_parameter("name") + .create() + ) + .is_ok()); + + let subject = sample_record("name", "jotandrehuda"); + 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("name")).borrow(), + Value::string(String::from("JOTANDREHUDA")) + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_downcase() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("downcase") + .with_parameter("name") + .create() + ) + .is_ok()); + + let subject = sample_record("name", "JOTANDREHUDA"); + 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("name")).borrow(), + Value::string(String::from("jotandrehuda")) + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_to_int() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("to-int") + .with_parameter("Nu_birthday") + .create() + ) + .is_ok()); + + let subject = sample_record("Nu_birthday", "10"); + 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("Nu_birthday")).borrow(), + Value::int(10) + ), + _ => {} + } + } +} diff --git a/tests/filters_test.rs b/tests/filters_test.rs index 74523fa38..a0d2d92e6 100644 --- a/tests/filters_test.rs +++ b/tests/filters_test.rs @@ -76,7 +76,7 @@ fn str_can_only_apply_one() { "open caco3_plastics.csv | first 1 | str origin --downcase --upcase" ); - assert!(output.contains("Usage: str [--downcase, --upcase]")); + assert!(output.contains("Usage: str field [--downcase|--upcase|--to-int]")); } #[test] @@ -101,6 +101,17 @@ fn str_upcases() { assert_eq!(output, "NUSHELL"); } +#[test] +fn str_converts_to_int() { + nu!( + output, + cwd("tests/fixtures/formats"), + "open caco3_plastics.csv | get 0 | str tariff_item --to-int | where tariff_item == 2509000000 | get tariff_item | echo $it" + ); + + assert_eq!(output, "2509000000"); +} + #[test] fn can_inc_version() { nu!(