Restructure nu-protocol in more meaningful units (#11917)

This is partially "feng-shui programming" of moving things to new
separate places.

The later commits include "`git blame` tollbooths" by moving out chunks
of code into new files, which requires an extra step to track things
with `git blame`. We can negiotiate if you want to keep particular
things in their original place.

If egregious I tried to add a bit of documentation. If I see something
that is unused/unnecessarily `pub` I will try to remove that.


- Move `nu_protocol::Exportable` to `nu-parser`
- Guess doccomment for `Exportable`
- Move `Unit` enum from `value` to `AST`
- Move engine state `Variable` def into its folder
- Move error-related files in `nu-protocol` subdir
- Move `pipeline_data` module into its own folder
- Move `stream.rs` over into the `pipeline_data` mod
- Move `PipelineMetadata` into its own file
- Doccomment `PipelineMetadata`
- Remove unused `is_leap_year` in `value/mod`
- Note about criminal `type_compatible` helper
- Move duration fmting into new `value/duration.rs`
- Move filesize fmting logic to new `value/filesize`
- Split reexports from standard imports in `value/mod`
- Doccomment trait `CustomValue`
- Polish doccomments and intradoc links
This commit is contained in:
Stefan Holderbach
2024-03-10 18:45:45 +01:00
committed by GitHub
parent 067ceedf79
commit f695ba408a
26 changed files with 392 additions and 358 deletions

View File

@ -2,25 +2,30 @@ use std::{cmp::Ordering, fmt};
use crate::{ast::Operator, ShellError, Span, Value};
// Trait definition for a custom value
/// Trait definition for a custom [`Value`](crate::Value) type
#[typetag::serde(tag = "type")]
pub trait CustomValue: fmt::Debug + Send + Sync {
/// Custom `Clone` implementation
///
/// This can reemit a `Value::CustomValue(Self, span)` or materialize another representation
/// if necessary.
fn clone_value(&self, span: Span) -> Value;
//fn category(&self) -> Category;
// Define string representation of the custom value
/// Define string representation of the custom value
fn value_string(&self) -> String;
// Converts the custom value to a base nushell value
// This is used to represent the custom value using the table representations
// That already exist in nushell
/// Converts the custom value to a base nushell value.
///
/// This imposes the requirement that you can represent the custom value in some form using the
/// Value representations that already exist in nushell
fn to_base_value(&self, span: Span) -> Result<Value, ShellError>;
// Any representation used to downcast object to its original type
/// Any representation used to downcast object to its original type
fn as_any(&self) -> &dyn std::any::Any;
// Follow cell path functions
/// Follow cell path by numeric index (e.g. rows)
fn follow_path_int(&self, _count: usize, span: Span) -> Result<Value, ShellError> {
Err(ShellError::IncompatiblePathAccess {
type_name: self.value_string(),
@ -28,6 +33,7 @@ pub trait CustomValue: fmt::Debug + Send + Sync {
})
}
/// Follow cell path by string key (e.g. columns)
fn follow_path_string(&self, _column_name: String, span: Span) -> Result<Value, ShellError> {
Err(ShellError::IncompatiblePathAccess {
type_name: self.value_string(),
@ -35,14 +41,17 @@ pub trait CustomValue: fmt::Debug + Send + Sync {
})
}
// ordering with other value
/// ordering with other value (see [`std::cmp::PartialOrd`])
fn partial_cmp(&self, _other: &Value) -> Option<Ordering> {
None
}
// Definition of an operation between the object that implements the trait
// and another Value.
// The Operator enum is used to indicate the expected operation
/// Definition of an operation between the object that implements the trait
/// and another Value.
///
/// The Operator enum is used to indicate the expected operation.
///
/// Default impl raises [`ShellError::UnsupportedOperator`].
fn operation(
&self,
_lhs_span: Span,

View File

@ -0,0 +1,181 @@
use chrono::Duration;
use std::{
borrow::Cow,
fmt::{Display, Formatter},
};
#[derive(Clone, Copy)]
pub enum TimePeriod {
Nanos(i64),
Micros(i64),
Millis(i64),
Seconds(i64),
Minutes(i64),
Hours(i64),
Days(i64),
Weeks(i64),
Months(i64),
Years(i64),
}
impl TimePeriod {
pub fn to_text(self) -> Cow<'static, str> {
match self {
Self::Nanos(n) => format!("{n} ns").into(),
Self::Micros(n) => format!("{n} µs").into(),
Self::Millis(n) => format!("{n} ms").into(),
Self::Seconds(n) => format!("{n} sec").into(),
Self::Minutes(n) => format!("{n} min").into(),
Self::Hours(n) => format!("{n} hr").into(),
Self::Days(n) => format!("{n} day").into(),
Self::Weeks(n) => format!("{n} wk").into(),
Self::Months(n) => format!("{n} month").into(),
Self::Years(n) => format!("{n} yr").into(),
}
}
}
impl Display for TimePeriod {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_text())
}
}
pub fn format_duration(duration: i64) -> String {
let (sign, periods) = format_duration_as_timeperiod(duration);
let text = periods
.into_iter()
.map(|p| p.to_text().to_string().replace(' ', ""))
.collect::<Vec<String>>();
format!(
"{}{}",
if sign == -1 { "-" } else { "" },
text.join(" ").trim()
)
}
pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
// Current duration doesn't know a date it's based on, weeks is the max time unit it can normalize into.
// Don't guess or estimate how many years or months it might contain.
let (sign, duration) = if duration >= 0 {
(1, duration)
} else {
(-1, -duration)
};
let dur = Duration::nanoseconds(duration);
/// Split this a duration into number of whole weeks and the remainder
fn split_weeks(duration: Duration) -> (Option<i64>, Duration) {
let weeks = duration.num_weeks();
normalize_split(weeks, Duration::try_weeks(weeks), duration)
}
/// Split this a duration into number of whole days and the remainder
fn split_days(duration: Duration) -> (Option<i64>, Duration) {
let days = duration.num_days();
normalize_split(days, Duration::try_days(days), duration)
}
/// Split this a duration into number of whole hours and the remainder
fn split_hours(duration: Duration) -> (Option<i64>, Duration) {
let hours = duration.num_hours();
normalize_split(hours, Duration::try_hours(hours), duration)
}
/// Split this a duration into number of whole minutes and the remainder
fn split_minutes(duration: Duration) -> (Option<i64>, Duration) {
let minutes = duration.num_minutes();
normalize_split(minutes, Duration::try_minutes(minutes), duration)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_seconds(duration: Duration) -> (Option<i64>, Duration) {
let seconds = duration.num_seconds();
normalize_split(seconds, Duration::try_seconds(seconds), duration)
}
/// Split this a duration into number of whole milliseconds and the remainder
fn split_milliseconds(duration: Duration) -> (Option<i64>, Duration) {
let millis = duration.num_milliseconds();
normalize_split(millis, Duration::try_milliseconds(millis), duration)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_microseconds(duration: Duration) -> (Option<i64>, Duration) {
let micros = duration.num_microseconds().unwrap_or_default();
normalize_split(micros, Duration::microseconds(micros), duration)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_nanoseconds(duration: Duration) -> (Option<i64>, Duration) {
let nanos = duration.num_nanoseconds().unwrap_or_default();
normalize_split(nanos, Duration::nanoseconds(nanos), duration)
}
fn normalize_split(
wholes: i64,
wholes_duration: impl Into<Option<Duration>>,
total_duration: Duration,
) -> (Option<i64>, Duration) {
match wholes_duration.into() {
Some(wholes_duration) if wholes != 0 => {
(Some(wholes), total_duration - wholes_duration)
}
_ => (None, total_duration),
}
}
let mut periods = vec![];
let (weeks, remainder) = split_weeks(dur);
if let Some(weeks) = weeks {
periods.push(TimePeriod::Weeks(weeks));
}
let (days, remainder) = split_days(remainder);
if let Some(days) = days {
periods.push(TimePeriod::Days(days));
}
let (hours, remainder) = split_hours(remainder);
if let Some(hours) = hours {
periods.push(TimePeriod::Hours(hours));
}
let (minutes, remainder) = split_minutes(remainder);
if let Some(minutes) = minutes {
periods.push(TimePeriod::Minutes(minutes));
}
let (seconds, remainder) = split_seconds(remainder);
if let Some(seconds) = seconds {
periods.push(TimePeriod::Seconds(seconds));
}
let (millis, remainder) = split_milliseconds(remainder);
if let Some(millis) = millis {
periods.push(TimePeriod::Millis(millis));
}
let (micros, remainder) = split_microseconds(remainder);
if let Some(micros) = micros {
periods.push(TimePeriod::Micros(micros));
}
let (nanos, _remainder) = split_nanoseconds(remainder);
if let Some(nanos) = nanos {
periods.push(TimePeriod::Nanos(nanos));
}
if periods.is_empty() {
periods.push(TimePeriod::Seconds(0));
}
(sign, periods)
}

View File

@ -0,0 +1,116 @@
use crate::Config;
use byte_unit::UnitType;
use nu_utils::get_system_locale;
use num_format::ToFormattedString;
pub fn format_filesize_from_conf(num_bytes: i64, config: &Config) -> String {
// We need to take into account config.filesize_metric so, if someone asks for KB
// and filesize_metric is false, return KiB
format_filesize(
num_bytes,
config.filesize_format.as_str(),
Some(config.filesize_metric),
)
}
// filesize_metric is explicit when printed a value according to user config;
// other places (such as `format filesize`) don't.
pub fn format_filesize(
num_bytes: i64,
format_value: &str,
filesize_metric: Option<bool>,
) -> String {
// Allow the user to specify how they want their numbers formatted
// When format_value is "auto" or an invalid value, the returned ByteUnit doesn't matter
// and is always B.
let filesize_unit = get_filesize_format(format_value, filesize_metric);
let byte = byte_unit::Byte::from_u64(num_bytes.unsigned_abs());
let adj_byte = if let Some(unit) = filesize_unit {
byte.get_adjusted_unit(unit)
} else {
// When filesize_metric is None, format_value should never be "auto", so this
// unwrap_or() should always work.
byte.get_appropriate_unit(if filesize_metric.unwrap_or(false) {
UnitType::Decimal
} else {
UnitType::Binary
})
};
match adj_byte.get_unit() {
byte_unit::Unit::B => {
let locale = get_system_locale();
let locale_byte = adj_byte.get_value() as u64;
let locale_byte_string = locale_byte.to_formatted_string(&locale);
let locale_signed_byte_string = if num_bytes.is_negative() {
format!("-{locale_byte_string}")
} else {
locale_byte_string
};
if filesize_unit.is_none() {
format!("{locale_signed_byte_string} B")
} else {
locale_signed_byte_string
}
}
_ => {
if num_bytes.is_negative() {
format!("-{:.1}", adj_byte)
} else {
format!("{:.1}", adj_byte)
}
}
}
}
/// Get the filesize unit, or None if format is "auto"
fn get_filesize_format(
format_value: &str,
filesize_metric: Option<bool>,
) -> Option<byte_unit::Unit> {
// filesize_metric always overrides the unit of filesize_format.
let metric = filesize_metric.unwrap_or(!format_value.ends_with("ib"));
macro_rules! either {
($metric:ident, $binary:ident) => {
Some(if metric {
byte_unit::Unit::$metric
} else {
byte_unit::Unit::$binary
})
};
}
match format_value {
"b" => Some(byte_unit::Unit::B),
"kb" | "kib" => either!(KB, KiB),
"mb" | "mib" => either!(MB, MiB),
"gb" | "gib" => either!(GB, GiB),
"tb" | "tib" => either!(TB, TiB),
"pb" | "pib" => either!(TB, TiB),
"eb" | "eib" => either!(EB, EiB),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(1000, Some(true), "auto", "1.0 KB")]
#[case(1000, Some(false), "auto", "1,000 B")]
#[case(1000, Some(false), "kb", "1.0 KiB")]
#[case(3000, Some(false), "auto", "2.9 KiB")]
#[case(3_000_000, None, "auto", "2.9 MiB")]
#[case(3_000_000, None, "kib", "2929.7 KiB")]
fn test_filesize(
#[case] val: i64,
#[case] filesize_metric: Option<bool>,
#[case] filesize_format: String,
#[case] exp: &str,
) {
assert_eq!(exp, format_filesize(val, &filesize_format, filesize_metric));
}
}

View File

@ -1,44 +1,40 @@
mod custom_value;
mod duration;
mod filesize;
mod from;
mod from_value;
mod glob;
mod lazy_record;
mod range;
mod stream;
mod unit;
pub mod record;
pub use custom_value::CustomValue;
pub use duration::*;
pub use filesize::*;
pub use from_value::FromValue;
pub use glob::*;
pub use lazy_record::LazyRecord;
pub use range::*;
pub use record::Record;
use crate::ast::{Bits, Boolean, CellPath, Comparison, Math, Operator, PathMember, RangeInclusion};
use crate::engine::{Closure, EngineState};
use crate::{did_you_mean, BlockId, Config, ShellError, Span, Type};
use byte_unit::UnitType;
use chrono::{DateTime, Datelike, Duration, FixedOffset, Locale, TimeZone};
use chrono::{DateTime, Datelike, FixedOffset, Locale, TimeZone};
use chrono_humanize::HumanTime;
pub use custom_value::CustomValue;
use fancy_regex::Regex;
pub use from_value::FromValue;
pub use glob::*;
pub use lazy_record::LazyRecord;
use nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR;
use nu_utils::{
contains_emoji, get_system_locale, locale::get_system_locale_string, IgnoreCaseExt,
};
use num_format::ToFormattedString;
pub use range::*;
pub use record::Record;
use nu_utils::{contains_emoji, locale::get_system_locale_string, IgnoreCaseExt};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::{
borrow::Cow,
fmt::{Display, Formatter, Result as FmtResult},
fmt::Display,
path::PathBuf,
{cmp::Ordering, fmt::Debug},
};
pub use stream::*;
pub use unit::*;
/// Core structured values that pass through the pipeline in Nushell.
// NOTE: Please do not reorder these enum cases without thinking through the
@ -3696,6 +3692,8 @@ fn reorder_record_inner(record: &Record) -> (Vec<&String>, Vec<&Value>) {
kv_pairs.into_iter().unzip()
}
// TODO: The name of this function is overly broad with partial compatibility
// Should be replaced by an explicitly named helper on `Type` (take `Any` into account)
fn type_compatible(a: Type, b: Type) -> bool {
if a == b {
return true;
@ -3704,278 +3702,6 @@ fn type_compatible(a: Type, b: Type) -> bool {
matches!((a, b), (Type::Int, Type::Float) | (Type::Float, Type::Int))
}
/// Is the given year a leap year?
#[allow(clippy::nonminimal_bool)]
pub fn is_leap_year(year: i32) -> bool {
(year % 4 == 0) && (year % 100 != 0 || (year % 100 == 0 && year % 400 == 0))
}
#[derive(Clone, Copy)]
pub enum TimePeriod {
Nanos(i64),
Micros(i64),
Millis(i64),
Seconds(i64),
Minutes(i64),
Hours(i64),
Days(i64),
Weeks(i64),
Months(i64),
Years(i64),
}
impl TimePeriod {
pub fn to_text(self) -> Cow<'static, str> {
match self {
Self::Nanos(n) => format!("{n} ns").into(),
Self::Micros(n) => format!("{n} µs").into(),
Self::Millis(n) => format!("{n} ms").into(),
Self::Seconds(n) => format!("{n} sec").into(),
Self::Minutes(n) => format!("{n} min").into(),
Self::Hours(n) => format!("{n} hr").into(),
Self::Days(n) => format!("{n} day").into(),
Self::Weeks(n) => format!("{n} wk").into(),
Self::Months(n) => format!("{n} month").into(),
Self::Years(n) => format!("{n} yr").into(),
}
}
}
impl Display for TimePeriod {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.to_text())
}
}
pub fn format_duration(duration: i64) -> String {
let (sign, periods) = format_duration_as_timeperiod(duration);
let text = periods
.into_iter()
.map(|p| p.to_text().to_string().replace(' ', ""))
.collect::<Vec<String>>();
format!(
"{}{}",
if sign == -1 { "-" } else { "" },
text.join(" ").trim()
)
}
pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
// Current duration doesn't know a date it's based on, weeks is the max time unit it can normalize into.
// Don't guess or estimate how many years or months it might contain.
let (sign, duration) = if duration >= 0 {
(1, duration)
} else {
(-1, -duration)
};
let dur = Duration::nanoseconds(duration);
/// Split this a duration into number of whole weeks and the remainder
fn split_weeks(duration: Duration) -> (Option<i64>, Duration) {
let weeks = duration.num_weeks();
normalize_split(weeks, Duration::try_weeks(weeks), duration)
}
/// Split this a duration into number of whole days and the remainder
fn split_days(duration: Duration) -> (Option<i64>, Duration) {
let days = duration.num_days();
normalize_split(days, Duration::try_days(days), duration)
}
/// Split this a duration into number of whole hours and the remainder
fn split_hours(duration: Duration) -> (Option<i64>, Duration) {
let hours = duration.num_hours();
normalize_split(hours, Duration::try_hours(hours), duration)
}
/// Split this a duration into number of whole minutes and the remainder
fn split_minutes(duration: Duration) -> (Option<i64>, Duration) {
let minutes = duration.num_minutes();
normalize_split(minutes, Duration::try_minutes(minutes), duration)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_seconds(duration: Duration) -> (Option<i64>, Duration) {
let seconds = duration.num_seconds();
normalize_split(seconds, Duration::try_seconds(seconds), duration)
}
/// Split this a duration into number of whole milliseconds and the remainder
fn split_milliseconds(duration: Duration) -> (Option<i64>, Duration) {
let millis = duration.num_milliseconds();
normalize_split(millis, Duration::try_milliseconds(millis), duration)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_microseconds(duration: Duration) -> (Option<i64>, Duration) {
let micros = duration.num_microseconds().unwrap_or_default();
normalize_split(micros, Duration::microseconds(micros), duration)
}
/// Split this a duration into number of whole seconds and the remainder
fn split_nanoseconds(duration: Duration) -> (Option<i64>, Duration) {
let nanos = duration.num_nanoseconds().unwrap_or_default();
normalize_split(nanos, Duration::nanoseconds(nanos), duration)
}
fn normalize_split(
wholes: i64,
wholes_duration: impl Into<Option<Duration>>,
total_duration: Duration,
) -> (Option<i64>, Duration) {
match wholes_duration.into() {
Some(wholes_duration) if wholes != 0 => {
(Some(wholes), total_duration - wholes_duration)
}
_ => (None, total_duration),
}
}
let mut periods = vec![];
let (weeks, remainder) = split_weeks(dur);
if let Some(weeks) = weeks {
periods.push(TimePeriod::Weeks(weeks));
}
let (days, remainder) = split_days(remainder);
if let Some(days) = days {
periods.push(TimePeriod::Days(days));
}
let (hours, remainder) = split_hours(remainder);
if let Some(hours) = hours {
periods.push(TimePeriod::Hours(hours));
}
let (minutes, remainder) = split_minutes(remainder);
if let Some(minutes) = minutes {
periods.push(TimePeriod::Minutes(minutes));
}
let (seconds, remainder) = split_seconds(remainder);
if let Some(seconds) = seconds {
periods.push(TimePeriod::Seconds(seconds));
}
let (millis, remainder) = split_milliseconds(remainder);
if let Some(millis) = millis {
periods.push(TimePeriod::Millis(millis));
}
let (micros, remainder) = split_microseconds(remainder);
if let Some(micros) = micros {
periods.push(TimePeriod::Micros(micros));
}
let (nanos, _remainder) = split_nanoseconds(remainder);
if let Some(nanos) = nanos {
periods.push(TimePeriod::Nanos(nanos));
}
if periods.is_empty() {
periods.push(TimePeriod::Seconds(0));
}
(sign, periods)
}
pub fn format_filesize_from_conf(num_bytes: i64, config: &Config) -> String {
// We need to take into account config.filesize_metric so, if someone asks for KB
// and filesize_metric is false, return KiB
format_filesize(
num_bytes,
config.filesize_format.as_str(),
Some(config.filesize_metric),
)
}
// filesize_metric is explicit when printed a value according to user config;
// other places (such as `format filesize`) don't.
pub fn format_filesize(
num_bytes: i64,
format_value: &str,
filesize_metric: Option<bool>,
) -> String {
// Allow the user to specify how they want their numbers formatted
// When format_value is "auto" or an invalid value, the returned ByteUnit doesn't matter
// and is always B.
let filesize_unit = get_filesize_format(format_value, filesize_metric);
let byte = byte_unit::Byte::from_u64(num_bytes.unsigned_abs());
let adj_byte = if let Some(unit) = filesize_unit {
byte.get_adjusted_unit(unit)
} else {
// When filesize_metric is None, format_value should never be "auto", so this
// unwrap_or() should always work.
byte.get_appropriate_unit(if filesize_metric.unwrap_or(false) {
UnitType::Decimal
} else {
UnitType::Binary
})
};
match adj_byte.get_unit() {
byte_unit::Unit::B => {
let locale = get_system_locale();
let locale_byte = adj_byte.get_value() as u64;
let locale_byte_string = locale_byte.to_formatted_string(&locale);
let locale_signed_byte_string = if num_bytes.is_negative() {
format!("-{locale_byte_string}")
} else {
locale_byte_string
};
if filesize_unit.is_none() {
format!("{locale_signed_byte_string} B")
} else {
locale_signed_byte_string
}
}
_ => {
if num_bytes.is_negative() {
format!("-{:.1}", adj_byte)
} else {
format!("{:.1}", adj_byte)
}
}
}
}
/// Get the filesize unit, or None if format is "auto"
fn get_filesize_format(
format_value: &str,
filesize_metric: Option<bool>,
) -> Option<byte_unit::Unit> {
// filesize_metric always overrides the unit of filesize_format.
let metric = filesize_metric.unwrap_or(!format_value.ends_with("ib"));
macro_rules! either {
($metric:ident, $binary:ident) => {
Some(if metric {
byte_unit::Unit::$metric
} else {
byte_unit::Unit::$binary
})
};
}
match format_value {
"b" => Some(byte_unit::Unit::B),
"kb" | "kib" => either!(KB, KiB),
"mb" | "mib" => either!(MB, MiB),
"gb" | "gib" => either!(GB, GiB),
"tb" | "tib" => either!(TB, TiB),
"pb" | "pib" => either!(TB, TiB),
"eb" | "eib" => either!(EB, EiB),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{Record, Value};
@ -4055,10 +3781,8 @@ mod tests {
mod into_string {
use chrono::{DateTime, FixedOffset};
use rstest::rstest;
use super::*;
use crate::format_filesize;
#[test]
fn test_datetime() {
@ -4087,21 +3811,5 @@ mod tests {
let formatted = string.split(' ').next().unwrap();
assert_eq!("-0316-02-11T06:13:20+00:00", formatted);
}
#[rstest]
#[case(1000, Some(true), "auto", "1.0 KB")]
#[case(1000, Some(false), "auto", "1,000 B")]
#[case(1000, Some(false), "kb", "1.0 KiB")]
#[case(3000, Some(false), "auto", "2.9 KiB")]
#[case(3_000_000, None, "auto", "2.9 MiB")]
#[case(3_000_000, None, "kib", "2929.7 KiB")]
fn test_filesize(
#[case] val: i64,
#[case] filesize_metric: Option<bool>,
#[case] filesize_format: String,
#[case] exp: &str,
) {
assert_eq!(exp, format_filesize(val, &filesize_format, filesize_metric));
}
}
}

View File

@ -1,245 +0,0 @@
use crate::*;
use std::{
fmt::Debug,
sync::{atomic::AtomicBool, Arc},
};
pub struct RawStream {
pub stream: Box<dyn Iterator<Item = Result<Vec<u8>, ShellError>> + Send + 'static>,
pub leftover: Vec<u8>,
pub ctrlc: Option<Arc<AtomicBool>>,
pub is_binary: bool,
pub span: Span,
pub known_size: Option<u64>, // (bytes)
}
impl RawStream {
pub fn new(
stream: Box<dyn Iterator<Item = Result<Vec<u8>, ShellError>> + Send + 'static>,
ctrlc: Option<Arc<AtomicBool>>,
span: Span,
known_size: Option<u64>,
) -> Self {
Self {
stream,
leftover: vec![],
ctrlc,
is_binary: false,
span,
known_size,
}
}
pub fn into_bytes(self) -> Result<Spanned<Vec<u8>>, ShellError> {
let mut output = vec![];
for item in self.stream {
if nu_utils::ctrl_c::was_pressed(&self.ctrlc) {
break;
}
output.extend(item?);
}
Ok(Spanned {
item: output,
span: self.span,
})
}
pub fn into_string(self) -> Result<Spanned<String>, ShellError> {
let mut output = String::new();
let span = self.span;
let ctrlc = &self.ctrlc.clone();
for item in self {
if nu_utils::ctrl_c::was_pressed(ctrlc) {
break;
}
output.push_str(&item?.coerce_into_string()?);
}
Ok(Spanned { item: output, span })
}
pub fn chain(self, stream: RawStream) -> RawStream {
RawStream {
stream: Box::new(self.stream.chain(stream.stream)),
leftover: self.leftover.into_iter().chain(stream.leftover).collect(),
ctrlc: self.ctrlc,
is_binary: self.is_binary,
span: self.span,
known_size: self.known_size,
}
}
pub fn drain(self) -> Result<(), ShellError> {
for next in self {
match next {
Ok(val) => {
if let Value::Error { error, .. } = val {
return Err(*error);
}
}
Err(err) => return Err(err),
}
}
Ok(())
}
}
impl Debug for RawStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawStream").finish()
}
}
impl Iterator for RawStream {
type Item = Result<Value, ShellError>;
fn next(&mut self) -> Option<Self::Item> {
if nu_utils::ctrl_c::was_pressed(&self.ctrlc) {
return None;
}
// If we know we're already binary, just output that
if self.is_binary {
self.stream.next().map(|buffer| {
buffer.map(|mut v| {
if !self.leftover.is_empty() {
for b in self.leftover.drain(..).rev() {
v.insert(0, b);
}
}
Value::binary(v, self.span)
})
})
} else {
// We *may* be text. We're only going to try utf-8. Other decodings
// needs to be taken as binary first, then passed through `decode`.
if let Some(buffer) = self.stream.next() {
match buffer {
Ok(mut v) => {
if !self.leftover.is_empty() {
while let Some(b) = self.leftover.pop() {
v.insert(0, b);
}
}
match String::from_utf8(v.clone()) {
Ok(s) => {
// Great, we have a complete string, let's output it
Some(Ok(Value::string(s, self.span)))
}
Err(err) => {
// Okay, we *might* have a string but we've also got some errors
if v.is_empty() {
// We can just end here
None
} else if v.len() > 3
&& (v.len() - err.utf8_error().valid_up_to() > 3)
{
// As UTF-8 characters are max 4 bytes, if we have more than that in error we know
// that it's not just a character spanning two frames.
// We now know we are definitely binary, so switch to binary and stay there.
self.is_binary = true;
Some(Ok(Value::binary(v, self.span)))
} else {
// Okay, we have a tiny bit of error at the end of the buffer. This could very well be
// a character that spans two frames. Since this is the case, remove the error from
// the current frame an dput it in the leftover buffer.
self.leftover = v[err.utf8_error().valid_up_to()..].to_vec();
let buf = v[0..err.utf8_error().valid_up_to()].to_vec();
match String::from_utf8(buf) {
Ok(s) => Some(Ok(Value::string(s, self.span))),
Err(_) => {
// Something is definitely wrong. Switch to binary, and stay there
self.is_binary = true;
Some(Ok(Value::binary(v, self.span)))
}
}
}
}
}
}
Err(e) => Some(Err(e)),
}
} else if !self.leftover.is_empty() {
let output = Ok(Value::binary(self.leftover.clone(), self.span));
self.leftover.clear();
Some(output)
} else {
None
}
}
}
}
/// A potentially infinite stream of values, optionally with a mean to send a Ctrl-C signal to stop
/// the stream from continuing.
///
/// In practice, a "stream" here means anything which can be iterated and produce Values as it iterates.
/// Like other iterators in Rust, observing values from this stream will drain the items as you view them
/// and the stream cannot be replayed.
pub struct ListStream {
pub stream: Box<dyn Iterator<Item = Value> + Send + 'static>,
pub ctrlc: Option<Arc<AtomicBool>>,
first_guard: bool,
}
impl ListStream {
pub fn into_string(self, separator: &str, config: &Config) -> String {
self.map(|x: Value| x.to_expanded_string(", ", config))
.collect::<Vec<String>>()
.join(separator)
}
pub fn drain(self) -> Result<(), ShellError> {
for next in self {
if let Value::Error { error, .. } = next {
return Err(*error);
}
}
Ok(())
}
pub fn from_stream(
input: impl Iterator<Item = Value> + Send + 'static,
ctrlc: Option<Arc<AtomicBool>>,
) -> ListStream {
ListStream {
stream: Box::new(input),
ctrlc,
first_guard: true,
}
}
}
impl Debug for ListStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ListStream").finish()
}
}
impl Iterator for ListStream {
type Item = Value;
fn next(&mut self) -> Option<Self::Item> {
// We need to check `first_guard` to guarantee that it always have something to return in
// underlying stream.
//
// A realworld example is running an external commands, which have an `exit_code`
// ListStream.
// When we press ctrl-c, the external command receives the signal too, if we don't have
// `first_guard`, the `exit_code` ListStream will return Nothing, which is not expected
if self.first_guard {
self.first_guard = false;
return self.stream.next();
}
if nu_utils::ctrl_c::was_pressed(&self.ctrlc) {
None
} else {
self.stream.next()
}
}
}

View File

@ -1,110 +0,0 @@
use crate::{ShellError, Span, Value};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Unit {
// Filesize units: metric
Byte,
Kilobyte,
Megabyte,
Gigabyte,
Terabyte,
Petabyte,
Exabyte,
// Filesize units: ISO/IEC 80000
Kibibyte,
Mebibyte,
Gibibyte,
Tebibyte,
Pebibyte,
Exbibyte,
// Duration units
Nanosecond,
Microsecond,
Millisecond,
Second,
Minute,
Hour,
Day,
Week,
}
impl Unit {
pub fn to_value(&self, size: i64, span: Span) -> Result<Value, ShellError> {
match self {
Unit::Byte => Ok(Value::filesize(size, span)),
Unit::Kilobyte => Ok(Value::filesize(size * 1000, span)),
Unit::Megabyte => Ok(Value::filesize(size * 1000 * 1000, span)),
Unit::Gigabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000, span)),
Unit::Terabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000 * 1000, span)),
Unit::Petabyte => Ok(Value::filesize(
size * 1000 * 1000 * 1000 * 1000 * 1000,
span,
)),
Unit::Exabyte => Ok(Value::filesize(
size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
span,
)),
Unit::Kibibyte => Ok(Value::filesize(size * 1024, span)),
Unit::Mebibyte => Ok(Value::filesize(size * 1024 * 1024, span)),
Unit::Gibibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024, span)),
Unit::Tebibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024 * 1024, span)),
Unit::Pebibyte => Ok(Value::filesize(
size * 1024 * 1024 * 1024 * 1024 * 1024,
span,
)),
Unit::Exbibyte => Ok(Value::filesize(
size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
span,
)),
Unit::Nanosecond => Ok(Value::duration(size, span)),
Unit::Microsecond => Ok(Value::duration(size * 1000, span)),
Unit::Millisecond => Ok(Value::duration(size * 1000 * 1000, span)),
Unit::Second => Ok(Value::duration(size * 1000 * 1000 * 1000, span)),
Unit::Minute => match size.checked_mul(1000 * 1000 * 1000 * 60) {
Some(val) => Ok(Value::duration(val, span)),
None => Err(ShellError::GenericError {
error: "duration too large".into(),
msg: "duration too large".into(),
span: Some(span),
help: None,
inner: vec![],
}),
},
Unit::Hour => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60) {
Some(val) => Ok(Value::duration(val, span)),
None => Err(ShellError::GenericError {
error: "duration too large".into(),
msg: "duration too large".into(),
span: Some(span),
help: None,
inner: vec![],
}),
},
Unit::Day => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24) {
Some(val) => Ok(Value::duration(val, span)),
None => Err(ShellError::GenericError {
error: "duration too large".into(),
msg: "duration too large".into(),
span: Some(span),
help: None,
inner: vec![],
}),
},
Unit::Week => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 7) {
Some(val) => Ok(Value::duration(val, span)),
None => Err(ShellError::GenericError {
error: "duration too large".into(),
msg: "duration too large".into(),
span: Some(span),
help: None,
inner: vec![],
}),
},
}
}
}