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::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,
),
@ -69,8 +74,8 @@ impl Command for IntoCellPath {
example: "'some.path' | split row '.' | into cell-path",
result: Some(Value::test_cell_path(CellPath {
members: vec![
PathMember::test_string("some".into(), false),
PathMember::test_string("path".into(), false),
PathMember::test_string("some".into(), false, false),
PathMember::test_string("path".into(), false, false),
],
})),
},
@ -80,9 +85,9 @@ impl Command for IntoCellPath {
result: Some(Value::test_cell_path(CellPath {
members: vec![
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_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 {
members: vec![
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)
}
@ -196,7 +207,7 @@ fn value_to_path_member(val: &Value, span: Span) -> Result<PathMember, ShellErro
let val_span = val.span();
let member = match val {
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)?,
other => {
return Err(ShellError::CantConvert {

View File

@ -16,7 +16,12 @@ impl Command for SplitCellPath {
(
Type::CellPath,
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 {
value: Value,
optional: bool,
insensitive: bool,
}
impl PathMemberRecord {
fn from_path_member(pm: PathMember) -> Self {
let (optional, internal_span) = match pm {
PathMember::String { optional, span, .. }
| PathMember::Int { optional, span, .. } => (optional, span),
let (optional, insensitive, internal_span) = match pm {
PathMember::String {
optional,
insensitive,
span,
..
} => (optional, insensitive, span),
PathMember::Int { optional, span, .. } => (optional, false, span),
};
let value = match pm {
PathMember::String { val, .. } => Value::string(val, 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() {
for val in input {
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());
}
}

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
/// (e.g. return `Value::Nothing`)
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)
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 {
val,
span,
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 {
val,
optional,
insensitive,
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 {
match self {
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 ('?').
pub fn to_column_name(&self) -> String {
let mut s = String::new();
@ -213,14 +233,20 @@ impl Display for CellPath {
let question_mark = if *optional { "?" } else { "" };
write!(f, ".{val}{question_mark}")?
}
PathMember::String { val, optional, .. } => {
PathMember::String {
val,
optional,
insensitive,
..
} => {
let question_mark = if *optional { "?" } else { "" };
let exclamation_mark = if *insensitive { "!" } else { "" };
let val = if needs_quoting(val) {
&escape_quote_string(val)
} else {
val
};
write!(f, ".{val}{question_mark}")?
write!(f, ".{val}{exclamation_mark}{question_mark}")?
}
}
}
@ -243,7 +269,11 @@ mod test {
fn path_member_partial_ord() {
assert_eq!(
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!(
@ -258,14 +288,20 @@ mod test {
assert_eq!(
Some(Greater),
PathMember::test_string("e".into(), true)
.partial_cmp(&PathMember::test_string("e".into(), false))
PathMember::test_string("e".into(), true, false).partial_cmp(&PathMember::test_string(
"e".into(),
false,
false
))
);
assert_eq!(
Some(Greater),
PathMember::test_string("f".into(), true)
.partial_cmp(&PathMember::test_string("e".into(), true))
PathMember::test_string("f".into(), true, false).partial_cmp(&PathMember::test_string(
"e".into(),
true,
false
))
);
}
}