use nu::{ serve_plugin, CallInfo, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, Signature, SyntaxType, 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, } impl Str { fn new() -> Str { Str { field: None, params: Some(Vec::::new()), error: None, action: None, } } fn apply(&self, input: &str) -> Result { let applied = match self.action.as_ref() { Some(Action::Downcase) => Value::string(input.to_ascii_lowercase()), Some(Action::Upcase) => Value::string(input.to_ascii_uppercase()), Some(Action::ToInteger) => match input.trim() { other => match other.parse::() { Ok(v) => Value::int(v), Err(_) => Value::string(input), }, }, Some(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), } } }, None => Value::string(input), }; Ok(applied) } 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 { self.action.is_none() } fn log_error(&mut self, message: &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); } 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) { if self.permit() { self.action = Some(Action::Upcase); } else { self.log_error("can only apply one"); } } fn usage(&self) -> &'static str { "Usage: str field [--downcase|--upcase|--to-int|--replace|--find-replace]" } } impl Str { fn strutils(&self, value: Tagged) -> Result, ShellError> { match value.item { Value::Primitive(Primitive::String(ref s)) => { Ok(Tagged::from_item(self.apply(&s)?, value.tag())) } Value::Object(_) => match self.field { Some(ref f) => { let replacement = match value.item.get_data_by_path(value.tag(), f) { Some(result) => self.strutils(result.map(|x| x.clone()))?, None => return Ok(Tagged::from_item(Value::nothing(), value.tag)), }; match value .item .replace_data_at_path(value.tag(), f, replacement.item.clone()) { Some(v) => return Ok(v), None => { return Err(ShellError::string("str could not find field to replace")) } } } 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: {:?}", x ))), } } } impl Plugin for Str { fn config(&mut self) -> Result { Ok(Signature::build("str") .switch("downcase") .switch("upcase") .switch("to-int") .switch("replace") .switch("find-replace") .rest(SyntaxType::Member) .filter()) } fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { let args = call_info.args; if args.has("downcase") { self.for_downcase(); } if args.has("upcase") { self.for_upcase(); } 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(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); } } 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()))) } None => Ok(vec![]), } } fn filter(&mut self, input: Tagged) -> Result, ShellError> { Ok(vec![ReturnSuccess::value(self.strutils(input)?)]) } } fn main() { serve_plugin(&mut Str::new()); } #[cfg(test)] mod tests { use super::{Action, ReplaceAction, Str}; use indexmap::IndexMap; use nu::{ 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>, } 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).simple_spanned(Span::unknown()), ); self } fn with_parameter(&mut self, name: &str) -> &mut Self { self.positionals .push(Value::string(name.to_string()).simple_spanned(Span::unknown())); self } fn create(&self) -> CallInfo { CallInfo { args: EvaluatedArgs::new(Some(self.positionals.clone()), Some(self.flags.clone())), source_map: SourceMap::new(), name_span: Span::unknown(), } } } 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", "replace", "find-replace"] { 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_eq!(plugin.action.unwrap(), Action::Downcase); } #[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_eq!(plugin.action.unwrap(), Action::Upcase); } #[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_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] 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").unwrap(), Value::string("andres")); } #[test] fn str_upcases() { let mut strutils = Str::new(); strutils.for_upcase(); assert_eq!(strutils.apply("andres").unwrap(), Value::string("ANDRES")); } #[test] fn str_to_int() { let mut strutils = Str::new(); strutils.for_to_int(); assert_eq!(strutils.apply("9999").unwrap(), Value::int(9999 as i64)); } #[test] fn str_replace() { let mut strutils = Str::new(); strutils.for_replace(ReplaceAction::Direct); strutils.replace_with("robalino"); assert_eq!(strutils.apply("andres").unwrap(), 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").unwrap(), Value::string("wyjotandrehuda") ); } #[test] fn str_plugin_applies_upcase_with_field() { let mut plugin = Str::new(); assert!(plugin .begin_filter( CallStub::new() .with_long_flag("upcase") .with_parameter("name") .create() ) .is_ok()); let subject = structured_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_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 .begin_filter( CallStub::new() .with_long_flag("downcase") .with_parameter("name") .create() ) .is_ok()); let subject = structured_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_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 .begin_filter( CallStub::new() .with_long_flag("to-int") .with_parameter("Nu_birthday") .create() ) .is_ok()); let subject = structured_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) ), _ => {} } } #[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")), _ => {} } } }