feat(cell-path): opt-in case insensitivity for cell-path members

- Add `CellPath::String::insensitive: bool`, represented with an exclamation
  mark in `Display`
- Update `into cell-path` and `split cell-path` to take case
  insensitivity into account while constructing and splitting cell-paths
This commit is contained in:
Bahex
2025-05-04 09:58:06 +03:00
parent 39b95fc59e
commit 0294419c76
4 changed files with 84 additions and 22 deletions

View File

@ -17,7 +17,12 @@ impl Command for IntoCellPath {
(Type::List(Box::new(Type::Any)), Type::CellPath), (Type::List(Box::new(Type::Any)), Type::CellPath),
( (
Type::List(Box::new(Type::Record( Type::List(Box::new(Type::Record(
[("value".into(), Type::Any), ("optional".into(), Type::Bool)].into(), [
("value".into(), Type::Any),
("optional".into(), Type::Bool),
("insensitive".into(), Type::Bool),
]
.into(),
))), ))),
Type::CellPath, Type::CellPath,
), ),
@ -69,8 +74,8 @@ impl Command for IntoCellPath {
example: "'some.path' | split row '.' | into cell-path", example: "'some.path' | split row '.' | into cell-path",
result: Some(Value::test_cell_path(CellPath { result: Some(Value::test_cell_path(CellPath {
members: vec![ members: vec![
PathMember::test_string("some".into(), false), PathMember::test_string("some".into(), false, false),
PathMember::test_string("path".into(), false), PathMember::test_string("path".into(), false, false),
], ],
})), })),
}, },
@ -80,9 +85,9 @@ impl Command for IntoCellPath {
result: Some(Value::test_cell_path(CellPath { result: Some(Value::test_cell_path(CellPath {
members: vec![ members: vec![
PathMember::test_int(5, false), PathMember::test_int(5, false),
PathMember::test_string("c".into(), false), PathMember::test_string("c".into(), false, false),
PathMember::test_int(7, false), PathMember::test_int(7, false),
PathMember::test_string("h".into(), false), PathMember::test_string("h".into(), false, false),
], ],
})), })),
}, },
@ -92,7 +97,7 @@ impl Command for IntoCellPath {
result: Some(Value::test_cell_path(CellPath { result: Some(Value::test_cell_path(CellPath {
members: vec![ members: vec![
PathMember::test_int(5, true), PathMember::test_int(5, true),
PathMember::test_string("c".into(), false), PathMember::test_string("c".into(), false, false),
], ],
})), })),
}, },
@ -175,6 +180,12 @@ fn record_to_path_member(
} }
}; };
if let Some(insensitive) = record.get("insensitive") {
if insensitive.as_bool()? {
member.make_insensitive();
}
};
Ok(member) Ok(member)
} }
@ -196,7 +207,7 @@ fn value_to_path_member(val: &Value, span: Span) -> Result<PathMember, ShellErro
let val_span = val.span(); let val_span = val.span();
let member = match val { let member = match val {
Value::Int { val, .. } => int_to_path_member(*val, val_span)?, Value::Int { val, .. } => int_to_path_member(*val, val_span)?,
Value::String { val, .. } => PathMember::string(val.into(), false, val_span), Value::String { val, .. } => PathMember::string(val.into(), false, false, val_span),
Value::Record { val, .. } => record_to_path_member(val, val_span, span)?, Value::Record { val, .. } => record_to_path_member(val, val_span, span)?,
other => { other => {
return Err(ShellError::CantConvert { return Err(ShellError::CantConvert {

View File

@ -16,7 +16,12 @@ impl Command for SplitCellPath {
( (
Type::CellPath, Type::CellPath,
Type::List(Box::new(Type::Record( Type::List(Box::new(Type::Record(
[("value".into(), Type::Any), ("optional".into(), Type::Bool)].into(), [
("value".into(), Type::Any),
("optional".into(), Type::Bool),
("insensitive".into(), Type::Bool),
]
.into(),
))), ))),
), ),
]) ])
@ -114,19 +119,29 @@ fn split_cell_path(val: CellPath, span: Span) -> Result<Value, ShellError> {
struct PathMemberRecord { struct PathMemberRecord {
value: Value, value: Value,
optional: bool, optional: bool,
insensitive: bool,
} }
impl PathMemberRecord { impl PathMemberRecord {
fn from_path_member(pm: PathMember) -> Self { fn from_path_member(pm: PathMember) -> Self {
let (optional, internal_span) = match pm { let (optional, insensitive, internal_span) = match pm {
PathMember::String { optional, span, .. } PathMember::String {
| PathMember::Int { optional, span, .. } => (optional, span), optional,
insensitive,
span,
..
} => (optional, insensitive, span),
PathMember::Int { optional, span, .. } => (optional, false, span),
}; };
let value = match pm { let value = match pm {
PathMember::String { val, .. } => Value::string(val, internal_span), PathMember::String { val, .. } => Value::string(val, internal_span),
PathMember::Int { val, .. } => Value::int(val as i64, internal_span), PathMember::Int { val, .. } => Value::int(val as i64, internal_span),
}; };
Self { value, optional } Self {
value,
optional,
insensitive,
}
} }
} }

View File

@ -15,7 +15,7 @@ pub fn empty(
if !columns.is_empty() { if !columns.is_empty() {
for val in input { for val in input {
for column in &columns { for column in &columns {
if !val.follow_cell_path(&column.members, false)?.is_nothing() { if !val.follow_cell_path(&column.members)?.is_nothing() {
return Ok(Value::bool(negate, head).into_pipeline_data()); return Ok(Value::bool(negate, head).into_pipeline_data());
} }
} }

View File

@ -14,6 +14,8 @@ pub enum PathMember {
/// If marked as optional don't throw an error if not found but perform default handling /// If marked as optional don't throw an error if not found but perform default handling
/// (e.g. return `Value::Nothing`) /// (e.g. return `Value::Nothing`)
optional: bool, optional: bool,
/// If marked as insensitive, column lookup happens case insensitively
insensitive: bool,
}, },
/// Accessing a member by index (i.e. row of a table or item in a list) /// Accessing a member by index (i.e. row of a table or item in a list)
Int { Int {
@ -34,11 +36,12 @@ impl PathMember {
} }
} }
pub fn string(val: String, optional: bool, span: Span) -> Self { pub fn string(val: String, optional: bool, insensitive: bool, span: Span) -> Self {
PathMember::String { PathMember::String {
val, val,
span, span,
optional, optional,
insensitive,
} }
} }
@ -50,10 +53,11 @@ impl PathMember {
} }
} }
pub fn test_string(val: String, optional: bool) -> Self { pub fn test_string(val: String, optional: bool, insensitive: bool) -> Self {
PathMember::String { PathMember::String {
val, val,
optional, optional,
insensitive,
span: Span::test_data(), span: Span::test_data(),
} }
} }
@ -69,6 +73,16 @@ impl PathMember {
} }
} }
pub fn make_insensitive(&mut self) {
match self {
PathMember::String {
ref mut insensitive,
..
} => *insensitive = true,
PathMember::Int { .. } => {}
}
}
pub fn span(&self) -> Span { pub fn span(&self) -> Span {
match self { match self {
PathMember::String { span, .. } => *span, PathMember::String { span, .. } => *span,
@ -182,6 +196,12 @@ impl CellPath {
} }
} }
pub fn make_insensitive(&mut self) {
for member in &mut self.members {
member.make_insensitive();
}
}
// Formats the cell-path as a column name, i.e. without quoting and optional markers ('?'). // Formats the cell-path as a column name, i.e. without quoting and optional markers ('?').
pub fn to_column_name(&self) -> String { pub fn to_column_name(&self) -> String {
let mut s = String::new(); let mut s = String::new();
@ -213,14 +233,20 @@ impl Display for CellPath {
let question_mark = if *optional { "?" } else { "" }; let question_mark = if *optional { "?" } else { "" };
write!(f, ".{val}{question_mark}")? write!(f, ".{val}{question_mark}")?
} }
PathMember::String { val, optional, .. } => { PathMember::String {
val,
optional,
insensitive,
..
} => {
let question_mark = if *optional { "?" } else { "" }; let question_mark = if *optional { "?" } else { "" };
let exclamation_mark = if *insensitive { "!" } else { "" };
let val = if needs_quoting(val) { let val = if needs_quoting(val) {
&escape_quote_string(val) &escape_quote_string(val)
} else { } else {
val val
}; };
write!(f, ".{val}{question_mark}")? write!(f, ".{val}{exclamation_mark}{question_mark}")?
} }
} }
} }
@ -243,7 +269,11 @@ mod test {
fn path_member_partial_ord() { fn path_member_partial_ord() {
assert_eq!( assert_eq!(
Some(Greater), Some(Greater),
PathMember::test_int(5, true).partial_cmp(&PathMember::test_string("e".into(), true)) PathMember::test_int(5, true).partial_cmp(&PathMember::test_string(
"e".into(),
true,
false
))
); );
assert_eq!( assert_eq!(
@ -258,14 +288,20 @@ mod test {
assert_eq!( assert_eq!(
Some(Greater), Some(Greater),
PathMember::test_string("e".into(), true) PathMember::test_string("e".into(), true, false).partial_cmp(&PathMember::test_string(
.partial_cmp(&PathMember::test_string("e".into(), false)) "e".into(),
false,
false
))
); );
assert_eq!( assert_eq!(
Some(Greater), Some(Greater),
PathMember::test_string("f".into(), true) PathMember::test_string("f".into(), true, false).partial_cmp(&PathMember::test_string(
.partial_cmp(&PathMember::test_string("e".into(), true)) "e".into(),
true,
false
))
); );
} }
} }