Create Record type (#10103)

# Description
This PR creates a new `Record` type to reduce duplicate code and
possibly bugs as well. (This is an edited version of #9648.)
- `Record` implements `FromIterator` and `IntoIterator` and so can be
iterated over or collected into. For example, this helps with
conversions to and from (hash)maps. (Also, no more
`cols.iter().zip(vals)`!)
- `Record` has a `push(col, val)` function to help insure that the
number of columns is equal to the number of values. I caught a few
potential bugs thanks to this (e.g. in the `ls` command).
- Finally, this PR also adds a `record!` macro that helps simplify
record creation. It is used like so:
   ```rust
   record! {
       "key1" => some_value,
       "key2" => Value::string("text", span),
       "key3" => Value::int(optional_int.unwrap_or(0), span),
       "key4" => Value::bool(config.setting, span),
   }
   ```
Since macros hinder formatting, etc., the right hand side values should
be relatively short and sweet like the examples above.

Where possible, prefer `record!` or `.collect()` on an iterator instead
of multiple `Record::push`s, since the first two automatically set the
record capacity and do less work overall.

# User-Facing Changes
Besides the changes in `nu-protocol` the only other breaking changes are
to `nu-table::{ExpandedTable::build_map, JustTable::kv_table}`.
This commit is contained in:
Ian Manske
2023-08-24 19:50:29 +00:00
committed by GitHub
parent 030e749fe7
commit 8da27a1a09
195 changed files with 4211 additions and 6245 deletions

View File

@ -3,7 +3,7 @@ use nu_engine::CallExt;
use nu_protocol::{
ast::{Call, CellPath},
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -60,31 +60,26 @@ impl Command for SubCommand {
example: "[[value]; ['false'] ['1'] [0] [1.0] [true]] | into bool value",
result: Some(Value::List {
vals: vec![
Value::Record {
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::bool(false, span)],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::bool(true, span)],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::bool(false, span)],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::bool(true, span)],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::bool(true, span)],
span,
},
}),
],
span,
}),

View File

@ -3,7 +3,7 @@ use nu_engine::CallExt;
use nu_protocol::{
ast::{Call, CellPath},
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -63,11 +63,10 @@ impl Command for SubCommand {
description: "Convert string to decimal in table",
example: "[[num]; ['5.01']] | into decimal num",
result: Some(Value::List {
vals: vec![Value::Record {
vals: vec![Value::test_record(Record {
cols: vec!["num".to_string()],
vals: vec![Value::test_float(5.01)],
span: Span::test_data(),
}],
})],
span: Span::test_data(),
}),
},

View File

@ -3,7 +3,8 @@ use nu_parser::{parse_unit_value, DURATION_UNIT_GROUPS};
use nu_protocol::{
ast::{Call, CellPath, Expr},
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Unit, Value,
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Unit,
Value,
};
const NS_PER_SEC: i64 = 1_000_000_000;
@ -80,46 +81,41 @@ impl Command for SubCommand {
"[[value]; ['1sec'] ['2min'] ['3hr'] ['4day'] ['5wk']] | into duration value",
result: Some(Value::List {
vals: vec![
Value::Record {
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::Duration {
val: NS_PER_SEC,
span,
}],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::Duration {
val: 2 * 60 * NS_PER_SEC,
span,
}],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::Duration {
val: 3 * 60 * 60 * NS_PER_SEC,
span,
}],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::Duration {
val: 4 * 24 * 60 * 60 * NS_PER_SEC,
span,
}],
span,
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::Duration {
val: 5 * 7 * 24 * 60 * 60 * NS_PER_SEC,
span,
}],
span,
},
}),
],
span,
}),

View File

@ -3,7 +3,7 @@ use nu_engine::CallExt;
use nu_protocol::{
ast::{Call, CellPath},
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
Category, Example, PipelineData, Record, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -81,7 +81,7 @@ impl Command for SubCommand {
example: r#"[[device size]; ["/dev/sda1" "200"] ["/dev/loop0" "50"]] | into filesize size"#,
result: Some(Value::List {
vals: vec![
Value::Record {
Value::test_record(Record {
cols: vec!["device".to_string(), "size".to_string()],
vals: vec![
Value::String {
@ -93,9 +93,8 @@ impl Command for SubCommand {
span: Span::test_data(),
},
],
span: Span::test_data(),
},
Value::Record {
}),
Value::test_record(Record {
cols: vec!["device".to_string(), "size".to_string()],
vals: vec![
Value::String {
@ -107,8 +106,7 @@ impl Command for SubCommand {
span: Span::test_data(),
},
],
span: Span::test_data(),
},
}),
],
span: Span::test_data(),
}),

View File

@ -1,10 +1,11 @@
use chrono::{DateTime, Datelike, FixedOffset, Timelike};
use nu_protocol::format_duration_as_timeperiod;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value,
format_duration_as_timeperiod, record, Category, Example, IntoPipelineData, PipelineData,
Record, ShellError, Signature, Span, Type, Value,
};
#[derive(Clone)]
pub struct SubCommand;
@ -50,42 +51,39 @@ impl Command for SubCommand {
Example {
description: "Convert from one row table to record",
example: "[[value]; [false]] | into record",
result: Some(Value::Record {
result: Some(Value::test_record(Record {
cols: vec!["value".to_string()],
vals: vec![Value::bool(false, span)],
span,
}),
})),
},
Example {
description: "Convert from list to record",
example: "[1 2 3] | into record",
result: Some(Value::Record {
result: Some(Value::test_record(Record {
cols: vec!["0".to_string(), "1".to_string(), "2".to_string()],
vals: vec![
Value::Int { val: 1, span },
Value::Int { val: 2, span },
Value::Int { val: 3, span },
],
span,
}),
})),
},
Example {
description: "Convert from range to record",
example: "0..2 | into record",
result: Some(Value::Record {
result: Some(Value::test_record(Record {
cols: vec!["0".to_string(), "1".to_string(), "2".to_string()],
vals: vec![
Value::Int { val: 0, span },
Value::Int { val: 1, span },
Value::Int { val: 2, span },
],
span,
}),
})),
},
Example {
description: "convert duration to record (weeks max)",
example: "(-500day - 4hr - 5sec) | into record",
result: Some(Value::Record {
result: Some(Value::test_record(Record {
cols: vec![
"week".into(),
"day".into(),
@ -103,22 +101,20 @@ impl Command for SubCommand {
span,
},
],
span,
}),
})),
},
Example {
description: "convert record to record",
example: "{a: 1, b: 2} | into record",
result: Some(Value::Record {
result: Some(Value::test_record(Record {
cols: vec!["a".to_string(), "b".to_string()],
vals: vec![Value::Int { val: 1, span }, Value::Int { val: 2, span }],
span,
}),
})),
},
Example {
description: "convert date to record",
example: "2020-04-12T22:10:57+02:00 | into record",
result: Some(Value::Record {
result: Some(Value::test_record(Record {
cols: vec![
"year".into(),
"month".into(),
@ -140,8 +136,7 @@ impl Command for SubCommand {
span,
},
],
span,
}),
})),
},
]
}
@ -155,34 +150,26 @@ fn into_record(
let input = input.into_value(call.head);
let input_type = input.get_type();
let res = match input {
Value::Date { val, span } => parse_date_into_record(Ok(val), span),
Value::Date { val, span } => parse_date_into_record(val, span),
Value::Duration { val, span } => parse_duration_into_record(val, span),
Value::List { mut vals, span } => match input_type {
Type::Table(..) if vals.len() == 1 => vals.pop().expect("already checked 1 item"),
_ => {
let mut cols = vec![];
let mut values = vec![];
for (idx, val) in vals.into_iter().enumerate() {
cols.push(format!("{idx}"));
values.push(val);
}
Value::Record {
cols,
vals: values,
span,
}
}
_ => Value::record(
vals.into_iter()
.enumerate()
.map(|(idx, val)| (format!("{idx}"), val))
.collect(),
span,
),
},
Value::Range { val, span } => {
let mut cols = vec![];
let mut vals = vec![];
for (idx, val) in val.into_range_iter(engine_state.ctrlc.clone())?.enumerate() {
cols.push(format!("{idx}"));
vals.push(val);
}
Value::Record { cols, vals, span }
}
Value::Record { cols, vals, span } => Value::Record { cols, vals, span },
Value::Range { val, span } => Value::record(
val.into_range_iter(engine_state.ctrlc.clone())?
.enumerate()
.map(|(idx, val)| (format!("{idx}"), val))
.collect(),
span,
),
Value::Record { val, span } => Value::Record { val, span },
Value::Error { .. } => input,
other => Value::Error {
error: Box::new(ShellError::OnlySupportsThisInputType {
@ -196,87 +183,50 @@ fn into_record(
Ok(res.into_pipeline_data())
}
fn parse_date_into_record(date: Result<DateTime<FixedOffset>, Value>, span: Span) -> Value {
let cols = vec![
"year".into(),
"month".into(),
"day".into(),
"hour".into(),
"minute".into(),
"second".into(),
"timezone".into(),
];
match date {
Ok(x) => {
let vals = vec![
Value::Int {
val: x.year() as i64,
span,
},
Value::Int {
val: x.month() as i64,
span,
},
Value::Int {
val: x.day() as i64,
span,
},
Value::Int {
val: x.hour() as i64,
span,
},
Value::Int {
val: x.minute() as i64,
span,
},
Value::Int {
val: x.second() as i64,
span,
},
Value::String {
val: x.offset().to_string(),
span,
},
];
Value::Record { cols, vals, span }
}
Err(e) => e,
}
fn parse_date_into_record(date: DateTime<FixedOffset>, span: Span) -> Value {
Value::record(
record! {
"year" => Value::int(date.year() as i64, span),
"month" => Value::int(date.month() as i64, span),
"day" => Value::int(date.day() as i64, span),
"hour" => Value::int(date.hour() as i64, span),
"minute" => Value::int(date.minute() as i64, span),
"second" => Value::int(date.second() as i64, span),
"timezone" => Value::string(date.offset().to_string(), span),
},
span,
)
}
fn parse_duration_into_record(duration: i64, span: Span) -> Value {
let (sign, periods) = format_duration_as_timeperiod(duration);
let mut cols = vec![];
let mut vals = vec![];
let mut record = Record::new();
for p in periods {
let num_with_unit = p.to_text().to_string();
let split = num_with_unit.split(' ').collect::<Vec<&str>>();
cols.push(match split[1] {
"ns" => "nanosecond".into(),
"µs" => "microsecond".into(),
"ms" => "millisecond".into(),
"sec" => "second".into(),
"min" => "minute".into(),
"hr" => "hour".into(),
"day" => "day".into(),
"wk" => "week".into(),
_ => "unknown".into(),
});
vals.push(Value::Int {
val: split[0].parse::<i64>().unwrap_or(0),
span,
});
record.push(
match split[1] {
"ns" => "nanosecond",
"µs" => "microsecond",
"ms" => "millisecond",
"sec" => "second",
"min" => "minute",
"hr" => "hour",
"day" => "day",
"wk" => "week",
_ => "unknown",
},
Value::int(split[0].parse().unwrap_or(0), span),
);
}
cols.push("sign".into());
vals.push(Value::String {
val: if sign == -1 { "-".into() } else { "+".into() },
span,
});
record.push(
"sign",
Value::string(if sign == -1 { "-" } else { "+" }, span),
);
Value::Record { cols, vals, span }
Value::record(record, span)
}
#[cfg(test)]

View File

@ -257,11 +257,7 @@ fn action(input: &Value, args: &Arguments, span: Span) -> Value {
val: "".to_string(),
span,
},
Value::Record {
cols: _,
vals: _,
span: _,
} => Value::Error {
Value::Record { .. } => Value::Error {
// Watch out for CantConvert's argument order
error: Box::new(ShellError::CantConvert {
to_type: "string".into(),