feat(record): Add CasedRecord, Record wrapper that affects case sensitivity

Make case sensitivity explicit in various places.
This commit is contained in:
Bahex 2025-05-04 16:28:52 +03:00
parent ea31bd3918
commit e8b6783529
2 changed files with 118 additions and 27 deletions

View File

@ -31,7 +31,7 @@ use fancy_regex::Regex;
use nu_utils::{ use nu_utils::{
contains_emoji, contains_emoji,
locale::{get_system_locale_string, LOCALE_OVERRIDE_ENV_VAR}, locale::{get_system_locale_string, LOCALE_OVERRIDE_ENV_VAR},
IgnoreCaseExt, SharedCow, SharedCow,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -1182,6 +1182,7 @@ impl Value {
PathMember::String { PathMember::String {
val: col_name, val: col_name,
span, span,
insensitive,
.. ..
} => match self { } => match self {
Value::List { vals, .. } => { Value::List { vals, .. } => {
@ -1189,7 +1190,9 @@ impl Value {
match val { match val {
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
let record = record.to_mut(); 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())?; val.upsert_data_at_cell_path(path, new_val.clone())?;
} else { } else {
let new_col = let new_col =
@ -1210,7 +1213,7 @@ impl Value {
} }
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
let record = record.to_mut(); 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)?; val.upsert_data_at_cell_path(path, new_val)?;
} else { } else {
let new_col = Value::with_data_at_cell_path(path, new_val.clone())?; let new_col = Value::with_data_at_cell_path(path, new_val.clone())?;
@ -1282,6 +1285,7 @@ impl Value {
PathMember::String { PathMember::String {
val: col_name, val: col_name,
span, span,
insensitive,
.. ..
} => match self { } => match self {
Value::List { vals, .. } => { Value::List { vals, .. } => {
@ -1289,7 +1293,9 @@ impl Value {
let v_span = val.span(); let v_span = val.span();
match val { match val {
Value::Record { val: record, .. } => { 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())?; val.update_data_at_cell_path(path, new_val.clone())?;
} else { } else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
@ -1311,7 +1317,8 @@ impl Value {
} }
} }
Value::Record { val: record, .. } => { 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)?; val.update_data_at_cell_path(path, new_val)?;
} else { } else {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
@ -1377,7 +1384,11 @@ impl Value {
let v_span = val.span(); let v_span = val.span();
match val { match val {
Value::Record { val: record, .. } => { 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 { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: Some(*span), span: Some(*span),
@ -1397,7 +1408,13 @@ impl Value {
Ok(()) Ok(())
} }
Value::Record { val: record, .. } => { 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 { return Err(ShellError::CantFindColumn {
col_name: col_name.clone(), col_name: col_name.clone(),
span: Some(*span), span: Some(*span),
@ -1453,7 +1470,11 @@ impl Value {
let v_span = val.span(); let v_span = val.span();
match val { match val {
Value::Record { val: record, .. } => { 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)?; val.remove_data_at_cell_path(path)?;
} else if !optional { } else if !optional {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
@ -1475,7 +1496,9 @@ impl Value {
Ok(()) Ok(())
} }
Value::Record { val: record, .. } => { 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)?; val.remove_data_at_cell_path(path)?;
} else if !optional { } else if !optional {
return Err(ShellError::CantFindColumn { return Err(ShellError::CantFindColumn {
@ -1532,6 +1555,7 @@ impl Value {
PathMember::String { PathMember::String {
val: col_name, val: col_name,
span, span,
insensitive,
.. ..
} => match self { } => match self {
Value::List { vals, .. } => { Value::List { vals, .. } => {
@ -1540,7 +1564,9 @@ impl Value {
match val { match val {
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
let record = record.to_mut(); 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() { if path.is_empty() {
return Err(ShellError::ColumnAlreadyExists { return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.clone(), col_name: col_name.clone(),
@ -1574,7 +1600,7 @@ impl Value {
} }
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
let record = record.to_mut(); 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() { if path.is_empty() {
return Err(ShellError::ColumnAlreadyExists { return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.clone(), col_name: col_name.clone(),
@ -2095,14 +2121,9 @@ fn get_value_member<'a>(
let span = current.span(); let span = current.span();
match current { match current {
Value::Record { val, .. } => { Value::Record { val, .. } => {
if let Some(found) = val.iter().rev().find(|x| { let found = val.cased(*insensitive).get(column_name);
if *insensitive { if let Some(found) = found {
x.0.eq_ignore_case(column_name) Ok(ControlFlow::Continue(Cow::Borrowed(found)))
} else {
x.0 == column_name
}
}) {
Ok(ControlFlow::Continue(Cow::Borrowed(found.1)))
} else if *optional { } else if *optional {
Ok(ControlFlow::Break(*origin_span)) Ok(ControlFlow::Break(*origin_span))
// short-circuit // short-circuit
@ -2129,14 +2150,9 @@ fn get_value_member<'a>(
let val_span = val.span(); let val_span = val.span();
match val { match val {
Value::Record { val, .. } => { Value::Record { val, .. } => {
if let Some(found) = val.iter().rev().find(|x| { let found = val.cased(*insensitive).get(column_name);
if *insensitive { if let Some(found) = found {
x.0.eq_ignore_case(column_name) Ok(found.clone())
} else {
x.0 == column_name
}
}) {
Ok(found.1.clone())
} else if *optional { } else if *optional {
Ok(Value::nothing(*origin_span)) Ok(Value::nothing(*origin_span))
} else if let Some(suggestion) = } else if let Some(suggestion) =

View File

@ -3,6 +3,7 @@ use std::{iter::FusedIterator, ops::RangeBounds};
use crate::{ShellError, Span, Value}; use crate::{ShellError, Span, Value};
use nu_utils::IgnoreCaseExt;
use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize}; use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize};
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -10,6 +11,60 @@ pub struct Record {
inner: Vec<(String, Value)>, inner: Vec<(String, Value)>,
} }
pub struct CasedRecord<R> {
record: R,
insensitive: bool,
}
impl<R> CasedRecord<R> {
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<str>) -> bool {
self.record.columns().any(|k| self.cmp(k, col.as_ref()))
}
pub fn index_of(&self, col: impl AsRef<str>) -> Option<usize> {
self.record
.columns()
.rposition(|k| self.cmp(k, col.as_ref()))
}
pub fn get(self, col: impl AsRef<str>) -> 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<str>) -> 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<str>) -> Option<Value> {
let idx = self.shared().index_of(col)?;
let (_, val) = self.record.inner.remove(idx);
Some(val)
}
}
impl Record { impl Record {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() 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 /// Create a [`Record`] from a `Vec` of columns and a `Vec` of [`Value`]s
/// ///
/// Returns an error if `cols` and `vals` have different lengths. /// 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)) 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 /// Remove single value by key
/// ///
/// Returns `None` if key not found /// Returns `None` if key not found