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

@ -2,8 +2,8 @@ use chrono_tz::TZ_VARIANTS;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
Type, Value,
record, Category, Example, IntoInterruptiblePipelineData, PipelineData, Record, ShellError,
Signature, Span, Type, Value,
};
#[derive(Clone)]
@ -40,12 +40,10 @@ impl Command for SubCommand {
Ok(TZ_VARIANTS
.iter()
.map(move |x| {
let cols = vec!["timezone".into()];
let vals = vec![Value::String {
val: x.name().to_string(),
Value::record(
record! { "timezone" => Value::string(x.name(), span) },
span,
}];
Value::Record { cols, vals, span }
)
})
.into_pipeline_data(engine_state.ctrlc.clone()))
}
@ -55,11 +53,10 @@ impl Command for SubCommand {
example: "date list-timezone | where timezone =~ Shanghai",
description: "Show timezone(s) that contains 'Shanghai'",
result: Some(Value::List {
vals: vec![Value::Record {
vals: vec![Value::test_record(Record {
cols: vec!["timezone".into()],
vals: vec![Value::test_string("Asia/Shanghai")],
span: Span::test_data(),
}],
})],
span: Span::test_data(),
}),
}]

View File

@ -3,10 +3,9 @@ use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, PipelineData, ShellError::DatetimeParseError, ShellError::PipelineEmpty,
Signature, Span, Value,
record, Category, Example, PipelineData, Record, ShellError, ShellError::DatetimeParseError,
ShellError::PipelineEmpty, Signature, Span, Type, Value,
};
use nu_protocol::{ShellError, Type};
#[derive(Clone)]
pub struct SubCommand;
@ -78,7 +77,7 @@ impl Command for SubCommand {
span,
},
];
Some(Value::Record { cols, vals, span })
Some(Value::test_record(Record { cols, vals }))
};
let example_result_2 = || {
@ -106,7 +105,7 @@ impl Command for SubCommand {
span,
},
];
Some(Value::Record { cols, vals, span })
Some(Value::test_record(Record { cols, vals }))
};
vec![
@ -134,37 +133,20 @@ impl Command for SubCommand {
}
}
fn parse_date_into_table(date: Result<DateTime<FixedOffset>, Value>, head: Span) -> Value {
let cols = vec![
"year".into(),
"month".into(),
"day".into(),
"hour".into(),
"minute".into(),
"second".into(),
"nanosecond".into(),
"timezone".into(),
];
match date {
Ok(x) => {
let vals = vec![
Value::int(x.year() as i64, head),
Value::int(x.month() as i64, head),
Value::int(x.day() as i64, head),
Value::int(x.hour() as i64, head),
Value::int(x.minute() as i64, head),
Value::int(x.second() as i64, head),
Value::int(x.nanosecond() as i64, head),
Value::string(x.offset().to_string(), head),
];
Value::Record {
cols,
vals,
span: head,
}
}
Err(e) => e,
}
fn parse_date_into_table(date: DateTime<FixedOffset>, head: Span) -> Value {
Value::record(
record! {
"year" => Value::int(date.year() as i64, head),
"month" => Value::int(date.month() as i64, head),
"day" => Value::int(date.day() as i64, head),
"hour" => Value::int(date.hour() as i64, head),
"minute" => Value::int(date.minute() as i64, head),
"second" => Value::int(date.second() as i64, head),
"nanosecond" => Value::int(date.nanosecond() as i64, head),
"timezone" => Value::string(date.offset().to_string(), head),
},
head,
)
}
fn helper(val: Value, head: Span) -> Value {
@ -172,16 +154,16 @@ fn helper(val: Value, head: Span) -> Value {
Value::String {
val,
span: val_span,
} => {
let date = parse_date_from_string(&val, val_span);
parse_date_into_table(date, head)
}
} => match parse_date_from_string(&val, val_span) {
Ok(date) => parse_date_into_table(date, head),
Err(e) => e,
},
Value::Nothing { span: _ } => {
let now = Local::now();
let n = now.with_timezone(now.offset());
parse_date_into_table(Ok(n), head)
parse_date_into_table(n, head)
}
Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head),
Value::Date { val, span: _ } => parse_date_into_table(val, head),
_ => Value::Error {
error: Box::new(DatetimeParseError(val.debug_value(), head)),
},

View File

@ -3,10 +3,9 @@ use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, PipelineData, ShellError::DatetimeParseError, ShellError::PipelineEmpty,
Signature, Span, Value,
record, Category, Example, PipelineData, Record, ShellError, ShellError::DatetimeParseError,
ShellError::PipelineEmpty, Signature, Span, Type, Value,
};
use nu_protocol::{ShellError, Type};
#[derive(Clone)]
pub struct SubCommand;
@ -76,7 +75,7 @@ impl Command for SubCommand {
},
];
Some(Value::List {
vals: vec![Value::Record { cols, vals, span }],
vals: vec![Value::test_record(Record { cols, vals })],
span,
})
};
@ -107,7 +106,7 @@ impl Command for SubCommand {
},
];
Some(Value::List {
vals: vec![Value::Record { cols, vals, span }],
vals: vec![Value::test_record(Record { cols, vals })],
span,
})
};
@ -137,40 +136,19 @@ impl Command for SubCommand {
}
}
fn parse_date_into_table(date: Result<DateTime<FixedOffset>, Value>, head: Span) -> Value {
let cols = vec![
"year".into(),
"month".into(),
"day".into(),
"hour".into(),
"minute".into(),
"second".into(),
"nanosecond".into(),
"timezone".into(),
];
match date {
Ok(x) => {
let vals = vec![
Value::int(x.year() as i64, head),
Value::int(x.month() as i64, head),
Value::int(x.day() as i64, head),
Value::int(x.hour() as i64, head),
Value::int(x.minute() as i64, head),
Value::int(x.second() as i64, head),
Value::int(x.nanosecond() as i64, head),
Value::string(x.offset().to_string(), head),
];
Value::List {
vals: vec![Value::Record {
cols,
vals,
span: head,
}],
span: head,
}
}
Err(e) => e,
}
fn parse_date_into_table(date: DateTime<FixedOffset>, head: Span) -> Value {
let record = record! {
"year" => Value::int(date.year() as i64, head),
"month" => Value::int(date.month() as i64, head),
"day" => Value::int(date.day() as i64, head),
"hour" => Value::int(date.hour() as i64, head),
"minute" => Value::int(date.minute() as i64, head),
"second" => Value::int(date.second() as i64, head),
"nanosecond" => Value::int(date.nanosecond() as i64, head),
"timezone" => Value::string(date.offset().to_string(), head),
};
Value::list(vec![Value::record(record, head)], head)
}
fn helper(val: Value, head: Span) -> Value {
@ -178,16 +156,16 @@ fn helper(val: Value, head: Span) -> Value {
Value::String {
val,
span: val_span,
} => {
let date = parse_date_from_string(&val, val_span);
parse_date_into_table(date, head)
}
} => match parse_date_from_string(&val, val_span) {
Ok(date) => parse_date_into_table(date, head),
Err(e) => e,
},
Value::Nothing { span: _ } => {
let now = Local::now();
let n = now.with_timezone(now.offset());
parse_date_into_table(Ok(n), head)
parse_date_into_table(n, head)
}
Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head),
Value::Date { val, span: _ } => parse_date_into_table(val, head),
_ => Value::Error {
error: Box::new(DatetimeParseError(val.debug_value(), head)),
},

View File

@ -1,5 +1,5 @@
use chrono::{DateTime, FixedOffset, Local, LocalResult, TimeZone};
use nu_protocol::{ShellError, Span, Value};
use nu_protocol::{record, ShellError, Span, Value};
pub(crate) fn parse_date_from_string(
input: &str,
@ -31,11 +31,6 @@ pub(crate) fn parse_date_from_string(
/// * `head` - use the call's head
/// * `show_parse_only_formats` - whether parse-only format specifiers (that can't be outputted) should be shown. Should only be used for `into datetime`, not `format date`
pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool) -> Value {
let column_names = vec![
"Specification".into(),
"Example".into(),
"Description".into(),
];
let now = Local::now();
struct FormatSpecification<'a> {
@ -258,14 +253,15 @@ pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool)
let mut records = specifications
.iter()
.map(|s| Value::Record {
cols: column_names.clone(),
vals: vec![
Value::string(s.spec, head),
Value::string(now.format(s.spec).to_string(), head),
Value::string(s.description, head),
],
span: head,
.map(|s| {
Value::record(
record! {
"Specification" => Value::string(s.spec, head),
"Example" => Value::string(now.format(s.spec).to_string(), head),
"Description" => Value::string(s.description, head),
},
head,
)
})
.collect::<Vec<Value>>();
@ -279,21 +275,16 @@ pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool)
.unwrap_or("")
.to_string();
records.push(Value::Record {
cols: column_names,
vals: vec![
Value::string("%#z", head),
Value::String {
val: example,
span: head,
},
Value::string(
"Parsing only: Same as %z but allows minutes to be missing or present.",
head,
),
],
span: head,
});
let description = "Parsing only: Same as %z but allows minutes to be missing or present.";
records.push(Value::record(
record! {
"Specification" => Value::string("%#z", head),
"Example" => Value::string(example, head),
"Description" => Value::string(description, head),
},
head,
));
}
Value::List {