From e8b678352952c3c00b381c7b7c7ca397ef4414cc Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 16:28:52 +0300 Subject: [PATCH] feat(record): Add CasedRecord, Record wrapper that affects case sensitivity Make case sensitivity explicit in various places. --- crates/nu-protocol/src/value/mod.rs | 70 ++++++++++++++---------- crates/nu-protocol/src/value/record.rs | 75 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 7f1e3c2a57..2c61afa514 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -31,7 +31,7 @@ use fancy_regex::Regex; use nu_utils::{ contains_emoji, locale::{get_system_locale_string, LOCALE_OVERRIDE_ENV_VAR}, - IgnoreCaseExt, SharedCow, + SharedCow, }; use serde::{Deserialize, Serialize}; use std::{ @@ -1182,6 +1182,7 @@ impl Value { PathMember::String { val: col_name, span, + insensitive, .. } => match self { Value::List { vals, .. } => { @@ -1189,7 +1190,9 @@ impl Value { match val { Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = + record.cased_mut(*insensitive).get_mut(col_name) + { val.upsert_data_at_cell_path(path, new_val.clone())?; } else { let new_col = @@ -1210,7 +1213,7 @@ impl Value { } Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = record.cased_mut(*insensitive).get_mut(col_name) { val.upsert_data_at_cell_path(path, new_val)?; } else { let new_col = Value::with_data_at_cell_path(path, new_val.clone())?; @@ -1282,6 +1285,7 @@ impl Value { PathMember::String { val: col_name, span, + insensitive, .. } => match self { Value::List { vals, .. } => { @@ -1289,7 +1293,9 @@ impl Value { let v_span = val.span(); match val { Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + if let Some(val) = + record.to_mut().cased_mut(*insensitive).get_mut(col_name) + { val.update_data_at_cell_path(path, new_val.clone())?; } else { return Err(ShellError::CantFindColumn { @@ -1311,7 +1317,8 @@ impl Value { } } Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + if let Some(val) = record.to_mut().cased_mut(*insensitive).get_mut(col_name) + { val.update_data_at_cell_path(path, new_val)?; } else { return Err(ShellError::CantFindColumn { @@ -1377,7 +1384,11 @@ impl Value { let v_span = val.span(); match val { Value::Record { val: record, .. } => { - if record.to_mut().remove(col_name).is_none() && !optional { + let value = record + .to_mut() + .cased_mut(*insensitive) + .remove(col_name); + if value.is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), span: Some(*span), @@ -1397,7 +1408,13 @@ impl Value { Ok(()) } Value::Record { val: record, .. } => { - if record.to_mut().remove(col_name).is_none() && !optional { + if record + .to_mut() + .cased_mut(*insensitive) + .remove(col_name) + .is_none() + && !optional + { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), span: Some(*span), @@ -1453,7 +1470,11 @@ impl Value { let v_span = val.span(); match val { Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + let val = record + .to_mut() + .cased_mut(*insensitive) + .get_mut(col_name); + if let Some(val) = val { val.remove_data_at_cell_path(path)?; } else if !optional { return Err(ShellError::CantFindColumn { @@ -1475,7 +1496,9 @@ impl Value { Ok(()) } Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + if let Some(val) = + record.to_mut().cased_mut(*insensitive).get_mut(col_name) + { val.remove_data_at_cell_path(path)?; } else if !optional { return Err(ShellError::CantFindColumn { @@ -1532,6 +1555,7 @@ impl Value { PathMember::String { val: col_name, span, + insensitive, .. } => match self { Value::List { vals, .. } => { @@ -1540,7 +1564,9 @@ impl Value { match val { Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = + record.cased_mut(*insensitive).get_mut(col_name) + { if path.is_empty() { return Err(ShellError::ColumnAlreadyExists { col_name: col_name.clone(), @@ -1574,7 +1600,7 @@ impl Value { } Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = record.cased_mut(*insensitive).get_mut(col_name) { if path.is_empty() { return Err(ShellError::ColumnAlreadyExists { col_name: col_name.clone(), @@ -2095,14 +2121,9 @@ fn get_value_member<'a>( let span = current.span(); match current { Value::Record { val, .. } => { - if let Some(found) = val.iter().rev().find(|x| { - if *insensitive { - x.0.eq_ignore_case(column_name) - } else { - x.0 == column_name - } - }) { - Ok(ControlFlow::Continue(Cow::Borrowed(found.1))) + let found = val.cased(*insensitive).get(column_name); + if let Some(found) = found { + Ok(ControlFlow::Continue(Cow::Borrowed(found))) } else if *optional { Ok(ControlFlow::Break(*origin_span)) // short-circuit @@ -2129,14 +2150,9 @@ fn get_value_member<'a>( let val_span = val.span(); match val { Value::Record { val, .. } => { - if let Some(found) = val.iter().rev().find(|x| { - if *insensitive { - x.0.eq_ignore_case(column_name) - } else { - x.0 == column_name - } - }) { - Ok(found.1.clone()) + let found = val.cased(*insensitive).get(column_name); + if let Some(found) = found { + Ok(found.clone()) } else if *optional { Ok(Value::nothing(*origin_span)) } else if let Some(suggestion) = diff --git a/crates/nu-protocol/src/value/record.rs b/crates/nu-protocol/src/value/record.rs index 2b667a7cf0..db86786a9a 100644 --- a/crates/nu-protocol/src/value/record.rs +++ b/crates/nu-protocol/src/value/record.rs @@ -3,6 +3,7 @@ use std::{iter::FusedIterator, ops::RangeBounds}; use crate::{ShellError, Span, Value}; +use nu_utils::IgnoreCaseExt; use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize}; #[derive(Debug, Clone, Default)] @@ -10,6 +11,60 @@ pub struct Record { inner: Vec<(String, Value)>, } +pub struct CasedRecord { + record: R, + insensitive: bool, +} + +impl CasedRecord { + fn cmp(&self, lhs: &str, rhs: &str) -> bool { + if self.insensitive { + lhs.eq_ignore_case(rhs) + } else { + lhs == rhs + } + } +} + +impl<'a> CasedRecord<&'a Record> { + pub fn contains(&self, col: impl AsRef) -> bool { + self.record.columns().any(|k| self.cmp(k, col.as_ref())) + } + + pub fn index_of(&self, col: impl AsRef) -> Option { + self.record + .columns() + .rposition(|k| self.cmp(k, col.as_ref())) + } + + pub fn get(self, col: impl AsRef) -> Option<&'a Value> { + let idx = self.index_of(col)?; + let (_, value) = self.record.get_index(idx)?; + Some(value) + } +} + +impl<'a> CasedRecord<&'a mut Record> { + fn shared(&'a self) -> CasedRecord<&'a Record> { + CasedRecord { + record: &*self.record, + insensitive: self.insensitive, + } + } + + pub fn get_mut(self, col: impl AsRef) -> Option<&'a mut Value> { + let idx = self.shared().index_of(col)?; + let (_, value) = self.record.get_index_mut(idx)?; + Some(value) + } + + pub fn remove(&mut self, col: impl AsRef) -> Option { + let idx = self.shared().index_of(col)?; + let (_, val) = self.record.inner.remove(idx); + Some(val) + } +} + impl Record { pub fn new() -> Self { Self::default() @@ -21,6 +76,20 @@ impl Record { } } + pub fn cased(&self, insensitive: bool) -> CasedRecord<&Record> { + CasedRecord { + record: self, + insensitive, + } + } + + pub fn cased_mut(&mut self, insensitive: bool) -> CasedRecord<&mut Record> { + CasedRecord { + record: self, + insensitive, + } + } + /// Create a [`Record`] from a `Vec` of columns and a `Vec` of [`Value`]s /// /// Returns an error if `cols` and `vals` have different lengths. @@ -108,6 +177,12 @@ impl Record { self.inner.get(idx).map(|(col, val): &(_, _)| (col, val)) } + pub fn get_index_mut(&mut self, idx: usize) -> Option<(&mut String, &mut Value)> { + self.inner + .get_mut(idx) + .map(|(col, val): &mut (_, _)| (col, val)) + } + /// Remove single value by key /// /// Returns `None` if key not found