diff --git a/src/commands/get.rs b/src/commands/get.rs index 70508bdb7..69ef15333 100644 --- a/src/commands/get.rs +++ b/src/commands/get.rs @@ -1,8 +1,8 @@ use crate::commands::WholeStreamCommand; -use crate::data::meta::tag_for_tagged_list; use crate::data::Value; use crate::errors::ShellError; use crate::prelude::*; +use crate::utils::did_you_mean; use log::trace; pub struct Get; @@ -50,56 +50,51 @@ pub fn get_column_path( path: &ColumnPath, obj: &Tagged, ) -> Result, ShellError> { - let mut current = Some(obj); - for p in path.iter() { - if let Some(obj) = current { - current = match obj.get_data_by_key(&p) { - Some(v) => Some(v), - None => - // Before we give up, see if they gave us a path that matches a field name by itself - { - let possibilities = obj.data_descriptors(); + let fields = path.clone(); - let mut possible_matches: Vec<_> = possibilities - .iter() - .map(|x| (natural::distance::levenshtein_distance(x, &p), x)) - .collect(); - - possible_matches.sort(); - - if possible_matches.len() > 0 { - return Err(ShellError::labeled_error( - "Unknown column", - format!("did you mean '{}'?", possible_matches[0].1), - tag_for_tagged_list(path.iter().map(|p| p.tag())), - )); - } else { - return Err(ShellError::labeled_error( - "Unknown column", - "row does not contain this column", - tag_for_tagged_list(path.iter().map(|p| p.tag())), - )); - } + let value = obj.get_data_by_column_path( + obj.tag(), + path, + Box::new(move |(obj_source, column_path_tried)| { + match did_you_mean(&obj_source, &column_path_tried) { + Some(suggestions) => { + return ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", suggestions[0].1), + tag_for_tagged_list(fields.iter().map(|p| p.tag())), + ) + } + None => { + return ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + tag_for_tagged_list(fields.iter().map(|p| p.tag())), + ) } } - } - } + }), + ); - match current { - Some(v) => Ok(v.clone()), - None => match obj { - // If its None check for certain values. - Tagged { - item: Value::Primitive(Primitive::String(_)), - .. - } => Ok(obj.clone()), - Tagged { - item: Value::Primitive(Primitive::Path(_)), - .. - } => Ok(obj.clone()), - _ => Ok(Value::nothing().tagged(&obj.tag)), + let res = match value { + Ok(fetched) => match fetched { + Some(Tagged { item: v, tag }) => Ok((v.clone()).tagged(&tag)), + None => match obj { + // If its None check for certain values. + Tagged { + item: Value::Primitive(Primitive::String(_)), + .. + } => Ok(obj.clone()), + Tagged { + item: Value::Primitive(Primitive::Path(_)), + .. + } => Ok(obj.clone()), + _ => Ok(Value::nothing().tagged(&obj.tag)), + }, }, - } + Err(reason) => Err(reason), + }; + + res } pub fn get( @@ -118,26 +113,30 @@ pub fn get( let member = vec![member.clone()]; - let fields = vec![&member, &fields] + let column_paths = vec![&member, &fields] .into_iter() .flatten() .collect::>(); - for column_path in &fields { - match get_column_path(column_path, &item) { - Ok(Tagged { - item: Value::Table(l), - .. - }) => { - for item in l { - result.push_back(ReturnSuccess::value(item.clone())); + for path in column_paths { + let res = get_column_path(&path, &item); + + match res { + Ok(got) => match got { + Tagged { + item: Value::Table(rows), + .. + } => { + for item in rows { + result.push_back(ReturnSuccess::value(item.clone())); + } } - } - Ok(x) => result.push_back(ReturnSuccess::value(x.clone())), - Err(x) => result.push_back(Err(x)), + other => result + .push_back(ReturnSuccess::value((*other).clone().tagged(&item.tag))), + }, + Err(reason) => result.push_back(Err(reason)), } } - result }) .flatten(); diff --git a/src/data/base.rs b/src/data/base.rs index 43cb83784..2d0288ac8 100644 --- a/src/data/base.rs +++ b/src/data/base.rs @@ -530,7 +530,8 @@ impl Value { &self, tag: Tag, path: &Vec>, - ) -> Option> { + callback: Box)) -> ShellError>, + ) -> Result>, ShellError> { let mut current = self; for p in path { let value = if p.chars().all(char::is_numeric) { @@ -543,11 +544,11 @@ impl Value { match value { Some(v) => current = v, - None => return None, + None => return Err(callback((¤t.clone(), &p.clone()))), } } - Some(current.tagged(tag)) + Ok(Some(current.tagged(tag))) } pub fn insert_data_at_path( @@ -927,6 +928,7 @@ fn coerce_compare_primitive( mod tests { use crate::data::meta::*; + use crate::ShellError; use crate::Value; use indexmap::IndexMap; @@ -942,6 +944,10 @@ mod tests { Value::table(list).tagged_unknown() } + fn error_callback() -> impl FnOnce((&Value, &Tagged)) -> ShellError { + move |(_obj_source, _column_path_tried)| ShellError::unimplemented("will never be called.") + } + fn column_path(paths: &Vec>) -> Tagged>> { table( &paths @@ -984,7 +990,10 @@ mod tests { }); assert_eq!( - **value.get_data_by_column_path(tag, &field_path).unwrap(), + **value + .get_data_by_column_path(tag, &field_path, Box::new(error_callback())) + .unwrap() + .unwrap(), version ) } @@ -1008,7 +1017,10 @@ mod tests { }); assert_eq!( - **value.get_data_by_column_path(tag, &field_path).unwrap(), + **value + .get_data_by_column_path(tag, &field_path, Box::new(error_callback())) + .unwrap() + .unwrap(), name ) } @@ -1032,7 +1044,10 @@ mod tests { }); assert_eq!( - **value.get_data_by_column_path(tag, &field_path).unwrap(), + **value + .get_data_by_column_path(tag, &field_path, Box::new(error_callback())) + .unwrap() + .unwrap(), Value::row(indexmap! { "name".into() => string("AndrĂ©s N. Robalino") }) diff --git a/src/lib.rs b/src/lib.rs index 520e08a13..f21a70cfe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,12 +30,12 @@ pub use crate::env::host::BasicHost; pub use crate::parser::hir::SyntaxShape; pub use crate::parser::parse::token_tree_builder::TokenTreeBuilder; pub use crate::plugin::{serve_plugin, Plugin}; -pub use crate::utils::{AbsoluteFile, AbsolutePath, RelativePath}; +pub use crate::utils::{did_you_mean, AbsoluteFile, AbsolutePath, RelativePath}; pub use cli::cli; pub use data::base::{Primitive, Value}; pub use data::config::{config_path, APP_INFO}; pub use data::dict::{Dictionary, TaggedDictBuilder}; -pub use data::meta::{Span, Spanned, SpannedItem, Tag, Tagged, TaggedItem}; +pub use data::meta::{tag_for_tagged_list, Span, Spanned, SpannedItem, Tag, Tagged, TaggedItem}; pub use errors::{CoerceInto, ShellError}; pub use num_traits::cast::ToPrimitive; pub use parser::parse::text::Text; diff --git a/src/plugins/inc.rs b/src/plugins/inc.rs index ed0416ce4..fb3836dfd 100644 --- a/src/plugins/inc.rs +++ b/src/plugins/inc.rs @@ -1,6 +1,6 @@ use nu::{ - serve_plugin, CallInfo, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, Signature, - SyntaxShape, Tagged, TaggedItem, Value, + did_you_mean, serve_plugin, tag_for_tagged_list, CallInfo, Plugin, Primitive, ReturnSuccess, + ReturnValue, ShellError, Signature, SyntaxShape, Tagged, TaggedItem, Value, }; enum Action { @@ -93,22 +93,51 @@ impl Inc { )); } } + Value::Row(_) => match self.field { Some(ref f) => { - let replacement = match value.item.get_data_by_column_path(value.tag(), f) { - Some(result) => self.inc(result.map(|x| x.clone()))?, - None => { - return Err(ShellError::labeled_error( - "inc could not find field to replace", - "column name", - value.tag(), - )) - } + let fields = f.clone(); + + let replace_for = value.item.get_data_by_column_path( + value.tag(), + &f, + Box::new(move |(obj_source, column_path_tried)| { + match did_you_mean(&obj_source, &column_path_tried) { + Some(suggestions) => { + return ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", suggestions[0].1), + tag_for_tagged_list(fields.iter().map(|p| p.tag())), + ) + } + None => { + return ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + tag_for_tagged_list(fields.iter().map(|p| p.tag())), + ) + } + } + }), + ); + + let replacement = match replace_for { + Ok(got) => match got { + Some(result) => self.inc(result.map(|x| x.clone()))?, + None => { + return Err(ShellError::labeled_error( + "inc could not find field to replace", + "column name", + value.tag(), + )) + } + }, + Err(reason) => return Err(reason), }; match value.item.replace_data_at_column_path( value.tag(), - f, + &f, replacement.item.clone(), ) { Some(v) => return Ok(v), diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 8260bdac2..f565d5209 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -1,6 +1,6 @@ use nu::{ - serve_plugin, CallInfo, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, Signature, - SyntaxShape, Tagged, TaggedItem, Value, + did_you_mean, serve_plugin, tag_for_tagged_list, CallInfo, Plugin, Primitive, ReturnSuccess, + ReturnValue, ShellError, Signature, SyntaxShape, Tagged, TaggedItem, Value, }; #[derive(Debug, Eq, PartialEq)] @@ -92,13 +92,50 @@ impl Str { Value::Primitive(Primitive::String(ref s)) => Ok(self.apply(&s)?.tagged(value.tag())), Value::Row(_) => match self.field { Some(ref f) => { - let replacement = match value.item.get_data_by_column_path(value.tag(), f) { - Some(result) => self.strutils(result.map(|x| x.clone()))?, - None => return Ok(Value::nothing().tagged(value.tag)), + let fields = f.clone(); + + let replace_for = value.item.get_data_by_column_path( + value.tag(), + &f, + Box::new(move |(obj_source, column_path_tried)| { + //let fields = f.clone(); + + match did_you_mean(&obj_source, &column_path_tried) { + Some(suggestions) => { + return ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", suggestions[0].1), + tag_for_tagged_list(fields.iter().map(|p| p.tag())), + ) + } + None => { + return ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + tag_for_tagged_list(fields.iter().map(|p| p.tag())), + ) + } + } + }), + ); + + let replacement = match replace_for { + Ok(got) => match got { + Some(result) => self.strutils(result.map(|x| x.clone()))?, + None => { + return Err(ShellError::labeled_error( + "inc could not find field to replace", + "column name", + value.tag(), + )) + } + }, + Err(reason) => return Err(reason), }; + match value.item.replace_data_at_column_path( value.tag(), - f, + &f, replacement.item.clone(), ) { Some(v) => return Ok(v), diff --git a/src/prelude.rs b/src/prelude.rs index 4b12a07bd..6ff62c324 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -66,7 +66,9 @@ pub(crate) use crate::commands::RawCommandArgs; pub(crate) use crate::context::CommandRegistry; pub(crate) use crate::context::{AnchorLocation, Context}; pub(crate) use crate::data::base as value; -pub(crate) use crate::data::meta::{Span, Spanned, SpannedItem, Tag, Tagged, TaggedItem}; +pub(crate) use crate::data::meta::{ + tag_for_tagged_list, Span, Spanned, SpannedItem, Tag, Tagged, TaggedItem, +}; pub(crate) use crate::data::types::ExtractType; pub(crate) use crate::data::{Primitive, Value}; pub(crate) use crate::env::host::handle_unexpected; diff --git a/src/utils.rs b/src/utils.rs index 56fee491b..9822b7627 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,6 +5,30 @@ use std::fmt; use std::ops::Div; use std::path::{Component, Path, PathBuf}; +pub fn did_you_mean( + obj_source: &Value, + field_tried: &Tagged, +) -> Option> { + let possibilities = obj_source.data_descriptors(); + + let mut possible_matches: Vec<_> = possibilities + .into_iter() + .map(|x| { + let word = x.clone(); + let distance = natural::distance::levenshtein_distance(&word, &field_tried); + + (distance, word) + }) + .collect(); + + if possible_matches.len() > 0 { + possible_matches.sort(); + return Some(possible_matches); + } + + None +} + pub struct AbsoluteFile { inner: PathBuf, }