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

@ -1,4 +1,4 @@
use crate::{ShellError, Span, Value};
use crate::{record, Record, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -323,7 +323,8 @@ impl Value {
// $env.config.ls.use_ls_colors = 2 results in an error, so
// the current use_ls_colors config setting is converted to a Value::Boolean and inserted in the
// record in place of the 2.
if let Value::Record { cols, vals, span } = self {
if let Value::Record { val, span } = self {
let Record { cols, vals } = val;
let span = *span;
// Because this whole algorithm removes while iterating, this must iterate in reverse.
for index in (0..cols.len()).rev() {
@ -332,7 +333,8 @@ impl Value {
match key {
// Grouped options
"ls" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -363,17 +365,17 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["use_ls_colors".into(), "clickable_links".into()],
vec![
Value::bool(config.use_ls_colors, span),
Value::bool(config.show_clickable_links_in_ls, span),
],
record! {
"use_ls_colors" => Value::bool(config.use_ls_colors, span),
"clickable_links" => Value::bool(config.show_clickable_links_in_ls, span),
},
span,
);
}
}
"cd" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
for index in (0..cols.len()).rev() {
let value = &vals[index];
let key2 = cols[index].as_str();
@ -396,17 +398,17 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["use_ls_colors".into(), "clickable_links".into()],
vec![
Value::bool(config.use_ls_colors, span),
Value::bool(config.show_clickable_links_in_ls, span),
],
record! {
"use_ls_colors" => Value::bool(config.use_ls_colors, span),
"clickable_links" => Value::bool(config.show_clickable_links_in_ls, span),
},
span,
);
}
}
"rm" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
for index in (0..cols.len()).rev() {
let value = &vals[index];
let key2 = cols[index].as_str();
@ -429,8 +431,9 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["always_trash".into()],
vec![Value::bool(config.rm_always_trash, span)],
record! {
"always_trash" => Value::bool(config.rm_always_trash, span),
},
span,
);
}
@ -447,7 +450,8 @@ impl Value {
)
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -504,18 +508,12 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec![
"sync_on_enter".into(),
"max_size".into(),
"file_format".into(),
"isolation".into(),
],
vec![
Value::bool(config.sync_history_on_enter, span),
Value::int(config.max_history_size, span),
reconstruct_history_file_format!(span),
Value::bool(config.history_isolation, span),
],
record! {
"sync_on_enter" => Value::bool(config.sync_history_on_enter, span),
"max_size" => Value::int(config.max_history_size, span),
"file_format" => reconstruct_history_file_format!(span),
"isolation" => Value::bool(config.history_isolation, span),
},
span,
);
}
@ -536,17 +534,17 @@ impl Value {
macro_rules! reconstruct_external {
($span: expr) => {
Value::record(
vec!["max_results".into(), "completer".into(), "enable".into()],
vec![
Value::int(config.max_external_completion_results, $span),
reconstruct_external_completer!($span),
Value::bool(config.enable_external_completion, $span),
],
record! {
"max_results" => Value::int(config.max_external_completion_results, $span),
"completer" => reconstruct_external_completer!($span),
"enable" => Value::bool(config.enable_external_completion, $span),
},
$span,
)
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -596,8 +594,8 @@ impl Value {
)
}
"external" => {
if let Value::Record { cols, vals, span } = &mut vals[index]
{
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -672,20 +670,13 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct record
vals[index] = Value::record(
vec![
"quick".into(),
"partial".into(),
"algorithm".into(),
"case_sensitive".into(),
"external".into(),
],
vec![
Value::bool(config.quick_completions, span),
Value::bool(config.partial_completions, span),
Value::string(config.completion_algorithm.clone(), span),
Value::bool(config.case_sensitive_completions, span),
reconstruct_external!(span),
],
record! {
"quick" => Value::bool(config.quick_completions, span),
"partial" => Value::bool(config.partial_completions, span),
"algorithm" => Value::string(config.completion_algorithm.clone(), span),
"case_sensitive" => Value::bool(config.case_sensitive_completions, span),
"external" => reconstruct_external!(span),
},
span,
);
}
@ -706,7 +697,8 @@ impl Value {
)
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -870,12 +862,11 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["vi_insert".into(), "vi_normal".into(), "emacs".into()],
vec![
reconstruct_cursor_shape!(config.cursor_shape_vi_insert, span),
reconstruct_cursor_shape!(config.cursor_shape_vi_normal, span),
reconstruct_cursor_shape!(config.cursor_shape_emacs, span),
],
record! {
"vi_insert" => reconstruct_cursor_shape!(config.cursor_shape_vi_insert, span),
"vi_normal" => reconstruct_cursor_shape!(config.cursor_shape_vi_normal, span),
"emacs" => reconstruct_cursor_shape!(config.cursor_shape_emacs, span),
},
span,
);
}
@ -897,34 +888,30 @@ impl Value {
($span:expr) => {
match &config.trim_strategy {
TrimStrategy::Wrap { try_to_keep_words } => Value::record(
vec![
"methodology".into(),
"wrapping_try_keep_words".into(),
],
vec![
Value::string("wrapping", $span),
Value::bool(*try_to_keep_words, $span),
],
record! {
"methodology" => Value::string("wrapping", $span),
"wrapping_try_keep_words" => Value::bool(*try_to_keep_words, $span),
},
$span,
),
TrimStrategy::Truncate { suffix } => Value::record(
vec!["methodology".into(), "truncating_suffix".into()],
match suffix {
Some(s) => vec![
Value::string("truncating", $span),
Value::string(s.clone(), $span),
],
None => vec![
Value::string("truncating", $span),
Value::Nothing { span: $span },
],
Some(s) => record! {
"methodology" => Value::string("truncating", $span),
"truncating_suffix" => Value::string(s.clone(), $span),
},
None => record! {
"methodology" => Value::string("truncating", $span),
"truncating_suffix" => Value::Nothing { span: $span },
},
},
$span,
),
}
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -951,7 +938,8 @@ impl Value {
config.table_indent.left = *val as usize;
config.table_indent.right = *val as usize;
}
Value::Record { vals, cols, .. } => {
Value::Record { val, .. } => {
let Record { cols, vals } = val;
let left = cols.iter().position(|e| e == "left");
let right = cols.iter().position(|e| e == "right");
@ -1044,24 +1032,19 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec![
"mode".into(),
"index_mode".into(),
"trim".into(),
"show_empty".into(),
],
vec![
Value::string(config.table_mode.clone(), span),
reconstruct_index_mode!(span),
reconstruct_trim_strategy!(span),
Value::bool(config.table_show_empty, span),
],
record! {
"mode" => Value::string(config.table_mode.clone(), span),
"index_mode" => reconstruct_index_mode!(span),
"trim" => reconstruct_trim_strategy!(span),
"show_empty" => Value::bool(config.table_show_empty, span),
},
span,
)
}
}
"filesize" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
@ -1095,11 +1078,10 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["metric".into(), "format".into()],
vec![
Value::bool(config.filesize_metric, span),
Value::string(config.filesize_format.clone(), span),
],
record! {
"metric" => Value::bool(config.filesize_metric, span),
"format" => Value::string(config.filesize_format.clone(), span),
},
span,
);
}
@ -1110,7 +1092,14 @@ impl Value {
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record_from_hashmap(&config.explore, span);
vals[index] = Value::record(
config
.explore
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
span,
);
}
}
// Misc. options
@ -1120,7 +1109,14 @@ impl Value {
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record_from_hashmap(&config.color_config, span);
vals[index] = Value::record(
config
.color_config
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
span,
);
}
}
"use_grid_icons" => {
@ -1206,25 +1202,17 @@ impl Value {
menu_type, // WARNING: this is not the same name as what is used in Config.nu! ("type")
source,
}| {
Value::Record {
cols: vec![
"name".into(),
"only_buffer_difference".into(),
"marker".into(),
"style".into(),
"type".into(),
"source".into(),
],
vals: vec![
name.clone(),
only_buffer_difference.clone(),
marker.clone(),
style.clone(),
menu_type.clone(),
source.clone(),
],
Value::record(
record! {
"name" => name.clone(),
"only_buffer_difference" => only_buffer_difference.clone(),
"marker" => marker.clone(),
"style" => style.clone(),
"type" => menu_type.clone(),
"source" => source.clone(),
},
span,
}
)
},
)
.collect(),
@ -1250,21 +1238,15 @@ impl Value {
mode,
event,
}| {
Value::Record {
cols: vec![
"modifier".into(),
"keycode".into(),
"mode".into(),
"event".into(),
],
vals: vec![
modifier.clone(),
keycode.clone(),
mode.clone(),
event.clone(),
],
Value::record(
record! {
"modifier" => modifier.clone(),
"keycode" => keycode.clone(),
"mode" => mode.clone(),
"event" => event.clone(),
},
span,
}
)
},
)
.collect(),
@ -1279,33 +1261,26 @@ impl Value {
invalid!(Some(span), "should be a valid hooks list");
errors.push(e);
// Reconstruct
let mut hook_cols = vec![];
let mut hook_vals = vec![];
let mut hook = Record::new();
if let Some(ref value) = config.hooks.pre_prompt {
hook_cols.push("pre_prompt".into());
hook_vals.push(value.clone());
hook.push("pre_prompt", value.clone());
}
if let Some(ref value) = config.hooks.pre_execution {
hook_cols.push("pre_execution".into());
hook_vals.push(value.clone());
hook.push("pre_execution", value.clone());
}
if let Some(ref value) = config.hooks.env_change {
hook_cols.push("env_change".into());
hook_vals.push(value.clone());
hook.push("env_change", value.clone());
}
if let Some(ref value) = config.hooks.display_output {
hook_cols.push("display_output".into());
hook_vals.push(value.clone());
hook.push("display_output", value.clone());
}
vals.push(Value::Record {
cols: hook_cols,
vals: hook_vals,
span,
});
vals.push(Value::record(hook, span));
}
},
"datetime_format" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
if let Value::Record { val, span } = &mut vals[index] {
let Record { cols, vals } = val;
let span = *span;
for index in (0..cols.len()).rev() {
let value = &vals[index];
let key2 = cols[index].as_str();
@ -1314,14 +1289,14 @@ impl Value {
if let Ok(v) = value.as_string() {
config.datetime_normal_format = Some(v);
} else {
invalid!(Some(*span), "should be a string");
invalid!(Some(span), "should be a string");
}
}
"table" => {
if let Ok(v) = value.as_string() {
config.datetime_table_format = Some(v);
} else {
invalid!(Some(*span), "should be a string");
invalid!(Some(span), "should be a string");
}
}
x => {
@ -1339,11 +1314,10 @@ impl Value {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["metric".into(), "format".into()],
vec![
Value::bool(config.filesize_metric, span),
Value::string(config.filesize_format.clone(), span),
],
record! {
"metric" => Value::bool(config.filesize_metric, span),
"format" => Value::string(config.filesize_format.clone(), span),
},
span,
);
}
@ -1481,29 +1455,26 @@ fn try_parse_trim_methodology(value: &Value) -> Option<TrimStrategy> {
}
fn create_map(value: &Value) -> Result<HashMap<String, Value>, ShellError> {
let (cols, inner_vals) = value.as_record()?;
let mut hm: HashMap<String, Value> = HashMap::new();
for (k, v) in cols.iter().zip(inner_vals) {
hm.insert(k.to_string(), v.clone());
}
Ok(hm)
Ok(value
.as_record()?
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect())
}
// Parse the hooks to find the blocks to run when the hooks fire
fn create_hooks(value: &Value) -> Result<Hooks, ShellError> {
match value {
Value::Record { cols, vals, span } => {
Value::Record { val, span } => {
let mut hooks = Hooks::new();
for idx in 0..cols.len() {
match cols[idx].as_str() {
"pre_prompt" => hooks.pre_prompt = Some(vals[idx].clone()),
"pre_execution" => hooks.pre_execution = Some(vals[idx].clone()),
"env_change" => hooks.env_change = Some(vals[idx].clone()),
"display_output" => hooks.display_output = Some(vals[idx].clone()),
"command_not_found" => hooks.command_not_found = Some(vals[idx].clone()),
for (col, val) in val {
match col.as_str() {
"pre_prompt" => hooks.pre_prompt = Some(val.clone()),
"pre_execution" => hooks.pre_execution = Some(val.clone()),
"env_change" => hooks.env_change = Some(val.clone()),
"display_output" => hooks.display_output = Some(val.clone()),
"command_not_found" => hooks.command_not_found = Some(val.clone()),
x => {
return Err(ShellError::UnsupportedConfigValue(
"'pre_prompt', 'pre_execution', 'env_change', 'display_output', 'command_not_found'"
@ -1528,13 +1499,13 @@ fn create_hooks(value: &Value) -> Result<Hooks, ShellError> {
// Parses the config object to extract the strings that will compose a keybinding for reedline
fn create_keybindings(value: &Value) -> Result<Vec<ParsedKeybinding>, ShellError> {
match value {
Value::Record { cols, vals, span } => {
Value::Record { val, span } => {
let span = *span;
// Finding the modifier value in the record
let modifier = extract_value("modifier", cols, vals, span)?.clone();
let keycode = extract_value("keycode", cols, vals, span)?.clone();
let mode = extract_value("mode", cols, vals, span)?.clone();
let event = extract_value("event", cols, vals, span)?.clone();
let modifier = extract_value("modifier", val, span)?.clone();
let keycode = extract_value("keycode", val, span)?.clone();
let mode = extract_value("mode", val, span)?.clone();
let event = extract_value("event", val, span)?.clone();
let keybinding = ParsedKeybinding {
modifier,
@ -1566,18 +1537,18 @@ fn create_keybindings(value: &Value) -> Result<Vec<ParsedKeybinding>, ShellError
// Parses the config object to extract the strings that will compose a keybinding for reedline
pub fn create_menus(value: &Value) -> Result<Vec<ParsedMenu>, ShellError> {
match value {
Value::Record { cols, vals, span } => {
Value::Record { val, span } => {
let span = *span;
// Finding the modifier value in the record
let name = extract_value("name", cols, vals, span)?.clone();
let marker = extract_value("marker", cols, vals, span)?.clone();
let name = extract_value("name", val, span)?.clone();
let marker = extract_value("marker", val, span)?.clone();
let only_buffer_difference =
extract_value("only_buffer_difference", cols, vals, span)?.clone();
let style = extract_value("style", cols, vals, span)?.clone();
let menu_type = extract_value("type", cols, vals, span)?.clone();
extract_value("only_buffer_difference", val, span)?.clone();
let style = extract_value("style", val, span)?.clone();
let menu_type = extract_value("type", val, span)?.clone();
// Source is an optional value
let source = match extract_value("source", cols, vals, span) {
let source = match extract_value("source", val, span) {
Ok(source) => source.clone(),
Err(_) => Value::Nothing { span },
};
@ -1609,12 +1580,11 @@ pub fn create_menus(value: &Value) -> Result<Vec<ParsedMenu>, ShellError> {
pub fn extract_value<'record>(
name: &str,
cols: &'record [String],
vals: &'record [Value],
record: &'record Record,
span: Span,
) -> Result<&'record Value, ShellError> {
cols.iter()
.position(|col| col.as_str() == name)
.and_then(|index| vals.get(index))
record
.iter()
.find_map(|(col, val)| if col == name { Some(val) } else { None })
.ok_or_else(|| ShellError::MissingConfigValue(name.to_string(), span))
}

View File

@ -21,12 +21,12 @@ impl Matcher for Pattern {
Pattern::IgnoreRest => false, // `..` and `..$foo` only match in specific contexts
Pattern::Rest(_) => false, // so we return false here and handle them elsewhere
Pattern::Record(field_patterns) => match value {
Value::Record { cols, vals, .. } => {
Value::Record { val, .. } => {
'top: for field_pattern in field_patterns {
for (col_idx, col) in cols.iter().enumerate() {
for (col, val) in val {
if col == &field_pattern.0 {
// We have found the field
let result = field_pattern.1.match_value(&vals[col_idx], matches);
let result = field_pattern.1.match_value(val, matches);
if !result {
return false;
} else {

View File

@ -136,16 +136,14 @@ impl Module {
}
}
let mut const_cols = vec![];
let mut const_vals = vec![];
for (name, val) in const_rows {
const_cols.push(String::from_utf8_lossy(&name).to_string());
const_vals.push(val);
}
let span = self.span.unwrap_or(backup_span);
let const_record = Value::record(const_cols, const_vals, span);
let const_record = Value::record(
const_rows
.into_iter()
.map(|(name, val)| (String::from_utf8_lossy(&name).to_string(), val))
.collect(),
span,
);
return (
ResolvedImportPattern::new(

View File

@ -4,8 +4,7 @@ use std::str::FromStr;
use crate::ast::{CellPath, MatchPattern, PathMember};
use crate::engine::{Block, Closure};
use crate::ShellError;
use crate::{Range, Spanned, Value};
use crate::{Range, Record, ShellError, Spanned, Value};
use chrono::{DateTime, FixedOffset};
pub trait FromValue: Sized {
@ -490,11 +489,10 @@ impl FromValue for Vec<Value> {
}
}
// A record
impl FromValue for (Vec<String>, Vec<Value>) {
impl FromValue for Record {
fn from_value(v: &Value) -> Result<Self, ShellError> {
match v {
Value::Record { cols, vals, .. } => Ok((cols.clone(), vals.clone())),
Value::Record { val, .. } => Ok(val.clone()),
v => Err(ShellError::CantConvert {
to_type: "Record".into(),
from_type: v.get_type().to_string(),

View File

@ -1,4 +1,4 @@
use crate::{ShellError, Span, Value};
use crate::{Record, ShellError, Span, Value};
use std::fmt;
// Trait definition for a lazy record (where columns are evaluated on-demand)
@ -15,20 +15,14 @@ pub trait LazyRecord<'a>: fmt::Debug + Send + Sync {
// Convert the lazy record into a regular Value::Record by collecting all its columns
fn collect(&'a self) -> Result<Value, ShellError> {
let mut cols = vec![];
let mut vals = vec![];
for column in self.column_names() {
cols.push(column.into());
let val = self.get_column_value(column)?;
vals.push(val);
}
Ok(Value::Record {
cols,
vals,
span: self.span(),
})
self.column_names()
.into_iter()
.map(|col| {
let val = self.get_column_value(col)?;
Ok((col.to_owned(), val))
})
.collect::<Result<Record, _>>()
.map(|record| Value::record(record, self.span()))
}
fn clone_value(&self, span: Span) -> Value;

View File

@ -3,6 +3,7 @@ mod from;
mod from_value;
mod lazy_record;
mod range;
mod record;
mod stream;
mod unit;
@ -18,19 +19,18 @@ use chrono_humanize::HumanTime;
pub use custom_value::CustomValue;
use fancy_regex::Regex;
pub use from_value::FromValue;
use indexmap::map::IndexMap;
pub use lazy_record::LazyRecord;
use nu_utils::get_system_locale;
use nu_utils::locale::get_system_locale_string;
use num_format::ToFormattedString;
pub use range::*;
pub use record::Record;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Write;
use std::{
borrow::Cow,
fmt::{Display, Formatter, Result as FmtResult},
iter,
path::PathBuf,
{cmp::Ordering, fmt::Debug},
};
@ -75,8 +75,7 @@ pub enum Value {
span: Span,
},
Record {
cols: Vec<String>,
vals: Vec<Value>,
val: Record,
span: Span,
},
List {
@ -148,9 +147,8 @@ impl Clone for Value {
val: val.clone(),
span: *span,
},
Value::Record { cols, vals, span } => Value::Record {
cols: cols.clone(),
vals: vals.clone(),
Value::Record { val, span } => Value::Record {
val: val.clone(),
span: *span,
},
Value::LazyRecord { val, span } => val.clone_value(*span),
@ -367,9 +365,9 @@ impl Value {
}
}
pub fn as_record(&self) -> Result<(&[String], &[Value]), ShellError> {
pub fn as_record(&self) -> Result<&Record, ShellError> {
match self {
Value::Record { cols, vals, .. } => Ok((cols, vals)),
Value::Record { val, .. } => Ok(val),
x => Err(ShellError::CantConvert {
to_type: "record".into(),
from_type: x.get_type().to_string(),
@ -547,12 +545,9 @@ impl Value {
Value::Date { .. } => Type::Date,
Value::Range { .. } => Type::Range,
Value::String { .. } => Type::String,
Value::Record { cols, vals, .. } => Type::Record(
cols.iter()
.zip(vals.iter())
.map(|(x, y)| (x.clone(), y.get_type()))
.collect(),
),
Value::Record { val, .. } => {
Type::Record(val.iter().map(|(x, y)| (x.clone(), y.get_type())).collect())
}
Value::List { vals, .. } => {
let mut ty = None;
for val in vals {
@ -595,9 +590,8 @@ impl Value {
pub fn get_data_by_key(&self, name: &str) -> Option<Value> {
match self {
Value::Record { cols, vals, .. } => cols
Value::Record { val, .. } => val
.iter()
.zip(vals.iter())
.find(|(col, _)| col == &name)
.map(|(_, val)| val.clone()),
Value::List { vals, span } => {
@ -676,10 +670,9 @@ impl Value {
.collect::<Vec<_>>()
.join(separator)
),
Value::Record { cols, vals, .. } => format!(
Value::Record { val, .. } => format!(
"{{{}}}",
cols.iter()
.zip(vals.iter())
val.iter()
.map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config)))
.collect::<Vec<_>>()
.join(separator)
@ -739,10 +732,10 @@ impl Value {
)
}
}
Value::Record { cols, .. } => format!(
Value::Record { val, .. } => format!(
"{{record {} field{}}}",
cols.len(),
if cols.len() == 1 { "" } else { "s" }
val.len(),
if val.len() == 1 { "" } else { "s" }
),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => val.into_abbreviated_string(config),
@ -800,10 +793,9 @@ impl Value {
.collect::<Vec<_>>()
.join(separator)
),
Value::Record { cols, vals, .. } => format!(
Value::Record { val, .. } => format!(
"{{{}}}",
cols.iter()
.zip(vals.iter())
val.iter()
.map(|(x, y)| format!("{}: {}", x, y.into_string_parsable(", ", config)))
.collect::<Vec<_>>()
.join(separator)
@ -838,10 +830,9 @@ impl Value {
.collect::<Vec<_>>()
.join(separator)
),
Value::Record { cols, vals, .. } => format!(
Value::Record { val, .. } => format!(
"{{{}}}",
cols.iter()
.zip(vals.iter())
val.iter()
.map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config)))
.collect::<Vec<_>>()
.join(separator)
@ -866,7 +857,7 @@ impl Value {
match self {
Value::String { val, .. } => val.is_empty(),
Value::List { vals, .. } => vals.is_empty(),
Value::Record { cols, .. } => cols.is_empty(),
Value::Record { val, .. } => val.is_empty(),
Value::Binary { val, .. } => val.is_empty(),
Value::Nothing { .. } => true,
_ => false,
@ -981,12 +972,11 @@ impl Value {
span: origin_span,
optional,
} => match &mut current {
Value::Record { cols, vals, span } => {
let cols = cols.clone();
Value::Record { val, span } => {
let span = *span;
// Make reverse iterate to avoid duplicate column leads to first value, actually last value is expected.
if let Some(found) = cols.iter().zip(vals.iter()).rev().find(|x| {
if let Some(found) = val.iter().rev().find(|x| {
if insensitive {
x.0.to_lowercase() == column_name.to_lowercase()
} else {
@ -998,7 +988,7 @@ impl Value {
return Ok(Value::nothing(*origin_span)); // short-circuit
} else {
if from_user_input {
if let Some(suggestion) = did_you_mean(&cols, column_name) {
if let Some(suggestion) = did_you_mean(&val.cols, column_name) {
return Err(ShellError::DidYouMean(suggestion, *origin_span));
}
}
@ -1126,12 +1116,12 @@ impl Value {
Value::List { vals, .. } => {
for val in vals.iter_mut() {
match val {
Value::Record { cols, vals, .. } => {
Value::Record { val: record, .. } => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
found = true;
col.1.upsert_data_at_cell_path(
val.upsert_data_at_cell_path(
&cell_path[1..],
new_val.clone(),
)?
@ -1139,15 +1129,11 @@ impl Value {
}
if !found {
if cell_path.len() == 1 {
cols.push(col_name.clone());
vals.push(new_val);
record.push(col_name, new_val);
break;
} else {
let mut new_col = Value::Record {
cols: vec![],
vals: vec![],
span: new_val.span()?,
};
let mut new_col =
Value::record(Record::new(), new_val.span()?);
new_col.upsert_data_at_cell_path(
&cell_path[1..],
new_val,
@ -1168,30 +1154,25 @@ impl Value {
}
}
}
Value::Record { cols, vals, .. } => {
Value::Record { val: record, .. } => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
found = true;
col.1
.upsert_data_at_cell_path(&cell_path[1..], new_val.clone())?
val.upsert_data_at_cell_path(&cell_path[1..], new_val.clone())?
}
}
if !found {
cols.push(col_name.clone());
if cell_path.len() == 1 {
vals.push(new_val);
let new_col = if cell_path.len() == 1 {
new_val
} else {
let mut new_col = Value::Record {
cols: vec![],
vals: vec![],
span: new_val.span()?,
};
let mut new_col = Value::record(Record::new(), new_val.span()?);
new_col.upsert_data_at_cell_path(&cell_path[1..], new_val)?;
vals.push(new_col);
}
new_col
};
record.push(col_name, new_col);
}
}
Value::LazyRecord { val, .. } => {
@ -1275,15 +1256,14 @@ impl Value {
for val in vals.iter_mut() {
match val {
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
found = true;
col.1.update_data_at_cell_path(
val.update_data_at_cell_path(
&cell_path[1..],
new_val.clone(),
)?
@ -1309,18 +1289,15 @@ impl Value {
}
}
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
found = true;
col.1
.update_data_at_cell_path(&cell_path[1..], new_val.clone())?
val.update_data_at_cell_path(&cell_path[1..], new_val.clone())?
}
}
if !found {
@ -1392,16 +1369,15 @@ impl Value {
for val in vals.iter_mut() {
match val {
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
let mut found = false;
let mut index = 0;
cols.retain_mut(|col| {
record.cols.retain_mut(|col| {
if col == col_name {
found = true;
vals.remove(index);
record.vals.remove(index);
false
} else {
index += 1;
@ -1428,18 +1404,21 @@ impl Value {
Ok(())
}
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
let mut found = false;
for (i, col) in cols.clone().iter().enumerate() {
let mut index = 0;
record.cols.retain_mut(|col| {
if col == col_name {
cols.remove(i);
vals.remove(i);
found = true;
record.vals.remove(index);
false
} else {
index += 1;
true
}
}
});
if !found && !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.to_string(),
@ -1501,15 +1480,14 @@ impl Value {
for val in vals.iter_mut() {
match val {
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
found = true;
col.1.remove_data_at_cell_path(&cell_path[1..])?
val.remove_data_at_cell_path(&cell_path[1..])?
}
}
if !found && !optional {
@ -1532,17 +1510,15 @@ impl Value {
Ok(())
}
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
found = true;
col.1.remove_data_at_cell_path(&cell_path[1..])?
val.remove_data_at_cell_path(&cell_path[1..])?
}
}
if !found && !optional {
@ -1613,12 +1589,11 @@ impl Value {
for val in vals.iter_mut() {
match val {
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
if cell_path.len() == 1 {
return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.to_string(),
@ -1626,7 +1601,7 @@ impl Value {
src_span: *v_span,
});
} else {
return col.1.insert_data_at_cell_path(
return val.insert_data_at_cell_path(
&cell_path[1..],
new_val,
head_span,
@ -1635,8 +1610,7 @@ impl Value {
}
}
cols.push(col_name.clone());
vals.push(new_val.clone());
record.push(col_name, new_val.clone());
}
// SIGH...
Value::Error { error } => return Err(*error.clone()),
@ -1652,12 +1626,11 @@ impl Value {
}
}
Value::Record {
cols,
vals,
val: record,
span: v_span,
} => {
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
for (col, val) in record.iter_mut() {
if col == col_name {
if cell_path.len() == 1 {
return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.to_string(),
@ -1665,7 +1638,7 @@ impl Value {
src_span: *v_span,
});
} else {
return col.1.insert_data_at_cell_path(
return val.insert_data_at_cell_path(
&cell_path[1..],
new_val,
head_span,
@ -1674,8 +1647,7 @@ impl Value {
}
}
cols.push(col_name.clone());
vals.push(new_val);
record.push(col_name, new_val);
}
Value::LazyRecord { val, span } => {
// convert to Record first.
@ -1734,7 +1706,7 @@ impl Value {
pub fn columns(&self) -> &[String] {
match self {
Value::Record { cols, .. } => cols,
Value::Record { val, .. } => &val.cols,
_ => &[],
}
}
@ -1777,18 +1749,8 @@ impl Value {
}
}
pub fn record(cols: Vec<String>, vals: Vec<Value>, span: Span) -> Value {
Value::Record { cols, vals, span }
}
pub fn record_from_hashmap(map: &HashMap<String, Value>, span: Span) -> Value {
let mut cols = vec![];
let mut vals = vec![];
for (key, val) in map.iter() {
cols.push(key.clone());
vals.push(val.clone());
}
Value::record(cols, vals, span)
pub fn record(val: Record, span: Span) -> Value {
Value::Record { val, span }
}
pub fn list(vals: Vec<Value>, span: Span) -> Value {
@ -1894,12 +1856,8 @@ impl Value {
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
/// when used in errors.
pub fn test_record(cols: Vec<impl Into<String>>, vals: Vec<Value>) -> Value {
Value::record(
cols.into_iter().map(|s| s.into()).collect(),
vals,
Span::test_data(),
)
pub fn test_record(val: Record) -> Value {
Value::record(val, Span::test_data())
}
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
@ -2148,14 +2106,7 @@ impl PartialOrd for Value {
Value::CustomValue { .. } => Some(Ordering::Less),
Value::MatchPattern { .. } => Some(Ordering::Less),
},
(
Value::Record {
cols: lhs_cols,
vals: lhs_vals,
..
},
rhs,
) => match rhs {
(Value::Record { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
@ -2164,18 +2115,12 @@ impl PartialOrd for Value {
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Record {
cols: rhs_cols,
vals: rhs_vals,
..
} => {
Value::Record { val: rhs, .. } => {
// reorder cols and vals to make more logically compare.
// more general, if two record have same col and values,
// the order of cols shouldn't affect the equal property.
let (lhs_cols_ordered, lhs_vals_ordered) =
reorder_record_inner(lhs_cols, lhs_vals);
let (rhs_cols_ordered, rhs_vals_ordered) =
reorder_record_inner(rhs_cols, rhs_vals);
let (lhs_cols_ordered, lhs_vals_ordered) = reorder_record_inner(lhs);
let (rhs_cols_ordered, rhs_vals_ordered) = reorder_record_inner(rhs);
let result = lhs_cols_ordered.partial_cmp(&rhs_cols_ordered);
if result == Some(Ordering::Equal) {
@ -3190,8 +3135,8 @@ impl Value {
val: rhs.contains(lhs),
span,
}),
(Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool {
val: rhs.contains(lhs),
(Value::String { val: lhs, .. }, Value::Record { val: rhs, .. }) => Ok(Value::Bool {
val: rhs.cols.contains(lhs),
span,
}),
(Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => {
@ -3247,8 +3192,8 @@ impl Value {
val: !rhs.contains(lhs),
span,
}),
(Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool {
val: !rhs.contains(lhs),
(Value::String { val: lhs, .. }, Value::Record { val: rhs, .. }) => Ok(Value::Bool {
val: !rhs.cols.contains(lhs),
span,
}),
(Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => {
@ -3648,36 +3593,10 @@ impl Value {
}
}
fn reorder_record_inner(cols: &[String], vals: &[Value]) -> (Vec<String>, Vec<Value>) {
let mut kv_pairs =
iter::zip(cols.to_owned(), vals.to_owned()).collect::<Vec<(String, Value)>>();
kv_pairs.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.expect("Columns should support compare")
});
let (mut cols, mut vals) = (vec![], vec![]);
for (col, val) in kv_pairs {
cols.push(col);
vals.push(val);
}
(cols, vals)
}
/// Create a Value::Record from a spanned hashmap
impl From<Spanned<HashMap<String, Value>>> for Value {
fn from(input: Spanned<HashMap<String, Value>>) -> Self {
let span = input.span;
let (cols, vals) = input
.item
.into_iter()
.fold((vec![], vec![]), |mut acc, (k, v)| {
acc.0.push(k);
acc.1.push(v);
acc
});
Value::Record { cols, vals, span }
}
fn reorder_record_inner(record: &Record) -> (Vec<&String>, Vec<&Value>) {
let mut kv_pairs = record.iter().collect::<Vec<_>>();
kv_pairs.sort_by_key(|(col, _)| *col);
kv_pairs.into_iter().unzip()
}
fn type_compatible(a: Type, b: Type) -> bool {
@ -3688,23 +3607,6 @@ fn type_compatible(a: Type, b: Type) -> bool {
matches!((a, b), (Type::Int, Type::Float) | (Type::Float, Type::Int))
}
/// Create a Value::Record from a spanned indexmap
impl From<Spanned<IndexMap<String, Value>>> for Value {
fn from(input: Spanned<IndexMap<String, Value>>) -> Self {
let span = input.span;
let (cols, vals) = input
.item
.into_iter()
.fold((vec![], vec![]), |mut acc, (k, v)| {
acc.0.push(k);
acc.1.push(v);
acc
});
Value::Record { cols, vals, span }
}
}
/// Is the given year a leap year?
#[allow(clippy::nonminimal_bool)]
pub fn is_leap_year(year: i32) -> bool {
@ -3985,7 +3887,7 @@ fn get_filesize_format(format_value: &str, filesize_metric: Option<bool>) -> (By
#[cfg(test)]
mod tests {
use super::{Span, Value};
use super::{Record, Span, Value};
mod is_empty {
use super::*;
@ -4013,26 +3915,24 @@ mod tests {
#[test]
fn test_record() {
let no_columns_nor_cell_values = Value::Record {
cols: vec![],
vals: vec![],
span: Span::unknown(),
};
let one_column_and_one_cell_value_with_empty_strings = Value::Record {
let no_columns_nor_cell_values = Value::test_record(Record::new());
let one_column_and_one_cell_value_with_empty_strings = Value::test_record(Record {
cols: vec![String::from("")],
vals: vec![Value::string("", Span::unknown())],
span: Span::unknown(),
};
let one_column_with_a_string_and_one_cell_value_with_empty_string = Value::Record {
cols: vec![String::from("column")],
vals: vec![Value::string("", Span::unknown())],
span: Span::unknown(),
};
let one_column_with_empty_string_and_one_value_with_a_string = Value::Record {
cols: vec![String::from("")],
vals: vec![Value::string("text", Span::unknown())],
span: Span::unknown(),
};
});
let one_column_with_a_string_and_one_cell_value_with_empty_string =
Value::test_record(Record {
cols: vec![String::from("column")],
vals: vec![Value::string("", Span::unknown())],
});
let one_column_with_empty_string_and_one_value_with_a_string =
Value::test_record(Record {
cols: vec![String::from("")],
vals: vec![Value::string("text", Span::unknown())],
});
assert!(no_columns_nor_cell_values.is_empty());
assert!(!one_column_and_one_cell_value_with_empty_strings.is_empty());

View File

@ -0,0 +1,99 @@
use crate::Value;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Record {
pub cols: Vec<String>,
pub vals: Vec<Value>,
}
impl Record {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
cols: Vec::with_capacity(capacity),
vals: Vec::with_capacity(capacity),
}
}
pub fn iter(&self) -> Iter {
self.into_iter()
}
pub fn iter_mut(&mut self) -> IterMut {
self.into_iter()
}
pub fn is_empty(&self) -> bool {
self.cols.is_empty() || self.vals.is_empty()
}
pub fn len(&self) -> usize {
usize::min(self.cols.len(), self.vals.len())
}
pub fn push(&mut self, col: impl Into<String>, val: Value) {
self.cols.push(col.into());
self.vals.push(val);
}
}
impl FromIterator<(String, Value)> for Record {
fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
let (cols, vals) = iter.into_iter().unzip();
Self { cols, vals }
}
}
pub type IntoIter = std::iter::Zip<std::vec::IntoIter<String>, std::vec::IntoIter<Value>>;
impl IntoIterator for Record {
type Item = (String, Value);
type IntoIter = IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.cols.into_iter().zip(self.vals)
}
}
pub type Iter<'a> = std::iter::Zip<std::slice::Iter<'a, String>, std::slice::Iter<'a, Value>>;
impl<'a> IntoIterator for &'a Record {
type Item = (&'a String, &'a Value);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.cols.iter().zip(&self.vals)
}
}
pub type IterMut<'a> = std::iter::Zip<std::slice::Iter<'a, String>, std::slice::IterMut<'a, Value>>;
impl<'a> IntoIterator for &'a mut Record {
type Item = (&'a String, &'a mut Value);
type IntoIter = IterMut<'a>;
fn into_iter(self) -> Self::IntoIter {
self.cols.iter().zip(&mut self.vals)
}
}
#[macro_export]
macro_rules! record {
{$($col:expr => $val:expr),+ $(,)?} => {
$crate::Record {
cols: vec![$($col.into(),)+],
vals: vec![$($val,)+]
}
};
{} => {
$crate::Record::new()
};
}