Move nu-data out of nu-cli (#2369)

* WIP for moving nu-data out

* Refactor nu-data out of nu-cli

* Remove unwraps

* Remove unwraps
This commit is contained in:
Jonathan Turner
2020-08-18 19:00:02 +12:00
committed by GitHub
parent 1d5518a214
commit 738541f727
73 changed files with 715 additions and 514 deletions

479
crates/nu-data/src/base.rs Normal file
View File

@ -0,0 +1,479 @@
pub(crate) mod shape;
use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use derive_new::new;
use nu_errors::ShellError;
use nu_protocol::{
hir, Primitive, ShellTypeName, SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value,
};
use nu_source::{Span, Tag};
use nu_value_ext::ValueExt;
use num_bigint::BigInt;
use num_traits::Zero;
use query_interface::{interfaces, vtable_for, ObjectHash};
use serde::{Deserialize, Serialize};
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new, Serialize)]
pub struct Operation {
pub(crate) left: Value,
pub(crate) operator: hir::Operator,
pub(crate) right: Value,
}
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, new)]
pub struct Block {
pub(crate) commands: hir::Commands,
pub(crate) tag: Tag,
}
interfaces!(Block: dyn ObjectHash);
#[derive(Serialize, Deserialize)]
pub enum Switch {
Present,
Absent,
}
impl std::convert::TryFrom<Option<&Value>> for Switch {
type Error = ShellError;
fn try_from(value: Option<&Value>) -> Result<Switch, ShellError> {
match value {
None => Ok(Switch::Absent),
Some(value) => match &value.value {
UntaggedValue::Primitive(Primitive::Boolean(true)) => Ok(Switch::Present),
_ => Err(ShellError::type_error("Boolean", value.spanned_type_name())),
},
}
}
}
pub fn select_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
let mut out = TaggedDictBuilder::new(tag);
let descs = obj.data_descriptors();
for column_name in fields {
match descs.iter().find(|d| *d == column_name) {
None => out.insert_untagged(column_name, UntaggedValue::nothing()),
Some(desc) => out.insert_value(desc.clone(), obj.get_data(desc).borrow().clone()),
}
}
out.into_value()
}
pub fn reject_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
let mut out = TaggedDictBuilder::new(tag);
let descs = obj.data_descriptors();
for desc in descs {
if fields.iter().any(|field| *field == desc) {
continue;
} else {
out.insert_value(desc.clone(), obj.get_data(&desc).borrow().clone())
}
}
out.into_value()
}
pub enum CompareValues {
Ints(BigInt, BigInt),
Decimals(BigDecimal, BigDecimal),
String(String, String),
Date(DateTime<Utc>, DateTime<Utc>),
DateDuration(DateTime<Utc>, BigInt),
Booleans(bool, bool),
}
impl CompareValues {
pub fn compare(&self) -> std::cmp::Ordering {
match self {
CompareValues::Ints(left, right) => left.cmp(right),
CompareValues::Decimals(left, right) => left.cmp(right),
CompareValues::String(left, right) => left.cmp(right),
CompareValues::Date(left, right) => left.cmp(right),
CompareValues::DateDuration(left, right) => {
// FIXME: Not sure if I could do something better with the Span.
let duration = Primitive::into_chrono_duration(
Primitive::Duration(right.clone()),
Span::unknown(),
)
.expect("Could not convert nushell Duration into chrono Duration.");
let right: DateTime<Utc> = Utc::now()
.checked_sub_signed(duration)
.expect("Data overflow");
right.cmp(left)
}
CompareValues::Booleans(left, right) => left.cmp(right),
}
}
}
pub fn coerce_compare(
left: &UntaggedValue,
right: &UntaggedValue,
) -> Result<CompareValues, (&'static str, &'static str)> {
match (left, right) {
(UntaggedValue::Primitive(left), UntaggedValue::Primitive(right)) => {
coerce_compare_primitive(left, right)
}
_ => Err((left.type_name(), right.type_name())),
}
}
fn coerce_compare_primitive(
left: &Primitive,
right: &Primitive,
) -> Result<CompareValues, (&'static str, &'static str)> {
use Primitive::*;
Ok(match (left, right) {
(Int(left), Int(right)) => CompareValues::Ints(left.clone(), right.clone()),
(Int(left), Decimal(right)) => {
CompareValues::Decimals(BigDecimal::zero() + left, right.clone())
}
(Int(left), Filesize(right)) => CompareValues::Ints(left.clone(), BigInt::from(*right)),
(Decimal(left), Decimal(right)) => CompareValues::Decimals(left.clone(), right.clone()),
(Decimal(left), Int(right)) => {
CompareValues::Decimals(left.clone(), BigDecimal::zero() + right)
}
(Decimal(left), Filesize(right)) => {
CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
}
(Filesize(left), Filesize(right)) => {
CompareValues::Ints(BigInt::from(*left), BigInt::from(*right))
}
(Filesize(left), Int(right)) => CompareValues::Ints(BigInt::from(*left), right.clone()),
(Filesize(left), Decimal(right)) => {
CompareValues::Decimals(BigDecimal::from(*left), right.clone())
}
(Nothing, Nothing) => CompareValues::Booleans(true, true),
(String(left), String(right)) => CompareValues::String(left.clone(), right.clone()),
(Line(left), String(right)) => CompareValues::String(left.clone(), right.clone()),
(String(left), Line(right)) => CompareValues::String(left.clone(), right.clone()),
(Line(left), Line(right)) => CompareValues::String(left.clone(), right.clone()),
(Date(left), Date(right)) => CompareValues::Date(*left, *right),
(Date(left), Duration(right)) => CompareValues::DateDuration(*left, right.clone()),
(Boolean(left), Boolean(right)) => CompareValues::Booleans(*left, *right),
_ => return Err((left.type_name(), right.type_name())),
})
}
#[cfg(test)]
mod tests {
use indexmap::{indexmap, IndexMap};
use nu_errors::ShellError;
use nu_protocol::{ColumnPath as ColumnPathValue, PathMember, UntaggedValue, Value};
use nu_source::*;
use nu_value_ext::{as_column_path, ValueExt};
use num_bigint::BigInt;
fn string(input: impl Into<String>) -> Value {
crate::utils::helpers::string(input)
}
fn int(input: impl Into<BigInt>) -> Value {
crate::utils::helpers::int(input)
}
fn row(entries: IndexMap<String, Value>) -> Value {
crate::utils::helpers::row(entries)
}
fn table(list: &[Value]) -> Value {
crate::utils::helpers::table(list)
}
fn error_callback(
reason: &'static str,
) -> impl FnOnce((&Value, &PathMember, ShellError)) -> ShellError {
move |(_obj_source, _column_path_tried, _err)| ShellError::unimplemented(reason)
}
fn column_path(paths: &[Value]) -> Result<Tagged<ColumnPathValue>, ShellError> {
as_column_path(&table(paths))
}
#[test]
fn gets_matching_field_from_a_row() -> Result<(), ShellError> {
let row = UntaggedValue::row(indexmap! {
"amigos".into() => table(&[string("andres"),string("jonathan"),string("yehuda")])
})
.into_untagged_value();
assert_eq!(
row.get_data_by_key("amigos".spanned_unknown())
.ok_or_else(|| ShellError::unexpected("Failure during testing"))?,
table(&[string("andres"), string("jonathan"), string("yehuda")])
);
Ok(())
}
#[test]
fn gets_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
let field_path = column_path(&[string("package"), string("version")]);
let (version, tag) = string("0.4.0").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() =>
row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0")
})
});
assert_eq!(
*value.into_value(tag).get_data_by_column_path(
&field_path?.item,
Box::new(error_callback("package.version"))
)?,
version
);
Ok(())
}
#[test]
fn gets_first_matching_field_from_rows_with_same_field_inside_a_table() -> Result<(), ShellError>
{
let field_path = column_path(&[string("package"), string("authors"), string("name")]);
let (_, tag) = string("Andrés N. Robalino").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0"),
"authors".into() => table(&[
row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
row(indexmap!{"name".into() => string("Jonathan Turner")}),
row(indexmap!{"name".into() => string("Yehuda Katz")})
])
})
});
assert_eq!(
value.into_value(tag).get_data_by_column_path(
&field_path?.item,
Box::new(error_callback("package.authors.name"))
)?,
table(&[
string("Andrés N. Robalino"),
string("Jonathan Turner"),
string("Yehuda Katz")
])
);
Ok(())
}
#[test]
fn column_path_that_contains_just_a_number_gets_a_row_from_a_table() -> Result<(), ShellError> {
let field_path = column_path(&[string("package"), string("authors"), int(0)]);
let (_, tag) = string("Andrés N. Robalino").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0"),
"authors".into() => table(&[
row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
row(indexmap!{"name".into() => string("Jonathan Turner")}),
row(indexmap!{"name".into() => string("Yehuda Katz")})
])
})
});
assert_eq!(
*value.into_value(tag).get_data_by_column_path(
&field_path?.item,
Box::new(error_callback("package.authors.0"))
)?,
UntaggedValue::row(indexmap! {
"name".into() => string("Andrés N. Robalino")
})
);
Ok(())
}
#[test]
fn column_path_that_contains_just_a_number_gets_a_row_from_a_row() -> Result<(), ShellError> {
let field_path = column_path(&[string("package"), string("authors"), string("0")]);
let (_, tag) = string("Andrés N. Robalino").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0"),
"authors".into() => row(indexmap! {
"0".into() => row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
"1".into() => row(indexmap!{"name".into() => string("Jonathan Turner")}),
"2".into() => row(indexmap!{"name".into() => string("Yehuda Katz")}),
})
})
});
assert_eq!(
*value.into_value(tag).get_data_by_column_path(
&field_path?.item,
Box::new(error_callback("package.authors.\"0\""))
)?,
UntaggedValue::row(indexmap! {
"name".into() => string("Andrés N. Robalino")
})
);
Ok(())
}
#[test]
fn replaces_matching_field_from_a_row() -> Result<(), ShellError> {
let field_path = column_path(&[string("amigos")]);
let sample = UntaggedValue::row(indexmap! {
"amigos".into() => table(&[
string("andres"),
string("jonathan"),
string("yehuda"),
]),
});
let replacement = string("jonas");
let actual = sample
.into_untagged_value()
.replace_data_at_column_path(&field_path?.item, replacement)
.ok_or_else(|| ShellError::untagged_runtime_error("Could not replace column"))?;
assert_eq!(actual, row(indexmap! {"amigos".into() => string("jonas")}));
Ok(())
}
#[test]
fn replaces_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
let field_path = column_path(&[
string("package"),
string("authors"),
string("los.3.caballeros"),
]);
let sample = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"authors".into() => row(indexmap! {
"los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.caballeros".into() => table(&[string("andres::yehuda::jonathan")])
})
})
});
let replacement = table(&[string("yehuda::jonathan::andres")]);
let tag = replacement.tag.clone();
let actual = sample
.into_value(&tag)
.replace_data_at_column_path(&field_path?.item, replacement.clone())
.ok_or_else(|| {
ShellError::labeled_error(
"Could not replace column",
"could not replace column",
&tag,
)
})?;
assert_eq!(
actual,
UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"authors".into() => row(indexmap! {
"los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.caballeros".into() => replacement})})})
.into_value(tag)
);
Ok(())
}
#[test]
fn replaces_matching_field_from_rows_inside_a_table() -> Result<(), ShellError> {
let field_path = column_path(&[
string("shell_policy"),
string("releases"),
string("nu.version.arepa"),
]);
let sample = UntaggedValue::row(indexmap! {
"shell_policy".into() => row(indexmap! {
"releases".into() => table(&[
row(indexmap! {
"nu.version.arepa".into() => row(indexmap! {
"code".into() => string("0.4.0"), "tag_line".into() => string("GitHub-era")
})
}),
row(indexmap! {
"nu.version.taco".into() => row(indexmap! {
"code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
})
}),
row(indexmap! {
"nu.version.stable".into() => row(indexmap! {
"code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
})
})
])
})
});
let replacement = row(indexmap! {
"code".into() => string("0.5.0"),
"tag_line".into() => string("CABALLEROS")
});
let tag = replacement.tag.clone();
let actual = sample
.into_value(tag.clone())
.replace_data_at_column_path(&field_path?.item, replacement.clone())
.ok_or_else(|| {
ShellError::labeled_error(
"Could not replace column",
"could not replace column",
&tag,
)
})?;
assert_eq!(
actual,
UntaggedValue::row(indexmap! {
"shell_policy".into() => row(indexmap! {
"releases".into() => table(&[
row(indexmap! {
"nu.version.arepa".into() => replacement
}),
row(indexmap! {
"nu.version.taco".into() => row(indexmap! {
"code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
})
}),
row(indexmap! {
"nu.version.stable".into() => row(indexmap! {
"code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
})
})
])
})
}).into_value(&tag)
);
Ok(())
}
}

View File

@ -0,0 +1,271 @@
use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use nu_protocol::RangeInclusion;
use nu_protocol::{format_primitive, ColumnPath, Dictionary, Primitive, UntaggedValue, Value};
use nu_source::{b, DebugDocBuilder, PrettyDebug};
use num_bigint::BigInt;
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::path::PathBuf;
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct InlineRange {
from: (InlineShape, RangeInclusion),
to: (InlineShape, RangeInclusion),
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum InlineShape {
Nothing,
Int(BigInt),
Decimal(BigDecimal),
Range(Box<InlineRange>),
Bytesize(u64),
String(String),
Line(String),
ColumnPath(ColumnPath),
Pattern(String),
Boolean(bool),
Date(DateTime<Utc>),
Duration(BigInt),
Path(PathBuf),
Binary(usize),
Row(BTreeMap<Column, InlineShape>),
Table(Vec<InlineShape>),
// TODO: Block arguments
Block,
// TODO: Error type
Error,
// Stream markers (used as bookend markers rather than actual values)
BeginningOfStream,
EndOfStream,
}
pub struct FormatInlineShape {
shape: InlineShape,
column: Option<Column>,
}
impl InlineShape {
pub fn from_primitive(primitive: &Primitive) -> InlineShape {
match primitive {
Primitive::Nothing => InlineShape::Nothing,
Primitive::Int(int) => InlineShape::Int(int.clone()),
Primitive::Range(range) => {
let (left, left_inclusion) = &range.from;
let (right, right_inclusion) = &range.to;
InlineShape::Range(Box::new(InlineRange {
from: (InlineShape::from_primitive(left), *left_inclusion),
to: (InlineShape::from_primitive(right), *right_inclusion),
}))
}
Primitive::Decimal(decimal) => InlineShape::Decimal(decimal.clone()),
Primitive::Filesize(bytesize) => InlineShape::Bytesize(*bytesize),
Primitive::String(string) => InlineShape::String(string.clone()),
Primitive::Line(string) => InlineShape::Line(string.clone()),
Primitive::ColumnPath(path) => InlineShape::ColumnPath(path.clone()),
Primitive::Pattern(pattern) => InlineShape::Pattern(pattern.clone()),
Primitive::Boolean(boolean) => InlineShape::Boolean(*boolean),
Primitive::Date(date) => InlineShape::Date(*date),
Primitive::Duration(duration) => InlineShape::Duration(duration.clone()),
Primitive::Path(path) => InlineShape::Path(path.clone()),
Primitive::Binary(b) => InlineShape::Binary(b.len()),
Primitive::BeginningOfStream => InlineShape::BeginningOfStream,
Primitive::EndOfStream => InlineShape::EndOfStream,
}
}
pub fn from_dictionary(dictionary: &Dictionary) -> InlineShape {
let mut map = BTreeMap::new();
for (key, value) in dictionary.entries.iter() {
let column = Column::String(key.clone());
map.insert(column, InlineShape::from_value(value));
}
InlineShape::Row(map)
}
pub fn from_table<'a>(table: impl IntoIterator<Item = &'a Value>) -> InlineShape {
let mut vec = vec![];
for item in table.into_iter() {
vec.push(InlineShape::from_value(item))
}
InlineShape::Table(vec)
}
pub fn from_value<'a>(value: impl Into<&'a UntaggedValue>) -> InlineShape {
match value.into() {
UntaggedValue::Primitive(p) => InlineShape::from_primitive(p),
UntaggedValue::Row(row) => InlineShape::from_dictionary(row),
UntaggedValue::Table(table) => InlineShape::from_table(table.iter()),
UntaggedValue::Error(_) => InlineShape::Error,
UntaggedValue::Block(_) => InlineShape::Block,
}
}
#[allow(unused)]
pub fn format_for_column(self, column: impl Into<Column>) -> FormatInlineShape {
FormatInlineShape {
shape: self,
column: Some(column.into()),
}
}
pub fn format(self) -> FormatInlineShape {
FormatInlineShape {
shape: self,
column: None,
}
}
}
impl PrettyDebug for FormatInlineShape {
fn pretty(&self) -> DebugDocBuilder {
let column = &self.column;
match &self.shape {
InlineShape::Nothing => b::blank(),
InlineShape::Int(int) => b::primitive(format!("{}", int)),
InlineShape::Decimal(decimal) => {
b::description(format_primitive(&Primitive::Decimal(decimal.clone()), None))
}
InlineShape::Range(range) => {
let (left, left_inclusion) = &range.from;
let (right, right_inclusion) = &range.to;
let op = match (left_inclusion, right_inclusion) {
(RangeInclusion::Inclusive, RangeInclusion::Exclusive) => "..",
_ => unimplemented!("No syntax for ranges that aren't inclusive on the left and exclusive on the right")
};
left.clone().format().pretty() + b::operator(op) + right.clone().format().pretty()
}
InlineShape::Bytesize(bytesize) => {
let byte = byte_unit::Byte::from_bytes(*bytesize as u128);
let byte = byte.get_appropriate_unit(false);
match byte.get_unit() {
byte_unit::ByteUnit::B => {
(b::primitive(format!("{}", byte.get_value())) + b::space() + b::kind("B"))
.group()
}
_ => b::primitive(byte.format(1)),
}
}
InlineShape::String(string) => b::primitive(string),
InlineShape::Line(string) => b::primitive(string),
InlineShape::ColumnPath(path) => {
b::intersperse(path.iter().map(|member| member.pretty()), b::keyword("."))
}
InlineShape::Pattern(pattern) => b::primitive(pattern),
InlineShape::Boolean(boolean) => b::primitive(
match (boolean, column) {
(true, None) => "Yes",
(false, None) => "No",
(true, Some(Column::String(s))) if !s.is_empty() => s,
(false, Some(Column::String(s))) if !s.is_empty() => "",
(true, Some(_)) => "Yes",
(false, Some(_)) => "No",
}
.to_owned(),
),
InlineShape::Date(date) => b::primitive(nu_protocol::format_date(date)),
InlineShape::Duration(duration) => b::description(format_primitive(
&Primitive::Duration(duration.clone()),
None,
)),
InlineShape::Path(path) => b::primitive(path.display()),
InlineShape::Binary(length) => b::opaque(format!("<binary: {} bytes>", length)),
InlineShape::Row(row) => b::delimit(
"[",
b::kind("row")
+ b::space()
+ if row.keys().len() <= 6 {
b::intersperse(
row.keys().map(|key| match key {
Column::String(string) => b::description(string),
Column::Value => b::blank(),
}),
b::space(),
)
} else {
b::description(format!("{} columns", row.keys().len()))
},
"]",
)
.group(),
InlineShape::Table(rows) => b::delimit(
"[",
b::kind("table")
+ b::space()
+ b::primitive(rows.len())
+ b::space()
+ b::description("rows"),
"]",
)
.group(),
InlineShape::Block => b::opaque("block"),
InlineShape::Error => b::error("error"),
InlineShape::BeginningOfStream => b::blank(),
InlineShape::EndOfStream => b::blank(),
}
}
}
pub trait GroupedValue: Debug + Clone {
type Item;
fn new() -> Self;
fn merge(&mut self, value: Self::Item);
}
impl GroupedValue for Vec<(usize, usize)> {
type Item = usize;
fn new() -> Vec<(usize, usize)> {
vec![]
}
fn merge(&mut self, new_value: usize) {
match self.last_mut() {
Some(value) if value.1 == new_value - 1 => {
value.1 += 1;
}
_ => self.push((new_value, new_value)),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Column {
String(String),
Value,
}
impl Into<Column> for String {
fn into(self) -> Column {
Column::String(self)
}
}
impl Into<Column> for &String {
fn into(self) -> Column {
Column::String(self.clone())
}
}
impl Into<Column> for &str {
fn into(self) -> Column {
Column::String(self.to_string())
}
}

View File

@ -0,0 +1,45 @@
use crate::TaggedListBuilder;
use nu_source::Tag;
use nu_protocol::{NamedType, PositionalType, Signature, TaggedDictBuilder, UntaggedValue, Value};
fn for_spec(name: &str, ty: &str, required: bool, tag: impl Into<Tag>) -> Value {
let tag = tag.into();
let mut spec = TaggedDictBuilder::new(tag);
spec.insert_untagged("name", UntaggedValue::string(name));
spec.insert_untagged("type", UntaggedValue::string(ty));
spec.insert_untagged(
"required",
UntaggedValue::string(if required { "yes" } else { "no" }),
);
spec.into_value()
}
pub fn signature_dict(signature: Signature, tag: impl Into<Tag>) -> Value {
let tag = tag.into();
let mut sig = TaggedListBuilder::new(&tag);
for arg in signature.positional.iter() {
let is_required = matches!(arg.0, PositionalType::Mandatory(_, _));
sig.push_value(for_spec(arg.0.name(), "argument", is_required, &tag));
}
if signature.rest_positional.is_some() {
let is_required = false;
sig.push_value(for_spec("rest", "argument", is_required, &tag));
}
for (name, ty) in signature.named.iter() {
match ty.0 {
NamedType::Mandatory(_, _) => sig.push_value(for_spec(name, "flag", true, &tag)),
NamedType::Optional(_, _) => sig.push_value(for_spec(name, "flag", false, &tag)),
NamedType::Switch(_) => sig.push_value(for_spec(name, "switch", false, &tag)),
}
}
sig.into_value()
}

View File

@ -0,0 +1,289 @@
mod conf;
mod nuconfig;
pub mod table;
pub mod tests;
pub use conf::Conf;
pub use nuconfig::NuConfig;
use indexmap::IndexMap;
use log::trace;
use nu_errors::{CoerceInto, ShellError};
use nu_protocol::{
Dictionary, Primitive, ShellTypeName, TaggedDictBuilder, UnspannedPathMember, UntaggedValue,
Value,
};
use nu_source::{SpannedItem, Tag, TaggedItem};
use std::fs::{self, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
pub fn convert_toml_value_to_nu_value(v: &toml::Value, tag: impl Into<Tag>) -> Value {
let tag = tag.into();
match v {
toml::Value::Boolean(b) => UntaggedValue::boolean(*b).into_value(tag),
toml::Value::Integer(n) => UntaggedValue::int(*n).into_value(tag),
toml::Value::Float(n) => UntaggedValue::decimal(*n).into_value(tag),
toml::Value::String(s) => {
UntaggedValue::Primitive(Primitive::String(String::from(s))).into_value(tag)
}
toml::Value::Array(a) => UntaggedValue::Table(
a.iter()
.map(|x| convert_toml_value_to_nu_value(x, &tag))
.collect(),
)
.into_value(tag),
toml::Value::Datetime(dt) => {
UntaggedValue::Primitive(Primitive::String(dt.to_string())).into_value(tag)
}
toml::Value::Table(t) => {
let mut collected = TaggedDictBuilder::new(&tag);
for (k, v) in t.iter() {
collected.insert_value(k.clone(), convert_toml_value_to_nu_value(v, &tag));
}
collected.into_value()
}
}
}
fn collect_values(input: &[Value]) -> Result<Vec<toml::Value>, ShellError> {
let mut out = vec![];
for value in input {
out.push(helper(value)?);
}
Ok(out)
}
// Helper method to recursively convert nu_protocol::Value -> toml::Value
// This shouldn't be called at the top-level
fn helper(v: &Value) -> Result<toml::Value, ShellError> {
Ok(match &v.value {
UntaggedValue::Primitive(Primitive::Boolean(b)) => toml::Value::Boolean(*b),
UntaggedValue::Primitive(Primitive::Filesize(b)) => toml::Value::Integer(*b as i64),
UntaggedValue::Primitive(Primitive::Duration(i)) => toml::Value::String(i.to_string()),
UntaggedValue::Primitive(Primitive::Date(d)) => toml::Value::String(d.to_string()),
UntaggedValue::Primitive(Primitive::EndOfStream) => {
toml::Value::String("<End of Stream>".to_string())
}
UntaggedValue::Primitive(Primitive::BeginningOfStream) => {
toml::Value::String("<Beginning of Stream>".to_string())
}
UntaggedValue::Primitive(Primitive::Decimal(f)) => {
toml::Value::Float(f.tagged(&v.tag).coerce_into("converting to TOML float")?)
}
UntaggedValue::Primitive(Primitive::Int(i)) => {
toml::Value::Integer(i.tagged(&v.tag).coerce_into("converting to TOML integer")?)
}
UntaggedValue::Primitive(Primitive::Nothing) => {
toml::Value::String("<Nothing>".to_string())
}
UntaggedValue::Primitive(Primitive::Pattern(s)) => toml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::String(s)) => toml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::Line(s)) => toml::Value::String(s.clone()),
UntaggedValue::Primitive(Primitive::Path(s)) => {
toml::Value::String(s.display().to_string())
}
UntaggedValue::Primitive(Primitive::ColumnPath(path)) => toml::Value::Array(
path.iter()
.map(|x| match &x.unspanned {
UnspannedPathMember::String(string) => Ok(toml::Value::String(string.clone())),
UnspannedPathMember::Int(int) => Ok(toml::Value::Integer(
int.tagged(&v.tag)
.coerce_into("converting to TOML integer")?,
)),
})
.collect::<Result<Vec<toml::Value>, ShellError>>()?,
),
UntaggedValue::Table(l) => toml::Value::Array(collect_values(l)?),
UntaggedValue::Error(e) => return Err(e.clone()),
UntaggedValue::Block(_) => toml::Value::String("<Block>".to_string()),
UntaggedValue::Primitive(Primitive::Range(_)) => toml::Value::String("<Range>".to_string()),
UntaggedValue::Primitive(Primitive::Binary(b)) => {
toml::Value::Array(b.iter().map(|x| toml::Value::Integer(*x as i64)).collect())
}
UntaggedValue::Row(o) => {
let mut m = toml::map::Map::new();
for (k, v) in o.entries.iter() {
m.insert(k.clone(), helper(v)?);
}
toml::Value::Table(m)
}
})
}
/// Converts a nu_protocol::Value into a toml::Value
/// Will return a Shell Error, if the Nu Value is not a valid top-level TOML Value
pub fn value_to_toml_value(v: &Value) -> Result<toml::Value, ShellError> {
match &v.value {
UntaggedValue::Row(o) => {
let mut m = toml::map::Map::new();
for (k, v) in o.entries.iter() {
m.insert(k.clone(), helper(v)?);
}
Ok(toml::Value::Table(m))
}
UntaggedValue::Primitive(Primitive::String(s)) => {
// Attempt to de-serialize the String
toml::de::from_str(s).map_err(|_| {
ShellError::labeled_error(
format!("{:?} unable to de-serialize string to TOML", s),
"invalid TOML",
v.tag(),
)
})
}
_ => Err(ShellError::labeled_error(
format!("{:?} is not a valid top-level TOML", v.value),
"invalid TOML",
v.tag(),
)),
}
}
#[cfg(feature = "directories")]
pub fn config_path() -> Result<PathBuf, ShellError> {
use directories::ProjectDirs;
let dir = ProjectDirs::from("org", "nushell", "nu")
.ok_or_else(|| ShellError::untagged_runtime_error("Couldn't find project directory"))?;
let path = ProjectDirs::config_dir(&dir).to_owned();
std::fs::create_dir_all(&path).map_err(|err| {
ShellError::untagged_runtime_error(&format!("Couldn't create {} path:\n{}", "config", err))
})?;
Ok(path)
}
#[cfg(not(feature = "directories"))]
pub fn config_path() -> Result<PathBuf, ShellError> {
// FIXME: unsure if this should be error or a simple default
Ok(std::path::PathBuf::from("/"))
}
pub fn default_path() -> Result<PathBuf, ShellError> {
default_path_for(&None)
}
pub fn default_path_for(file: &Option<PathBuf>) -> Result<PathBuf, ShellError> {
let mut filename = config_path()?;
let file: &Path = file
.as_ref()
.map(AsRef::as_ref)
.unwrap_or_else(|| "config.toml".as_ref());
filename.push(file);
Ok(filename)
}
#[cfg(feature = "directories")]
pub fn user_data() -> Result<PathBuf, ShellError> {
use directories::ProjectDirs;
let dir = ProjectDirs::from("org", "nushell", "nu")
.ok_or_else(|| ShellError::untagged_runtime_error("Couldn't find project directory"))?;
let path = ProjectDirs::data_local_dir(&dir).to_owned();
std::fs::create_dir_all(&path).map_err(|err| {
ShellError::untagged_runtime_error(&format!(
"Couldn't create {} path:\n{}",
"user data", err
))
})?;
Ok(path)
}
#[cfg(not(feature = "directories"))]
pub fn user_data() -> Result<PathBuf, ShellError> {
// FIXME: unsure if this should be error or a simple default
Ok(std::path::PathBuf::from("/"))
}
pub fn read(
tag: impl Into<Tag>,
at: &Option<PathBuf>,
) -> Result<IndexMap<String, Value>, ShellError> {
let filename = default_path()?;
let filename = match at {
None => filename,
Some(ref file) => file.clone(),
};
if !filename.exists() && touch(&filename).is_err() {
// If we can't create configs, let's just return an empty indexmap instead as we may be in
// a readonly environment
return Ok(IndexMap::new());
}
trace!("config file = {}", filename.display());
let tag = tag.into();
let contents = fs::read_to_string(filename)
.map(|v| v.tagged(&tag))
.map_err(|err| {
ShellError::labeled_error(
&format!("Couldn't read config file:\n{}", err),
"file name",
&tag,
)
})?;
let parsed: toml::Value = toml::from_str(&contents).map_err(|err| {
ShellError::labeled_error(
&format!("Couldn't parse config file:\n{}", err),
"file name",
&tag,
)
})?;
let value = convert_toml_value_to_nu_value(&parsed, tag);
let tag = value.tag();
match value.value {
UntaggedValue::Row(Dictionary { entries }) => Ok(entries),
other => Err(ShellError::type_error(
"Dictionary",
other.type_name().spanned(tag.span),
)),
}
}
pub fn config(tag: impl Into<Tag>) -> Result<IndexMap<String, Value>, ShellError> {
read(tag, &None)
}
pub fn write(config: &IndexMap<String, Value>, at: &Option<PathBuf>) -> Result<(), ShellError> {
let filename = &mut default_path()?;
let filename = match at {
None => filename,
Some(file) => {
filename.pop();
filename.push(file);
filename
}
};
let contents = value_to_toml_value(
&UntaggedValue::Row(Dictionary::new(config.clone())).into_untagged_value(),
)?;
let contents = toml::to_string(&contents)?;
fs::write(&filename, &contents)?;
Ok(())
}
// A simple implementation of `% touch path` (ignores existing files)
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}

View File

@ -0,0 +1,22 @@
use nu_protocol::Value;
use std::fmt::Debug;
pub trait Conf: Debug + Send {
fn env(&self) -> Option<Value>;
fn path(&self) -> Option<Value>;
fn reload(&self);
}
impl Conf for Box<dyn Conf> {
fn env(&self) -> Option<Value> {
(**self).env()
}
fn path(&self) -> Option<Value> {
(**self).path()
}
fn reload(&self) {
(**self).reload();
}
}

View File

@ -0,0 +1,64 @@
use crate::config::{read, Conf};
use indexmap::IndexMap;
use nu_protocol::Value;
use nu_source::Tag;
use parking_lot::Mutex;
use std::fmt::Debug;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct NuConfig {
pub vars: Arc<Mutex<IndexMap<String, Value>>>,
}
impl Conf for NuConfig {
fn env(&self) -> Option<Value> {
self.env()
}
fn path(&self) -> Option<Value> {
self.path()
}
fn reload(&self) {
let mut vars = self.vars.lock();
if let Ok(variables) = read(Tag::unknown(), &None) {
vars.extend(variables);
}
}
}
impl NuConfig {
pub fn new() -> NuConfig {
let vars = if let Ok(variables) = read(Tag::unknown(), &None) {
variables
} else {
IndexMap::default()
};
NuConfig {
vars: Arc::new(Mutex::new(vars)),
}
}
pub fn env(&self) -> Option<Value> {
let vars = self.vars.lock();
if let Some(env_vars) = vars.get("env") {
return Some(env_vars.clone());
}
None
}
pub fn path(&self) -> Option<Value> {
let vars = self.vars.lock();
if let Some(env_vars) = vars.get("path") {
return Some(env_vars.clone());
}
None
}
}

View File

@ -0,0 +1,131 @@
use crate::config::nuconfig::NuConfig;
use std::fmt::Debug;
#[derive(PartialEq, Debug)]
pub enum AutoPivotMode {
Auto,
Always,
Never,
}
pub trait HasTableProperties: Debug + Send {
fn pivot_mode(&self) -> AutoPivotMode;
fn header_alignment(&self) -> nu_table::Alignment;
fn header_color(&self) -> Option<ansi_term::Color>;
fn header_bold(&self) -> bool;
fn table_mode(&self) -> nu_table::Theme;
fn disabled_indexes(&self) -> bool;
}
pub fn pivot_mode(config: &NuConfig) -> AutoPivotMode {
let vars = config.vars.lock();
if let Some(mode) = vars.get("pivot_mode") {
let mode = match mode.as_string() {
Ok(m) if m.to_lowercase() == "auto" => AutoPivotMode::Auto,
Ok(m) if m.to_lowercase() == "always" => AutoPivotMode::Always,
Ok(m) if m.to_lowercase() == "never" => AutoPivotMode::Never,
_ => AutoPivotMode::Always,
};
return mode;
}
AutoPivotMode::Always
}
pub fn header_alignment(config: &NuConfig) -> nu_table::Alignment {
let vars = config.vars.lock();
let alignment = vars.get("header_align");
if alignment.is_none() {
return nu_table::Alignment::Center;
}
alignment.map_or(nu_table::Alignment::Left, |a| {
a.as_string().map_or(nu_table::Alignment::Center, |a| {
match a.to_lowercase().as_str() {
"center" | "c" => nu_table::Alignment::Center,
"right" | "r" => nu_table::Alignment::Right,
_ => nu_table::Alignment::Center,
}
})
})
}
pub fn header_color(config: &NuConfig) -> Option<ansi_term::Color> {
let vars = config.vars.lock();
Some(match vars.get("header_color") {
Some(c) => match c.as_string() {
Ok(color) => match color.to_lowercase().as_str() {
"g" | "green" => ansi_term::Color::Green,
"r" | "red" => ansi_term::Color::Red,
"u" | "blue" => ansi_term::Color::Blue,
"b" | "black" => ansi_term::Color::Black,
"y" | "yellow" => ansi_term::Color::Yellow,
"p" | "purple" => ansi_term::Color::Purple,
"c" | "cyan" => ansi_term::Color::Cyan,
"w" | "white" => ansi_term::Color::White,
_ => ansi_term::Color::Green,
},
_ => ansi_term::Color::Green,
},
_ => ansi_term::Color::Green,
})
}
pub fn header_bold(config: &NuConfig) -> bool {
let vars = config.vars.lock();
vars.get("header_bold")
.map(|x| x.as_bool().unwrap_or(true))
.unwrap_or(true)
}
pub fn table_mode(config: &NuConfig) -> nu_table::Theme {
let vars = config.vars.lock();
vars.get("table_mode")
.map_or(nu_table::Theme::compact(), |mode| match mode.as_string() {
Ok(m) if m == "basic" => nu_table::Theme::basic(),
Ok(m) if m == "compact" => nu_table::Theme::compact(),
Ok(m) if m == "light" => nu_table::Theme::light(),
Ok(m) if m == "thin" => nu_table::Theme::thin(),
_ => nu_table::Theme::compact(),
})
}
pub fn disabled_indexes(config: &NuConfig) -> bool {
let vars = config.vars.lock();
vars.get("disable_table_indexes")
.map_or(false, |x| x.as_bool().unwrap_or(false))
}
impl HasTableProperties for NuConfig {
fn pivot_mode(&self) -> AutoPivotMode {
pivot_mode(self)
}
fn header_alignment(&self) -> nu_table::Alignment {
header_alignment(self)
}
fn header_color(&self) -> Option<ansi_term::Color> {
header_color(self)
}
fn header_bold(&self) -> bool {
header_bold(self)
}
fn table_mode(&self) -> nu_table::Theme {
table_mode(self)
}
fn disabled_indexes(&self) -> bool {
disabled_indexes(self)
}
}

View File

@ -0,0 +1,44 @@
use crate::config::{read, Conf, NuConfig};
use indexmap::IndexMap;
use nu_protocol::Value;
use nu_source::Tag;
use parking_lot::Mutex;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug)]
pub struct FakeConfig {
pub config: NuConfig,
}
impl Conf for FakeConfig {
fn env(&self) -> Option<Value> {
self.config.env()
}
fn path(&self) -> Option<Value> {
self.config.path()
}
fn reload(&self) {
// no-op
}
}
impl FakeConfig {
pub fn new(config_file: &Path) -> FakeConfig {
let config_file = PathBuf::from(config_file);
let vars = if let Ok(variables) = read(Tag::unknown(), &Some(config_file)) {
variables
} else {
IndexMap::default()
};
FakeConfig {
config: NuConfig {
vars: Arc::new(Mutex::new(vars)),
},
}
}
}

102
crates/nu-data/src/dict.rs Normal file
View File

@ -0,0 +1,102 @@
use derive_new::new;
use nu_protocol::{Dictionary, MaybeOwned, Primitive, UntaggedValue, Value};
use nu_source::{b, DebugDocBuilder, PrettyDebug, Spanned, Tag};
#[derive(Debug, new)]
struct DebugEntry<'a> {
key: &'a str,
value: &'a Value,
}
impl<'a> PrettyDebug for DebugEntry<'a> {
fn pretty(&self) -> DebugDocBuilder {
(b::key(self.key.to_string()) + b::equals() + self.value.pretty().into_value()).group()
}
}
pub trait DictionaryExt {
fn get_data(&self, desc: &str) -> MaybeOwned<'_, Value>;
fn keys(&self) -> indexmap::map::Keys<String, Value>;
fn get_data_by_key(&self, name: Spanned<&str>) -> Option<Value>;
fn get_mut_data_by_key(&mut self, name: &str) -> Option<&mut Value>;
fn insert_data_at_key(&mut self, name: &str, value: Value);
}
impl DictionaryExt for Dictionary {
fn get_data(&self, desc: &str) -> MaybeOwned<'_, Value> {
match self.entries.get(desc) {
Some(v) => MaybeOwned::Borrowed(v),
None => MaybeOwned::Owned(
UntaggedValue::Primitive(Primitive::Nothing).into_untagged_value(),
),
}
}
fn keys(&self) -> indexmap::map::Keys<String, Value> {
self.entries.keys()
}
fn get_data_by_key(&self, name: Spanned<&str>) -> Option<Value> {
let result = self
.entries
.iter()
.find(|(desc_name, _)| *desc_name == name.item)?
.1;
Some(
result
.value
.clone()
.into_value(Tag::new(result.anchor(), name.span)),
)
}
fn get_mut_data_by_key(&mut self, name: &str) -> Option<&mut Value> {
self.entries
.iter_mut()
.find(|(desc_name, _)| *desc_name == name)
.map_or_else(|| None, |x| Some(x.1))
}
fn insert_data_at_key(&mut self, name: &str, value: Value) {
self.entries.insert(name.to_string(), value);
}
}
#[derive(Debug)]
pub struct TaggedListBuilder {
tag: Tag,
pub list: Vec<Value>,
}
impl TaggedListBuilder {
pub fn new(tag: impl Into<Tag>) -> TaggedListBuilder {
TaggedListBuilder {
tag: tag.into(),
list: vec![],
}
}
pub fn push_value(&mut self, value: impl Into<Value>) {
self.list.push(value.into());
}
pub fn push_untagged(&mut self, value: impl Into<UntaggedValue>) {
self.list.push(value.into().into_value(self.tag.clone()));
}
pub fn into_value(self) -> Value {
UntaggedValue::Table(self.list).into_value(self.tag)
}
pub fn into_untagged_value(self) -> UntaggedValue {
UntaggedValue::Table(self.list).into_value(self.tag).value
}
}
impl From<TaggedListBuilder> for Value {
fn from(input: TaggedListBuilder) -> Value {
input.into_value()
}
}

10
crates/nu-data/src/lib.rs Normal file
View File

@ -0,0 +1,10 @@
pub mod base;
pub mod command;
pub mod config;
pub mod dict;
pub mod primitive;
pub mod types;
pub mod utils;
pub mod value;
pub use dict::TaggedListBuilder;

View File

View File

@ -0,0 +1,20 @@
use nu_protocol::{hir::Number, Primitive};
use nu_table::TextStyle;
pub fn number(number: impl Into<Number>) -> Primitive {
let number = number.into();
match number {
Number::Int(int) => Primitive::Int(int),
Number::Decimal(decimal) => Primitive::Decimal(decimal),
}
}
pub fn style_primitive(primitive: &Primitive) -> TextStyle {
match primitive {
Primitive::Int(_) | Primitive::Filesize(_) | Primitive::Decimal(_) => {
TextStyle::basic_right()
}
_ => TextStyle::basic(),
}
}

View File

@ -0,0 +1,29 @@
use nu_data::{TaggedDictBuilder, Value};
use crate::prelude::*;
use itertools::join;
use sysinfo::ProcessExt;
pub(crate) fn process_dict(proc: &sysinfo::Process, tag: impl Into<Tag>) -> Value {
let mut dict = TaggedDictBuilder::new(tag);
let cmd = proc.cmd();
let cmd_value = if cmd.len() == 0 {
value::nothing()
} else {
value::string(join(cmd, ""))
};
dict.insert("pid", value::int(proc.pid() as i64));
dict.insert("status", value::string(proc.status().to_string()));
dict.insert("cpu", value::number(proc.cpu_usage()));
match cmd_value {
UntaggedValue::Primitive(Primitive::Nothing) => {
dict.insert("name", value::string(proc.name()));
}
_ => dict.insert("name", cmd_value),
}
dict.into_value()
}

View File

@ -0,0 +1,91 @@
use log::trace;
use nu_errors::{CoerceInto, ShellError};
use nu_protocol::{Primitive, SpannedTypeName, UntaggedValue, Value};
use nu_source::{Tagged, TaggedItem};
pub trait ExtractType: Sized {
fn extract(value: &Value) -> Result<Self, ShellError>;
}
impl<T: ExtractType> ExtractType for Tagged<T> {
fn extract(value: &Value) -> Result<Tagged<T>, ShellError> {
let name = std::any::type_name::<T>();
trace!("<Tagged> Extracting {:?} for Tagged<{}>", value, name);
Ok(T::extract(value)?.tagged(value.tag()))
}
}
impl ExtractType for bool {
fn extract(value: &Value) -> Result<bool, ShellError> {
trace!("Extracting {:?} for bool", value);
match &value {
Value {
value: UntaggedValue::Primitive(Primitive::Boolean(b)),
..
} => Ok(*b),
other => Err(ShellError::type_error("Boolean", other.spanned_type_name())),
}
}
}
impl ExtractType for std::path::PathBuf {
fn extract(value: &Value) -> Result<std::path::PathBuf, ShellError> {
trace!("Extracting {:?} for PathBuf", value);
match &value {
Value {
value: UntaggedValue::Primitive(Primitive::Path(p)),
..
} => Ok(p.clone()),
other => Err(ShellError::type_error("Path", other.spanned_type_name())),
}
}
}
impl ExtractType for i64 {
fn extract(value: &Value) -> Result<i64, ShellError> {
trace!("Extracting {:?} for i64", value);
match &value {
&Value {
value: UntaggedValue::Primitive(Primitive::Int(int)),
..
} => Ok(int.tagged(&value.tag).coerce_into("converting to i64")?),
other => Err(ShellError::type_error("Integer", other.spanned_type_name())),
}
}
}
impl ExtractType for u64 {
fn extract(value: &Value) -> Result<u64, ShellError> {
trace!("Extracting {:?} for u64", value);
match &value {
&Value {
value: UntaggedValue::Primitive(Primitive::Int(int)),
..
} => Ok(int.tagged(&value.tag).coerce_into("converting to u64")?),
other => Err(ShellError::type_error("Integer", other.spanned_type_name())),
}
}
}
impl ExtractType for String {
fn extract(value: &Value) -> Result<String, ShellError> {
trace!("Extracting {:?} for String", value);
match value {
Value {
value: UntaggedValue::Primitive(Primitive::String(string)),
..
} => Ok(string.clone()),
Value {
value: UntaggedValue::Primitive(Primitive::Line(string)),
..
} => Ok(string.clone()),
other => Err(ShellError::type_error("String", other.spanned_type_name())),
}
}
}

View File

@ -0,0 +1,35 @@
use indexmap::IndexMap;
use nu_errors::ShellError;
use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value};
use nu_source::Tag;
use nu_value_ext::as_string;
#[allow(clippy::type_complexity)]
pub fn group(
values: &Value,
grouper: &Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
let tag = tag.into();
let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
for (idx, value) in values.table_entries().enumerate() {
let group_key = if let Some(ref grouper) = grouper {
grouper(idx, &value)
} else {
as_string(&value)
};
let group = groups.entry(group_key?).or_insert(vec![]);
group.push((*value).clone());
}
let mut out = TaggedDictBuilder::new(&tag);
for (k, v) in groups.iter() {
out.insert_untagged(k, UntaggedValue::table(v));
}
Ok(out.into_value())
}

View File

@ -0,0 +1,273 @@
#![allow(clippy::type_complexity)]
use crate::value::compute_values;
use derive_new::new;
use nu_errors::ShellError;
use nu_protocol::hir::Operator;
use nu_protocol::{UntaggedValue, Value};
use nu_source::{SpannedItem, Tag, TaggedItem};
use nu_value_ext::ValueExt;
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new)]
pub struct Labels {
pub x: Vec<String>,
pub y: Vec<String>,
}
impl Labels {
pub fn at(&self, idx: usize) -> Option<&str> {
if let Some(k) = self.x.get(idx) {
Some(&k[..])
} else {
None
}
}
pub fn grouped(&self) -> impl Iterator<Item = &String> {
self.x.iter()
}
pub fn grouping_total(&self) -> Value {
UntaggedValue::int(self.x.len()).into_untagged_value()
}
pub fn splits(&self) -> impl Iterator<Item = &String> {
self.y.iter()
}
pub fn splits_total(&self) -> Value {
UntaggedValue::int(self.y.len()).into_untagged_value()
}
}
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new)]
pub struct Range {
pub start: Value,
pub end: Value,
}
fn formula(
acc_begin: Value,
calculator: Box<dyn Fn(Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static>,
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
Box::new(move |acc, datax| -> Result<Value, ShellError> {
let result = match compute_values(Operator::Multiply, &acc, &acc_begin) {
Ok(v) => v.into_untagged_value(),
Err((left_type, right_type)) => {
return Err(ShellError::coerce_error(
left_type.spanned_unknown(),
right_type.spanned_unknown(),
))
}
};
match calculator(datax) {
Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) {
Ok(v) => v.into_untagged_value(),
Err((left_type, right_type)) => {
return Err(ShellError::coerce_error(
left_type.spanned_unknown(),
right_type.spanned_unknown(),
))
}
}),
Err(reason) => Err(reason),
}
})
}
pub fn reducer_for(
command: Reduction,
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
match command {
Reduction::Accumulate => Box::new(formula(
UntaggedValue::int(1).into_untagged_value(),
Box::new(sum),
)),
_ => Box::new(formula(
UntaggedValue::int(0).into_untagged_value(),
Box::new(sum),
)),
}
}
pub fn max(values: &Value, tag: impl Into<Tag>) -> Result<&Value, ShellError> {
let tag = tag.into();
values
.table_entries()
.filter_map(|dataset| dataset.table_entries().max())
.max()
.ok_or_else(|| ShellError::labeled_error("err", "err", &tag))
}
pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
let mut acc = UntaggedValue::int(0);
for value in data {
match value.value {
UntaggedValue::Primitive(_) => {
acc = match compute_values(Operator::Plus, &acc, &value) {
Ok(v) => v,
Err((left_type, right_type)) => {
return Err(ShellError::coerce_error(
left_type.spanned_unknown(),
right_type.spanned_unknown(),
))
}
};
}
_ => {
return Err(ShellError::labeled_error(
"Attempted to compute the sum of a value that cannot be summed.",
"value appears here",
value.tag.span,
))
}
}
}
Ok(acc.into_untagged_value())
}
pub fn sort_columns(
values: &[String],
format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
) -> Result<Vec<String>, ShellError> {
let mut keys = vec![];
if let Some(fmt) = format {
for k in values.iter() {
let k = k.clone().tagged_unknown();
let v = crate::value::Date::naive_from_str(k.borrow_tagged())?.into_untagged_value();
keys.push(fmt(&v, k.to_string())?);
}
} else {
keys = values.to_vec();
}
keys.sort();
Ok(keys)
}
pub fn sort(planes: &Labels, values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
let tag = tag.into();
let mut x = vec![];
for column in planes.splits() {
let key = column.clone().tagged_unknown();
let groups = values
.get_data_by_key(key.borrow_spanned())
.ok_or_else(|| {
ShellError::labeled_error("unknown column", "unknown column", key.span())
})?;
let mut y = vec![];
for inner_column in planes.grouped() {
let key = inner_column.clone().tagged_unknown();
let grouped = groups.get_data_by_key(key.borrow_spanned());
if let Some(grouped) = grouped {
y.push(grouped.table_entries().cloned().collect::<Vec<_>>());
} else {
let empty = UntaggedValue::table(&[]).into_value(&tag);
y.push(empty.table_entries().cloned().collect::<Vec<_>>());
}
}
x.push(
UntaggedValue::table(&y.iter().cloned().flatten().collect::<Vec<Value>>())
.into_value(&tag),
);
}
Ok(UntaggedValue::table(&x).into_value(&tag))
}
pub fn evaluate(
values: &Value,
evaluator: &Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
let tag = tag.into();
let mut x = vec![];
for split in values.table_entries() {
let mut y = vec![];
for (idx, subset) in split.table_entries().enumerate() {
let mut set = vec![];
if let Some(ref evaluator) = evaluator {
let value = evaluator(idx, subset)?;
set.push(value);
} else {
set.push(UntaggedValue::int(1).into_value(&tag));
}
y.push(UntaggedValue::table(&set).into_value(&tag));
}
x.push(UntaggedValue::table(&y).into_value(&tag));
}
Ok(UntaggedValue::table(&x).into_value(&tag))
}
pub enum Reduction {
#[allow(dead_code)]
Count,
Accumulate,
}
pub fn reduce(values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
let tag = tag.into();
let reduce_with = reducer_for(Reduction::Accumulate);
let mut datasets = vec![];
for dataset in values.table_entries() {
let mut acc = UntaggedValue::int(0).into_value(&tag);
let mut subsets = vec![];
for subset in dataset.table_entries() {
acc = reduce_with(&acc, subset.table_entries().collect::<Vec<_>>())?;
subsets.push(acc.clone());
}
datasets.push(UntaggedValue::table(&subsets).into_value(&tag));
}
Ok(UntaggedValue::table(&datasets).into_value(&tag))
}
pub fn percentages(
maxima: &Value,
values: &Value,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
let tag = tag.into();
let mut x = vec![];
for split in values.table_entries() {
x.push(
UntaggedValue::table(
&split
.table_entries()
.filter_map(|s| {
let hundred = UntaggedValue::decimal(100);
match compute_values(Operator::Divide, &hundred, &maxima) {
Ok(v) => match compute_values(Operator::Multiply, &s, &v) {
Ok(v) => Some(v.into_untagged_value()),
Err(_) => None,
},
Err(_) => None,
}
})
.collect::<Vec<_>>(),
)
.into_value(&tag),
);
}
Ok(UntaggedValue::table(&x).into_value(&tag))
}

View File

@ -0,0 +1,309 @@
pub mod group;
pub mod split;
mod internal;
pub use crate::utils::group::group;
pub use crate::utils::split::split;
use crate::utils::internal::*;
use derive_new::new;
use getset::Getters;
use nu_errors::ShellError;
use nu_protocol::{UntaggedValue, Value};
use nu_source::Tag;
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Getters, Clone, new)]
pub struct Model {
pub labels: Labels,
pub ranges: (Range, Range),
pub data: Value,
pub percentages: Value,
}
#[allow(clippy::type_complexity)]
pub struct Operation<'a> {
pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
pub format: Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
}
pub fn report(
values: &Value,
options: Operation,
tag: impl Into<Tag>,
) -> Result<Model, ShellError> {
let tag = tag.into();
let grouped = group(&values, &options.grouper, &tag)?;
let splitted = split(&grouped, &options.splitter, &tag)?;
let x = grouped
.row_entries()
.map(|(key, _)| key.clone())
.collect::<Vec<_>>();
let x = if options.format.is_some() {
sort_columns(&x, &options.format)
} else {
sort_columns(&x, &None)
}?;
let mut y = splitted
.row_entries()
.map(|(key, _)| key.clone())
.collect::<Vec<_>>();
y.sort();
let planes = Labels { x, y };
let sorted = sort(&planes, &splitted, &tag)?;
let evaluated = evaluate(
&sorted,
if options.eval.is_some() {
options.eval
} else {
&None
},
&tag,
)?;
let group_labels = planes.grouping_total();
let reduced = reduce(&evaluated, &tag)?;
let max = max(&reduced, &tag)?.clone();
let maxima = max.clone();
let percents = percentages(&maxima, &reduced, &tag)?;
Ok(Model {
labels: planes,
ranges: (
Range {
start: UntaggedValue::int(0).into_untagged_value(),
end: group_labels,
},
Range {
start: UntaggedValue::int(0).into_untagged_value(),
end: max,
},
),
data: reduced,
percentages: percents,
})
}
pub mod helpers {
use super::Model;
use bigdecimal::BigDecimal;
use indexmap::indexmap;
use nu_errors::ShellError;
use nu_protocol::{UntaggedValue, Value};
use nu_source::{Tag, TaggedItem};
use nu_value_ext::ValueExt;
use num_bigint::BigInt;
use indexmap::IndexMap;
pub fn int(s: impl Into<BigInt>) -> Value {
UntaggedValue::int(s).into_untagged_value()
}
pub fn decimal(f: impl Into<BigDecimal>) -> Value {
UntaggedValue::decimal(f.into()).into_untagged_value()
}
pub fn string(input: impl Into<String>) -> Value {
UntaggedValue::string(input.into()).into_untagged_value()
}
pub fn row(entries: IndexMap<String, Value>) -> Value {
UntaggedValue::row(entries).into_untagged_value()
}
pub fn table(list: &[Value]) -> Value {
UntaggedValue::table(list).into_untagged_value()
}
pub fn date(input: impl Into<String>) -> Value {
let key = input.into().tagged_unknown();
crate::value::Date::naive_from_str(key.borrow_tagged())
.expect("date from string failed")
.into_untagged_value()
}
pub fn committers() -> Vec<Value> {
vec![
row(indexmap! {
"date".into() => date("2019-07-23"),
"name".into() => string("AR"),
"country".into() => string("EC"),
"chickens".into() => int(10),
}),
row(indexmap! {
"date".into() => date("2019-07-23"),
"name".into() => string("JT"),
"country".into() => string("NZ"),
"chickens".into() => int(5),
}),
row(indexmap! {
"date".into() => date("2019-10-10"),
"name".into() => string("YK"),
"country".into() => string("US"),
"chickens".into() => int(6),
}),
row(indexmap! {
"date".into() => date("2019-09-24"),
"name".into() => string("AR"),
"country".into() => string("EC"),
"chickens".into() => int(20),
}),
row(indexmap! {
"date".into() => date("2019-10-10"),
"name".into() => string("JT"),
"country".into() => string("NZ"),
"chickens".into() => int(15),
}),
row(indexmap! {
"date".into() => date("2019-09-24"),
"name".into() => string("YK"),
"country".into() => string("US"),
"chickens".into() => int(4),
}),
row(indexmap! {
"date".into() => date("2019-10-10"),
"name".into() => string("AR"),
"country".into() => string("EC"),
"chickens".into() => int(30),
}),
row(indexmap! {
"date".into() => date("2019-09-24"),
"name".into() => string("JT"),
"country".into() => string("NZ"),
"chickens".into() => int(10),
}),
row(indexmap! {
"date".into() => date("2019-07-23"),
"name".into() => string("YK"),
"country".into() => string("US"),
"chickens".into() => int(2),
}),
]
}
pub fn committers_grouped_by_date() -> Value {
let sample = table(&committers());
let grouper = Box::new(move |_, row: &Value| {
let key = String::from("date").tagged_unknown();
let group_key = row
.get_data_by_key(key.borrow_spanned())
.expect("get key failed");
group_key.format("%Y-%m-%d")
});
crate::utils::group(&sample, &Some(grouper), Tag::unknown())
.expect("failed to create group")
}
pub fn date_formatter(
fmt: &'static str,
) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> {
Box::new(move |date: &Value, _: String| date.format(&fmt))
}
pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) {
assert_eq!(report_a.labels.x, report_b.labels.x);
assert_eq!(report_a.labels.y, report_b.labels.y);
assert_eq!(report_a.ranges, report_b.ranges);
assert_eq!(report_a.data, report_b.data);
}
}
#[cfg(test)]
mod tests {
use super::helpers::{
assert_without_checking_percentages, committers, date_formatter, decimal, int, table,
};
use super::{report, Labels, Model, Operation, Range};
use nu_errors::ShellError;
use nu_protocol::Value;
use nu_source::{Tag, TaggedItem};
use nu_value_ext::ValueExt;
#[test]
fn prepares_report_using_accumulating_value() {
let committers = table(&committers());
let by_date = Box::new(move |_, row: &Value| {
let key = String::from("date").tagged_unknown();
let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
let callback = date_formatter("%Y-%m-%d");
callback(&key, "nothing".to_string())
});
let by_country = Box::new(move |_, row: &Value| {
let key = String::from("country").tagged_unknown();
let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
nu_value_ext::as_string(&key)
});
let options = Operation {
grouper: Some(by_date),
splitter: Some(by_country),
format: Some(date_formatter("%Y-%m-%d")),
eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| {
let chickens_key = String::from("chickens").tagged_unknown();
value
.get_data_by_key(chickens_key.borrow_spanned())
.ok_or_else(|| {
ShellError::labeled_error(
"unknown column",
"unknown column",
chickens_key.span(),
)
})
})),
};
assert_without_checking_percentages(
report(&committers, options, Tag::unknown()).unwrap(),
Model {
labels: Labels {
x: vec![
String::from("2019-07-23"),
String::from("2019-09-24"),
String::from("2019-10-10"),
],
y: vec![String::from("EC"), String::from("NZ"), String::from("US")],
},
ranges: (
Range {
start: int(0),
end: int(3),
},
Range {
start: int(0),
end: int(60),
},
),
data: table(&[
table(&[int(10), int(30), int(60)]),
table(&[int(5), int(15), int(30)]),
table(&[int(2), int(6), int(12)]),
]),
percentages: table(&[
table(&[decimal(16.66), decimal(50), decimal(100)]),
table(&[decimal(8.33), decimal(25), decimal(50)]),
table(&[decimal(3.33), decimal(10), decimal(20)]),
]),
},
);
}
}

View File

@ -0,0 +1,59 @@
use nu_errors::ShellError;
use nu_protocol::{SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value};
use nu_source::Tag;
use crate::utils::group;
#[allow(clippy::type_complexity)]
pub fn split(
value: &Value,
splitter: &Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
let tag = tag.into();
let mut splits = indexmap::IndexMap::new();
let mut out = TaggedDictBuilder::new(&tag);
if splitter.is_none() {
out.insert_untagged("table", UntaggedValue::table(&[value.clone()]));
return Ok(out.into_value());
}
for (column, value) in value.row_entries() {
if !&value.is_table() {
return Err(ShellError::type_error(
"a table value",
value.spanned_type_name(),
));
}
match group(&value, splitter, &tag) {
Ok(grouped) => {
for (split_label, subset) in grouped.row_entries() {
let s = splits
.entry(split_label.clone())
.or_insert(indexmap::IndexMap::new());
if !&subset.is_table() {
return Err(ShellError::type_error(
"a table value",
subset.spanned_type_name(),
));
}
s.insert(column.clone(), subset.clone());
}
}
Err(err) => return Err(err),
}
}
let mut out = TaggedDictBuilder::new(&tag);
for (k, v) in splits.into_iter() {
out.insert_untagged(k, UntaggedValue::row(v));
}
Ok(out.into_value())
}

297
crates/nu-data/src/value.rs Normal file
View File

@ -0,0 +1,297 @@
use crate::base::coerce_compare;
use crate::base::shape::{Column, InlineShape};
use crate::primitive::style_primitive;
use chrono::{DateTime, NaiveDate, Utc};
use nu_errors::ShellError;
use nu_protocol::hir::Operator;
use nu_protocol::ShellTypeName;
use nu_protocol::{Primitive, Type, UntaggedValue};
use nu_source::{DebugDocBuilder, PrettyDebug, Span, Tagged};
use nu_table::TextStyle;
use num_traits::Zero;
pub struct Date;
impl Date {
pub fn from_regular_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| {
ShellError::labeled_error(
&format!("Date parse error: {}", err),
"original value",
s.tag,
)
})?;
let date = date.with_timezone(&chrono::offset::Utc);
Ok(UntaggedValue::Primitive(Primitive::Date(date)))
}
pub fn naive_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
let date = NaiveDate::parse_from_str(s.item, "%Y-%m-%d").map_err(|reason| {
ShellError::labeled_error(
&format!("Date parse error: {}", reason),
"original value",
s.tag,
)
})?;
Ok(UntaggedValue::Primitive(Primitive::Date(
DateTime::<Utc>::from_utc(date.and_hms(12, 34, 56), Utc),
)))
}
}
pub fn date_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
Date::from_regular_str(s)
}
pub fn merge_values(
left: &UntaggedValue,
right: &UntaggedValue,
) -> Result<UntaggedValue, (&'static str, &'static str)> {
match (left, right) {
(UntaggedValue::Row(columns), UntaggedValue::Row(columns_b)) => {
Ok(UntaggedValue::Row(columns.merge_from(columns_b)))
}
(left, right) => Err((left.type_name(), right.type_name())),
}
}
fn zero_division_error() -> UntaggedValue {
UntaggedValue::Error(ShellError::untagged_runtime_error("division by zero"))
}
pub fn unsafe_compute_values(
operator: Operator,
left: &UntaggedValue,
right: &UntaggedValue,
) -> Result<UntaggedValue, (&'static str, &'static str)> {
let computed = compute_values(operator, left, right);
if computed.is_ok() {
return computed;
}
match (left, right) {
(UntaggedValue::Primitive(lhs), UntaggedValue::Primitive(rhs)) => match (lhs, rhs) {
(Primitive::Filesize(x), Primitive::Int(y)) => match operator {
Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Int(x + y))),
Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Int(x - y))),
Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::Int(x * y))),
Operator::Divide => Ok(UntaggedValue::Primitive(Primitive::Decimal(
bigdecimal::BigDecimal::from(*x) / bigdecimal::BigDecimal::from(y.clone()),
))),
_ => Err((left.type_name(), right.type_name())),
},
(Primitive::Int(x), Primitive::Filesize(y)) => match operator {
Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Int(x + y))),
Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Int(x - y))),
Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::Int(x * y))),
Operator::Divide => Ok(UntaggedValue::Primitive(Primitive::Decimal(
bigdecimal::BigDecimal::from(x.clone()) / bigdecimal::BigDecimal::from(*y),
))),
_ => Err((left.type_name(), right.type_name())),
},
_ => Err((left.type_name(), right.type_name())),
},
_ => Err((left.type_name(), right.type_name())),
}
}
pub fn compute_values(
operator: Operator,
left: &UntaggedValue,
right: &UntaggedValue,
) -> Result<UntaggedValue, (&'static str, &'static str)> {
match (left, right) {
(UntaggedValue::Primitive(lhs), UntaggedValue::Primitive(rhs)) => match (lhs, rhs) {
(Primitive::Filesize(x), Primitive::Filesize(y)) => {
let result = match operator {
Operator::Plus => Ok(x + y),
Operator::Minus => Ok(x - y),
_ => Err((left.type_name(), right.type_name())),
}?;
Ok(UntaggedValue::Primitive(Primitive::Filesize(result)))
}
(Primitive::Int(x), Primitive::Int(y)) => match operator {
Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Int(x + y))),
Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Int(x - y))),
Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::Int(x * y))),
Operator::Divide => {
if y.is_zero() {
Ok(zero_division_error())
} else if x - (y * (x / y)) == num_bigint::BigInt::from(0) {
Ok(UntaggedValue::Primitive(Primitive::Int(x / y)))
} else {
Ok(UntaggedValue::Primitive(Primitive::Decimal(
bigdecimal::BigDecimal::from(x.clone())
/ bigdecimal::BigDecimal::from(y.clone()),
)))
}
}
_ => Err((left.type_name(), right.type_name())),
},
(Primitive::Decimal(x), Primitive::Int(y)) => {
let result = match operator {
Operator::Plus => Ok(x + bigdecimal::BigDecimal::from(y.clone())),
Operator::Minus => Ok(x - bigdecimal::BigDecimal::from(y.clone())),
Operator::Multiply => Ok(x * bigdecimal::BigDecimal::from(y.clone())),
Operator::Divide => {
if y.is_zero() {
return Ok(zero_division_error());
}
Ok(x / bigdecimal::BigDecimal::from(y.clone()))
}
_ => Err((left.type_name(), right.type_name())),
}?;
Ok(UntaggedValue::Primitive(Primitive::Decimal(result)))
}
(Primitive::Int(x), Primitive::Decimal(y)) => {
let result = match operator {
Operator::Plus => Ok(bigdecimal::BigDecimal::from(x.clone()) + y),
Operator::Minus => Ok(bigdecimal::BigDecimal::from(x.clone()) - y),
Operator::Multiply => Ok(bigdecimal::BigDecimal::from(x.clone()) * y),
Operator::Divide => {
if y.is_zero() {
return Ok(zero_division_error());
}
Ok(bigdecimal::BigDecimal::from(x.clone()) / y)
}
_ => Err((left.type_name(), right.type_name())),
}?;
Ok(UntaggedValue::Primitive(Primitive::Decimal(result)))
}
(Primitive::Decimal(x), Primitive::Decimal(y)) => {
let result = match operator {
Operator::Plus => Ok(x + y),
Operator::Minus => Ok(x - y),
Operator::Multiply => Ok(x * y),
Operator::Divide => {
if y.is_zero() {
return Ok(zero_division_error());
}
Ok(x / y)
}
_ => Err((left.type_name(), right.type_name())),
}?;
Ok(UntaggedValue::Primitive(Primitive::Decimal(result)))
}
(Primitive::Date(x), Primitive::Date(y)) => match operator {
Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::from(
x.signed_duration_since(*y),
))),
_ => Err((left.type_name(), right.type_name())),
},
(Primitive::Date(x), Primitive::Duration(_)) => {
let result = match operator {
Operator::Plus => {
// FIXME: Not sure if I could do something better with the Span.
let y = Primitive::into_chrono_duration(rhs.clone(), Span::unknown())
.expect("Could not convert nushell Duration into chrono Duration.");
Ok(x.checked_add_signed(y).expect("Data overflow."))
}
_ => Err((left.type_name(), right.type_name())),
}?;
Ok(UntaggedValue::Primitive(Primitive::Date(result)))
}
(Primitive::Duration(x), Primitive::Duration(y)) => {
let result = match operator {
Operator::Plus => Ok(x + y),
Operator::Minus => Ok(x - y),
_ => Err((left.type_name(), right.type_name())),
}?;
Ok(UntaggedValue::Primitive(Primitive::Duration(result)))
}
_ => Err((left.type_name(), right.type_name())),
},
_ => Err((left.type_name(), right.type_name())),
}
}
/// If left is {{ Operator }} right
pub fn compare_values(
operator: Operator,
left: &UntaggedValue,
right: &UntaggedValue,
) -> Result<bool, (&'static str, &'static str)> {
let coerced = coerce_compare(left, right)?;
let ordering = coerced.compare();
use std::cmp::Ordering;
let result = matches!(
(operator, ordering),
(Operator::Equal, Ordering::Equal)
| (Operator::GreaterThan, Ordering::Greater)
| (Operator::GreaterThanOrEqual, Ordering::Greater)
| (Operator::GreaterThanOrEqual, Ordering::Equal)
| (Operator::LessThan, Ordering::Less)
| (Operator::LessThanOrEqual, Ordering::Less)
| (Operator::LessThanOrEqual, Ordering::Equal)
| (Operator::NotEqual, Ordering::Greater)
| (Operator::NotEqual, Ordering::Less)
);
Ok(result)
}
pub fn format_type<'a>(value: impl Into<&'a UntaggedValue>, width: usize) -> String {
Type::from_value(value.into()).colored_string(width)
}
pub fn format_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> DebugDocBuilder {
InlineShape::from_value(value.into()).format().pretty()
}
pub fn style_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> TextStyle {
match value.into() {
UntaggedValue::Primitive(p) => style_primitive(p),
_ => TextStyle::basic(),
}
}
pub fn format_for_column<'a>(
value: impl Into<&'a UntaggedValue>,
column: impl Into<Column>,
) -> DebugDocBuilder {
InlineShape::from_value(value.into())
.format_for_column(column)
.pretty()
}
#[cfg(test)]
mod tests {
use super::merge_values;
use super::Date as d;
use super::UntaggedValue as v;
use nu_source::TaggedItem;
use indexmap::indexmap;
#[test]
fn merges_tables() {
let (author_1_date, author_2_date) = (
"2020-04-29".to_string().tagged_unknown(),
"2019-10-10".to_string().tagged_unknown(),
);
let table_author_row = v::row(indexmap! {
"name".into() => v::string("Andrés").into_untagged_value(),
"country".into() => v::string("EC").into_untagged_value(),
"date".into() => d::naive_from_str(author_1_date.borrow_tagged()).unwrap().into_untagged_value()
});
let other_table_author_row = v::row(indexmap! {
"name".into() => v::string("YK").into_untagged_value(),
"country".into() => v::string("US").into_untagged_value(),
"date".into() => d::naive_from_str(author_2_date.borrow_tagged()).unwrap().into_untagged_value()
});
assert_eq!(
other_table_author_row,
merge_values(&table_author_row, &other_table_author_row).unwrap()
);
}
}