nushell/crates/nu_plugin_inc/src/inc.rs
Reilly Wood 21b84a6d65
Optional members in cell paths: Attempt 2 (#8379)
This is a follow up from https://github.com/nushell/nushell/pull/7540.
Please provide feedback if you have the time!

## Summary

This PR lets you use `?` to indicate that a member in a cell path is
optional and Nushell should return `null` if that member cannot be
accessed.

Unlike the previous PR, `?` is now a _postfix_ modifier for cell path
members. A cell path of `.foo?.bar` means that `foo` is optional and
`bar` is not.

`?` does _not_ suppress all errors; it is intended to help in situations
where data has "holes", i.e. the data types are correct but something is
missing. Type mismatches (like trying to do a string path access on a
date) will still fail.

### Record Examples

```bash

{ foo: 123 }.foo # returns 123

{ foo: 123 }.bar # errors
{ foo: 123 }.bar? # returns null

{ foo: 123 } | get bar # errors
{ foo: 123 } | get bar? # returns null

{ foo: 123 }.bar.baz # errors
{ foo: 123 }.bar?.baz # errors because `baz` is not present on the result from `bar?`
{ foo: 123 }.bar.baz? # errors
{ foo: 123 }.bar?.baz? # returns null
```

### List Examples
```
〉[{foo: 1} {foo: 2} {}].foo
Error: nu:🐚:column_not_found

  × Cannot find column
   ╭─[entry #30:1:1]
 1 │ [{foo: 1} {foo: 2} {}].foo
   ·                    ─┬  ─┬─
   ·                     │   ╰── cannot find column 'foo'
   ·                     ╰── value originates here
   ╰────
〉[{foo: 1} {foo: 2} {}].foo?
╭───┬───╮
│ 0 │ 1 │
│ 1 │ 2 │
│ 2 │   │
╰───┴───╯
〉[{foo: 1} {foo: 2} {}].foo?.2 | describe
nothing

〉[a b c].4? | describe
nothing

〉[{foo: 1} {foo: 2} {}] | where foo? == 1
╭───┬─────╮
│ # │ foo │
├───┼─────┤
│ 0 │   1 │
╰───┴─────╯
```

# Breaking changes

1. Column names with `?` in them now need to be quoted.
2. The `-i`/`--ignore-errors` flag has been removed from `get` and
`select`
1. After this PR, most `get` error handling can be done with `?` and/or
`try`/`catch`.
4. Cell path accesses like this no longer work without a `?`:
```bash
〉[{a:1 b:2} {a:3}].b.0
2
```
We had some clever code that was able to recognize that since we only
want row `0`, it's OK if other rows are missing column `b`. I removed
that because it's tricky to maintain, and now that query needs to be
written like:


```bash
〉[{a:1 b:2} {a:3}].b?.0
2
```

I think the regression is acceptable for now. I plan to do more work in
the future to enable streaming of cell path accesses, and when that
happens I'll be able to make `.b.0` work again.
2023-03-15 20:50:58 -07:00

171 lines
4.8 KiB
Rust

use nu_plugin::LabeledError;
use nu_protocol::{ast::CellPath, Span, Value};
use semver::{BuildMetadata, Prerelease, Version};
#[derive(Debug, Eq, PartialEq)]
pub enum Action {
SemVerAction(SemVerAction),
Default,
}
#[derive(Debug, Eq, PartialEq)]
pub enum SemVerAction {
Major,
Minor,
Patch,
}
#[derive(Default)]
pub struct Inc {
pub error: Option<String>,
pub cell_path: Option<CellPath>,
pub action: Option<Action>,
}
impl Inc {
pub fn new() -> Self {
Default::default()
}
fn apply(&self, input: &str, head: Span) -> Value {
match &self.action {
Some(Action::SemVerAction(act_on)) => {
let mut ver = match semver::Version::parse(input) {
Ok(parsed_ver) => parsed_ver,
Err(_) => return Value::string(input, head),
};
match act_on {
SemVerAction::Major => Self::increment_major(&mut ver),
SemVerAction::Minor => Self::increment_minor(&mut ver),
SemVerAction::Patch => Self::increment_patch(&mut ver),
}
Value::string(ver.to_string(), head)
}
Some(Action::Default) | None => {
if let Ok(v) = input.parse::<u64>() {
Value::string((v + 1).to_string(), head)
} else {
Value::string(input, head)
}
}
}
}
pub fn increment_patch(v: &mut Version) {
v.patch += 1;
v.pre = Prerelease::EMPTY;
v.build = BuildMetadata::EMPTY;
}
pub fn increment_minor(v: &mut Version) {
v.minor += 1;
v.patch = 0;
v.pre = Prerelease::EMPTY;
v.build = BuildMetadata::EMPTY;
}
pub fn increment_major(v: &mut Version) {
v.major += 1;
v.minor = 0;
v.patch = 0;
v.pre = Prerelease::EMPTY;
v.build = BuildMetadata::EMPTY;
}
pub fn for_semver(&mut self, part: SemVerAction) {
if self.permit() {
self.action = Some(Action::SemVerAction(part));
} else {
self.log_error("can only apply one");
}
}
fn permit(&mut self) -> bool {
self.action.is_none()
}
fn log_error(&mut self, message: &str) {
self.error = Some(message.to_string());
}
pub fn usage() -> &'static str {
"Usage: inc field [--major|--minor|--patch]"
}
pub fn inc(&self, head: Span, value: &Value) -> Result<Value, LabeledError> {
if let Some(cell_path) = &self.cell_path {
let working_value = value.clone();
let cell_value = working_value.follow_cell_path(&cell_path.members, false)?;
let cell_value = self.inc_value(head, &cell_value)?;
let mut value = value.clone();
value
.update_data_at_cell_path(&cell_path.members, cell_value)
.map_err(|x| {
let error: LabeledError = x.into();
error
})?;
Ok(value)
} else {
self.inc_value(head, value)
}
}
pub fn inc_value(&self, head: Span, value: &Value) -> Result<Value, LabeledError> {
match value {
Value::Int { val, span } => Ok(Value::int(val + 1, *span)),
Value::String { val, .. } => Ok(self.apply(val, head)),
x => {
let msg = x.as_string().map_err(|e| LabeledError {
label: "Unable to extract string".into(),
msg: format!("value cannot be converted to string {x:?} - {e}"),
span: Some(head),
})?;
Err(LabeledError {
label: "Incorrect value".into(),
msg,
span: Some(head),
})
}
}
}
}
#[cfg(test)]
mod tests {
mod semver {
use nu_protocol::{Span, Value};
use crate::inc::SemVerAction;
use crate::Inc;
#[test]
fn major() {
let expected = Value::test_string("1.0.0");
let mut inc = Inc::new();
inc.for_semver(SemVerAction::Major);
assert_eq!(inc.apply("0.1.3", Span::test_data()), expected)
}
#[test]
fn minor() {
let expected = Value::test_string("0.2.0");
let mut inc = Inc::new();
inc.for_semver(SemVerAction::Minor);
assert_eq!(inc.apply("0.1.3", Span::test_data()), expected)
}
#[test]
fn patch() {
let expected = Value::test_string("0.1.4");
let mut inc = Inc::new();
inc.for_semver(SemVerAction::Patch);
assert_eq!(inc.apply("0.1.3", Span::test_data()), expected)
}
}
}