Merge remote-tracking branch 'upstream' into sort-custom
This commit is contained in:
commit
dfd618133a
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
|
@ -10,4 +10,4 @@ jobs:
|
|||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Check spelling
|
||||
uses: crate-ci/typos@v1.22.4
|
||||
uses: crate-ci/typos@v1.22.7
|
||||
|
|
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -920,6 +920,15 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
@ -1678,9 +1687,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
|||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
|
||||
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"libc",
|
||||
|
@ -2279,9 +2288,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.16.2+1.7.2"
|
||||
version = "0.17.0+1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
|
||||
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -3020,6 +3029,17 @@ dependencies = [
|
|||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-derive-value"
|
||||
version = "0.94.3"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-engine"
|
||||
version = "0.94.3"
|
||||
|
@ -3209,11 +3229,13 @@ dependencies = [
|
|||
"byte-unit",
|
||||
"chrono",
|
||||
"chrono-humanize",
|
||||
"convert_case",
|
||||
"fancy-regex",
|
||||
"indexmap",
|
||||
"lru",
|
||||
"miette",
|
||||
"nix",
|
||||
"nu-derive-value",
|
||||
"nu-path",
|
||||
"nu-system",
|
||||
"nu-test-support",
|
||||
|
|
|
@ -39,6 +39,7 @@ members = [
|
|||
"crates/nu-lsp",
|
||||
"crates/nu-pretty-hex",
|
||||
"crates/nu-protocol",
|
||||
"crates/nu-derive-value",
|
||||
"crates/nu-plugin",
|
||||
"crates/nu-plugin-core",
|
||||
"crates/nu-plugin-engine",
|
||||
|
@ -74,6 +75,7 @@ chardetng = "0.1.17"
|
|||
chrono = { default-features = false, version = "0.4.34" }
|
||||
chrono-humanize = "0.2.3"
|
||||
chrono-tz = "0.8"
|
||||
convert_case = "0.6"
|
||||
crossbeam-channel = "0.5.8"
|
||||
crossterm = "0.27"
|
||||
csv = "1.3"
|
||||
|
@ -123,11 +125,14 @@ pathdiff = "0.2"
|
|||
percent-encoding = "2"
|
||||
pretty_assertions = "1.4"
|
||||
print-positions = "0.6"
|
||||
proc-macro-error = { version = "1.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
procfs = "0.16.0"
|
||||
pwd = "1.3"
|
||||
quick-xml = "0.31.0"
|
||||
quickcheck = "1.0"
|
||||
quickcheck_macros = "1.0"
|
||||
quote = "1.0"
|
||||
rand = "0.8"
|
||||
ratatui = "0.26"
|
||||
rayon = "1.10"
|
||||
|
@ -147,6 +152,7 @@ serde_urlencoded = "0.7.1"
|
|||
serde_yaml = "0.9"
|
||||
sha2 = "0.10"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
syn = "2.0"
|
||||
sysinfo = "0.30"
|
||||
tabled = { version = "0.14.0", default-features = false }
|
||||
tempfile = "3.10"
|
||||
|
|
|
@ -222,6 +222,7 @@ Please submit an issue or PR to be added to this list.
|
|||
- [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell)
|
||||
- [Dorothy](http://github.com/bevry/dorothy)
|
||||
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
|
||||
- [x-cmd](https://x-cmd.com/mod/nu)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -47,8 +47,7 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
|
|||
&mut engine,
|
||||
&mut stack,
|
||||
PipelineData::empty(),
|
||||
None,
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -90,8 +89,7 @@ fn bench_command(
|
|||
&mut engine,
|
||||
&mut stack,
|
||||
PipelineData::empty(),
|
||||
None,
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
|
|
@ -8,15 +8,45 @@ use nu_protocol::{
|
|||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EvaluateCommandsOpts {
|
||||
pub table_mode: Option<Value>,
|
||||
pub error_style: Option<Value>,
|
||||
pub no_newline: bool,
|
||||
}
|
||||
|
||||
/// Run a command (or commands) given to us by the user
|
||||
pub fn evaluate_commands(
|
||||
commands: &Spanned<String>,
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
input: PipelineData,
|
||||
table_mode: Option<Value>,
|
||||
no_newline: bool,
|
||||
opts: EvaluateCommandsOpts,
|
||||
) -> Result<(), ShellError> {
|
||||
let EvaluateCommandsOpts {
|
||||
table_mode,
|
||||
error_style,
|
||||
no_newline,
|
||||
} = opts;
|
||||
|
||||
// Handle the configured error style early
|
||||
if let Some(e_style) = error_style {
|
||||
match e_style.coerce_str()?.parse() {
|
||||
Ok(e_style) => {
|
||||
Arc::make_mut(&mut engine_state.config).error_style = e_style;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(ShellError::GenericError {
|
||||
error: "Invalid value for `--error-style`".into(),
|
||||
msg: err.into(),
|
||||
span: Some(e_style.span()),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Translate environment variables from Strings to Values
|
||||
convert_env_values(engine_state, stack)?;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ mod validation;
|
|||
pub use commands::add_cli_context;
|
||||
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
|
||||
pub use config_files::eval_config_contents;
|
||||
pub use eval_cmds::evaluate_commands;
|
||||
pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts};
|
||||
pub use eval_file::evaluate_file;
|
||||
pub use menus::NuHelpCompleter;
|
||||
pub use nu_cmd_base::util::get_init_cwd;
|
||||
|
|
|
@ -194,7 +194,7 @@ pub fn eval_hook(
|
|||
let Some(follow) = val.get("code") else {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: "code".into(),
|
||||
span,
|
||||
span: Some(span),
|
||||
src_span: span,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -20,13 +20,14 @@ pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf
|
|||
|
||||
type MakeRangeError = fn(&str, Span) -> ShellError;
|
||||
|
||||
/// Returns a inclusive pair of boundary in given `range`.
|
||||
pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> {
|
||||
match range {
|
||||
Range::IntRange(range) => {
|
||||
let start = range.start().try_into().unwrap_or(0);
|
||||
let end = match range.end() {
|
||||
Bound::Included(v) => (v + 1) as isize,
|
||||
Bound::Excluded(v) => v as isize,
|
||||
Bound::Included(v) => v as isize,
|
||||
Bound::Excluded(v) => (v - 1) as isize,
|
||||
Bound::Unbounded => isize::MAX,
|
||||
};
|
||||
Ok((start, end))
|
||||
|
|
|
@ -10,7 +10,7 @@ impl Command for Try {
|
|||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Try to run a block, if it fails optionally run a catch block."
|
||||
"Try to run a block, if it fails optionally run a catch closure."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
|
@ -18,7 +18,7 @@ impl Command for Try {
|
|||
.input_output_types(vec![(Type::Any, Type::Any)])
|
||||
.required("try_block", SyntaxShape::Block, "Block to run.")
|
||||
.optional(
|
||||
"catch_block",
|
||||
"catch_closure",
|
||||
SyntaxShape::Keyword(
|
||||
b"catch".to_vec(),
|
||||
Box::new(SyntaxShape::OneOf(vec![
|
||||
|
@ -26,7 +26,7 @@ impl Command for Try {
|
|||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||
])),
|
||||
),
|
||||
"Block to run if try block fails.",
|
||||
"Closure to run if try block fails.",
|
||||
)
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
@ -85,9 +85,14 @@ impl Command for Try {
|
|||
},
|
||||
Example {
|
||||
description: "Try to run a missing command",
|
||||
example: "try { asdfasdf } catch { 'missing' } ",
|
||||
example: "try { asdfasdf } catch { 'missing' }",
|
||||
result: Some(Value::test_string("missing")),
|
||||
},
|
||||
Example {
|
||||
description: "Try to run a missing command and report the message",
|
||||
example: "try { asdfasdf } catch { |err| $err.msg }",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,48 +128,24 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||
let range = &args.indexes;
|
||||
match input {
|
||||
Value::Binary { val, .. } => {
|
||||
use std::cmp::{self, Ordering};
|
||||
let len = val.len() as isize;
|
||||
|
||||
let start = if range.0 < 0 { range.0 + len } else { range.0 };
|
||||
let end = if range.1 < 0 { range.1 + len } else { range.1 };
|
||||
|
||||
let end = if range.1 < 0 {
|
||||
cmp::max(range.1 + len, 0)
|
||||
} else {
|
||||
range.1
|
||||
};
|
||||
|
||||
if start < len && end >= 0 {
|
||||
match start.cmp(&end) {
|
||||
Ordering::Equal => Value::binary(vec![], head),
|
||||
Ordering::Greater => Value::error(
|
||||
ShellError::TypeMismatch {
|
||||
err_message: "End must be greater than or equal to Start".to_string(),
|
||||
span: head,
|
||||
},
|
||||
head,
|
||||
),
|
||||
Ordering::Less => Value::binary(
|
||||
if end == isize::MAX {
|
||||
val.iter()
|
||||
.skip(start as usize)
|
||||
.copied()
|
||||
.collect::<Vec<u8>>()
|
||||
} else {
|
||||
val.iter()
|
||||
.skip(start as usize)
|
||||
.take((end - start) as usize)
|
||||
.copied()
|
||||
.collect()
|
||||
},
|
||||
head,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
if start > end {
|
||||
Value::binary(vec![], head)
|
||||
} else {
|
||||
let val_iter = val.iter().skip(start as usize);
|
||||
Value::binary(
|
||||
if end == isize::MAX {
|
||||
val_iter.copied().collect::<Vec<u8>>()
|
||||
} else {
|
||||
val_iter.take((end - start + 1) as usize).copied().collect()
|
||||
},
|
||||
head,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Value::Error { .. } => input.clone(),
|
||||
|
||||
other => Value::error(
|
||||
|
|
|
@ -194,7 +194,7 @@ fn run_histogram(
|
|||
if inputs.is_empty() {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: head_span,
|
||||
span: Some(head_span),
|
||||
src_span: list_span,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -154,7 +154,7 @@ fn record_to_path_member(
|
|||
let Some(value) = record.get("value") else {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: "value".into(),
|
||||
span: val_span,
|
||||
span: Some(val_span),
|
||||
src_span: span,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -130,7 +130,7 @@ pub fn split(
|
|||
Some(group_key) => Ok(group_key.coerce_string()?),
|
||||
None => Err(ShellError::CantFindColumn {
|
||||
col_name: column_name.item.to_string(),
|
||||
span: column_name.span,
|
||||
span: Some(column_name.span),
|
||||
src_span: row.span(),
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellEr
|
|||
if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: nonexistent,
|
||||
span,
|
||||
span: Some(span),
|
||||
src_span: val_span,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ use nu_cmd_base::{
|
|||
};
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::{engine::StateWorkingSet, Range};
|
||||
use std::cmp::Ordering;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -151,6 +150,11 @@ impl Command for SubCommand {
|
|||
example: " '🇯🇵ほげ ふが ぴよ' | str substring --grapheme-clusters 4..5",
|
||||
result: Some(Value::test_string("ふが")),
|
||||
},
|
||||
Example {
|
||||
description: "sub string by negative index",
|
||||
example: " 'good nushell' | str substring 5..-2",
|
||||
result: Some(Value::test_string("nushel")),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -167,22 +171,15 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||
options.0
|
||||
};
|
||||
let end: isize = if options.1 < 0 {
|
||||
std::cmp::max(len + options.1, 0)
|
||||
options.1 + len
|
||||
} else {
|
||||
options.1
|
||||
};
|
||||
|
||||
if start < len && end >= 0 {
|
||||
match start.cmp(&end) {
|
||||
Ordering::Equal => Value::string("", head),
|
||||
Ordering::Greater => Value::error(
|
||||
ShellError::TypeMismatch {
|
||||
err_message: "End must be greater than or equal to Start".to_string(),
|
||||
span: head,
|
||||
},
|
||||
head,
|
||||
),
|
||||
Ordering::Less => Value::string(
|
||||
if start > end {
|
||||
Value::string("", head)
|
||||
} else {
|
||||
Value::string(
|
||||
{
|
||||
if end == isize::MAX {
|
||||
if args.graphemes {
|
||||
|
@ -199,24 +196,21 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
|
|||
} else if args.graphemes {
|
||||
s.graphemes(true)
|
||||
.skip(start as usize)
|
||||
.take((end - start) as usize)
|
||||
.take((end - start + 1) as usize)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("")
|
||||
} else {
|
||||
String::from_utf8_lossy(
|
||||
&s.bytes()
|
||||
.skip(start as usize)
|
||||
.take((end - start) as usize)
|
||||
.take((end - start + 1) as usize)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
},
|
||||
head,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Value::string("", head)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Propagate errors by explicitly matching them before the final case.
|
||||
|
@ -243,6 +237,7 @@ mod tests {
|
|||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Expectation<'a> {
|
||||
options: (isize, isize),
|
||||
expected: &'a str,
|
||||
|
@ -266,18 +261,19 @@ mod tests {
|
|||
let word = Value::test_string("andres");
|
||||
|
||||
let cases = vec![
|
||||
expectation("a", (0, 1)),
|
||||
expectation("an", (0, 2)),
|
||||
expectation("and", (0, 3)),
|
||||
expectation("andr", (0, 4)),
|
||||
expectation("andre", (0, 5)),
|
||||
expectation("a", (0, 0)),
|
||||
expectation("an", (0, 1)),
|
||||
expectation("and", (0, 2)),
|
||||
expectation("andr", (0, 3)),
|
||||
expectation("andre", (0, 4)),
|
||||
expectation("andres", (0, 5)),
|
||||
expectation("andres", (0, 6)),
|
||||
expectation("", (0, -6)),
|
||||
expectation("a", (0, -5)),
|
||||
expectation("an", (0, -4)),
|
||||
expectation("and", (0, -3)),
|
||||
expectation("andr", (0, -2)),
|
||||
expectation("andre", (0, -1)),
|
||||
expectation("a", (0, -6)),
|
||||
expectation("an", (0, -5)),
|
||||
expectation("and", (0, -4)),
|
||||
expectation("andr", (0, -3)),
|
||||
expectation("andre", (0, -2)),
|
||||
expectation("andres", (0, -1)),
|
||||
// str substring [ -4 , _ ]
|
||||
// str substring -4 ,
|
||||
expectation("dres", (-4, isize::MAX)),
|
||||
|
@ -292,6 +288,7 @@ mod tests {
|
|||
];
|
||||
|
||||
for expectation in &cases {
|
||||
println!("{:?}", expectation);
|
||||
let expected = expectation.expected;
|
||||
let actual = action(
|
||||
&word,
|
||||
|
|
|
@ -13,7 +13,7 @@ impl Command for SysUsers {
|
|||
fn signature(&self) -> Signature {
|
||||
Signature::build("sys users")
|
||||
.category(Category::System)
|
||||
.input_output_types(vec![(Type::Nothing, Type::record())])
|
||||
.input_output_types(vec![(Type::Nothing, Type::table())])
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
|
|
|
@ -170,7 +170,10 @@ impl Command for Table {
|
|||
}),
|
||||
Value::test_record(record! {
|
||||
"a" => Value::test_int(3),
|
||||
"b" => Value::test_int(4),
|
||||
"b" => Value::test_list(vec![
|
||||
Value::test_int(4),
|
||||
Value::test_int(4),
|
||||
])
|
||||
}),
|
||||
])),
|
||||
},
|
||||
|
@ -184,7 +187,10 @@ impl Command for Table {
|
|||
}),
|
||||
Value::test_record(record! {
|
||||
"a" => Value::test_int(3),
|
||||
"b" => Value::test_int(4),
|
||||
"b" => Value::test_list(vec![
|
||||
Value::test_int(4),
|
||||
Value::test_int(4),
|
||||
])
|
||||
}),
|
||||
])),
|
||||
},
|
||||
|
|
|
@ -31,12 +31,12 @@ fn detect_columns_with_legacy_and_flag_c() {
|
|||
(
|
||||
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
|
||||
"[[c1,c3,c4,c5]; ['a b',c,d,e]]",
|
||||
"0..0",
|
||||
"0..1",
|
||||
),
|
||||
(
|
||||
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
|
||||
"[[c1,c2,c3,c4]; [a,b,c,'d e']]",
|
||||
"(-2)..(-2)",
|
||||
"(-2)..(-1)",
|
||||
),
|
||||
(
|
||||
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
|
||||
|
@ -72,10 +72,10 @@ drwxr-xr-x 2 root root 4.0K Mar 20 08:28 =(char nl)
|
|||
drwxr-xr-x 4 root root 4.0K Mar 20 08:18 ~(char nl)
|
||||
-rw-r--r-- 1 root root 3.0K Mar 20 07:23 ~asdf(char nl)\"";
|
||||
let expected = "[
|
||||
['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column8'];
|
||||
['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20 08:28', '='],
|
||||
['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20 08:18', '~'],
|
||||
['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20 07:23', '~asdf']
|
||||
['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8'];
|
||||
['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20', '08:28', '='],
|
||||
['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20', '08:18', '~'],
|
||||
['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20', '07:23', '~asdf']
|
||||
]";
|
||||
let range = "5..6";
|
||||
let cmd = format!(
|
||||
|
|
|
@ -4,8 +4,9 @@ use nu_test_support::nu;
|
|||
fn error_label_works() {
|
||||
let actual = nu!("error make {msg:foo label:{text:unseen}}");
|
||||
|
||||
assert!(actual.err.contains("unseen"));
|
||||
assert!(actual.err.contains("╰──"));
|
||||
assert!(actual
|
||||
.err
|
||||
.contains("label at line 1, columns 1 to 10: unseen"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -255,7 +255,7 @@ fn substrings_the_input() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn substring_errors_if_start_index_is_greater_than_end_index() {
|
||||
fn substring_empty_if_start_index_is_greater_than_end_index() {
|
||||
Playground::setup("str_test_9", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent(
|
||||
"sample.toml",
|
||||
|
@ -270,12 +270,10 @@ fn substring_errors_if_start_index_is_greater_than_end_index() {
|
|||
r#"
|
||||
open sample.toml
|
||||
| str substring 6..4 fortune.teller.phone
|
||||
| get fortune.teller.phone
|
||||
"#
|
||||
));
|
||||
|
||||
assert!(actual
|
||||
.err
|
||||
.contains("End must be greater than or equal to Start"))
|
||||
assert_eq!(actual.out, "")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -375,6 +373,21 @@ fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given(
|
|||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn substring_by_negative_index() {
|
||||
Playground::setup("str_test_13", |dirs, _| {
|
||||
let actual = nu!(
|
||||
cwd: dirs.test(), "'apples' | str substring 0..-1",
|
||||
);
|
||||
assert_eq!(actual.out, "apples");
|
||||
|
||||
let actual = nu!(
|
||||
cwd: dirs.test(), "'apples' | str substring 0..<-1",
|
||||
);
|
||||
assert_eq!(actual.out, "apple");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_reverse() {
|
||||
let actual = nu!(r#"
|
||||
|
|
21
crates/nu-derive-value/Cargo.toml
Normal file
21
crates/nu-derive-value/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Macros implementation of #[derive(FromValue, IntoValue)]"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-derive-value"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value"
|
||||
version = "0.94.3"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
# we can only use exposed macros in doctests really,
|
||||
# so we cannot test anything useful in a doctest
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
proc-macro-error = { workspace = true }
|
||||
convert_case = { workspace = true }
|
21
crates/nu-derive-value/LICENSE
Normal file
21
crates/nu-derive-value/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2023 The Nushell Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
116
crates/nu-derive-value/src/attributes.rs
Normal file
116
crates/nu-derive-value/src/attributes.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use convert_case::Case;
|
||||
use syn::{spanned::Spanned, Attribute, Fields, LitStr};
|
||||
|
||||
use crate::{error::DeriveError, HELPER_ATTRIBUTE};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ContainerAttributes {
|
||||
pub rename_all: Case,
|
||||
}
|
||||
|
||||
impl Default for ContainerAttributes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rename_all: Case::Snake,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContainerAttributes {
|
||||
pub fn parse_attrs<'a, M>(
|
||||
iter: impl Iterator<Item = &'a Attribute>,
|
||||
) -> Result<Self, DeriveError<M>> {
|
||||
let mut container_attrs = ContainerAttributes::default();
|
||||
for attr in filter(iter) {
|
||||
// This is a container to allow returning derive errors inside the parse_nested_meta fn.
|
||||
let mut err = Ok(());
|
||||
|
||||
attr.parse_nested_meta(|meta| {
|
||||
let ident = meta.path.require_ident()?;
|
||||
match ident.to_string().as_str() {
|
||||
"rename_all" => {
|
||||
// The matched case are all useful variants from `convert_case` with aliases
|
||||
// that `serde` uses.
|
||||
let case: LitStr = meta.value()?.parse()?;
|
||||
let case = match case.value().as_str() {
|
||||
"UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper,
|
||||
"lower case" | "lower with spaces case" => Case::Lower,
|
||||
"Title Case" => Case::Title,
|
||||
"camelCase" => Case::Camel,
|
||||
"PascalCase" | "UpperCamelCase" => Case::Pascal,
|
||||
"snake_case" => Case::Snake,
|
||||
"UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => Case::UpperSnake,
|
||||
"kebab-case" => Case::Kebab,
|
||||
"COBOL-CASE" | "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => {
|
||||
Case::Cobol
|
||||
}
|
||||
"Train-Case" => Case::Train,
|
||||
"flatcase" | "lowercase" => Case::Flat,
|
||||
"UPPERFLATCASE" | "UPPERCASE" => Case::UpperFlat,
|
||||
// Although very funny, we don't support `Case::{Toggle, Alternating}`,
|
||||
// as we see no real benefit.
|
||||
c => {
|
||||
err = Err(DeriveError::InvalidAttributeValue {
|
||||
value_span: case.span(),
|
||||
value: Box::new(c.to_string()),
|
||||
});
|
||||
return Ok(()); // We stored the err in `err`.
|
||||
}
|
||||
};
|
||||
container_attrs.rename_all = case;
|
||||
}
|
||||
ident => {
|
||||
err = Err(DeriveError::UnexpectedAttribute {
|
||||
meta_span: ident.span(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(DeriveError::Syn)?;
|
||||
|
||||
err?; // Shortcircuit here if `err` is holding some error.
|
||||
}
|
||||
|
||||
Ok(container_attrs)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter<'a>(
|
||||
iter: impl Iterator<Item = &'a Attribute>,
|
||||
) -> impl Iterator<Item = &'a Attribute> {
|
||||
iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE))
|
||||
}
|
||||
|
||||
// The deny functions are built to easily deny the use of the helper attribute if used incorrectly.
|
||||
// As the usage of it gets more complex, these functions might be discarded or replaced.
|
||||
|
||||
/// Deny any attribute that uses the helper attribute.
|
||||
pub fn deny<M>(attrs: &[Attribute]) -> Result<(), DeriveError<M>> {
|
||||
match filter(attrs.iter()).next() {
|
||||
Some(attr) => Err(DeriveError::InvalidAttributePosition {
|
||||
attribute_span: attr.span(),
|
||||
}),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deny any attributes that uses the helper attribute on any field.
|
||||
pub fn deny_fields<M>(fields: &Fields) -> Result<(), DeriveError<M>> {
|
||||
match fields {
|
||||
Fields::Named(fields) => {
|
||||
for field in fields.named.iter() {
|
||||
deny(&field.attrs)?;
|
||||
}
|
||||
}
|
||||
Fields::Unnamed(fields) => {
|
||||
for field in fields.unnamed.iter() {
|
||||
deny(&field.attrs)?;
|
||||
}
|
||||
}
|
||||
Fields::Unit => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
82
crates/nu-derive-value/src/error.rs
Normal file
82
crates/nu-derive-value/src/error.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::{any, fmt::Debug, marker::PhantomData};
|
||||
|
||||
use proc_macro2::Span;
|
||||
use proc_macro_error::{Diagnostic, Level};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DeriveError<M> {
|
||||
/// Marker variant, makes the `M` generic parameter valid.
|
||||
_Marker(PhantomData<M>),
|
||||
|
||||
/// Parsing errors thrown by `syn`.
|
||||
Syn(syn::parse::Error),
|
||||
|
||||
/// `syn::DeriveInput` was a union, currently not supported
|
||||
UnsupportedUnions,
|
||||
|
||||
/// Only plain enums are supported right now.
|
||||
UnsupportedEnums { fields_span: Span },
|
||||
|
||||
/// Found a `#[nu_value(x)]` attribute where `x` is unexpected.
|
||||
UnexpectedAttribute { meta_span: Span },
|
||||
|
||||
/// Found a `#[nu_value(x)]` attribute at a invalid position.
|
||||
InvalidAttributePosition { attribute_span: Span },
|
||||
|
||||
/// Found a valid `#[nu_value(x)]` attribute but the passed values is invalid.
|
||||
InvalidAttributeValue {
|
||||
value_span: Span,
|
||||
value: Box<dyn Debug>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<M> From<DeriveError<M>> for Diagnostic {
|
||||
fn from(value: DeriveError<M>) -> Self {
|
||||
let derive_name = any::type_name::<M>().split("::").last().expect("not empty");
|
||||
match value {
|
||||
DeriveError::_Marker(_) => panic!("used marker variant"),
|
||||
|
||||
DeriveError::Syn(e) => Diagnostic::spanned(e.span(), Level::Error, e.to_string()),
|
||||
|
||||
DeriveError::UnsupportedUnions => Diagnostic::new(
|
||||
Level::Error,
|
||||
format!("`{derive_name}` cannot be derived from unions"),
|
||||
)
|
||||
.help("consider refactoring to a struct".to_string())
|
||||
.note("if you really need a union, consider opening an issue on Github".to_string()),
|
||||
|
||||
DeriveError::UnsupportedEnums { fields_span } => Diagnostic::spanned(
|
||||
fields_span,
|
||||
Level::Error,
|
||||
format!("`{derive_name}` can only be derived from plain enums"),
|
||||
)
|
||||
.help(
|
||||
"consider refactoring your data type to a struct with a plain enum as a field"
|
||||
.to_string(),
|
||||
)
|
||||
.note("more complex enums could be implemented in the future".to_string()),
|
||||
|
||||
DeriveError::InvalidAttributePosition { attribute_span } => Diagnostic::spanned(
|
||||
attribute_span,
|
||||
Level::Error,
|
||||
"invalid attribute position".to_string(),
|
||||
)
|
||||
.help(format!(
|
||||
"check documentation for `{derive_name}` for valid placements"
|
||||
)),
|
||||
|
||||
DeriveError::UnexpectedAttribute { meta_span } => {
|
||||
Diagnostic::spanned(meta_span, Level::Error, "unknown attribute".to_string()).help(
|
||||
format!("check documentation for `{derive_name}` for valid attributes"),
|
||||
)
|
||||
}
|
||||
|
||||
DeriveError::InvalidAttributeValue { value_span, value } => {
|
||||
Diagnostic::spanned(value_span, Level::Error, format!("invalid value {value:?}"))
|
||||
.help(format!(
|
||||
"check documentation for `{derive_name}` for valid attribute values"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
539
crates/nu-derive-value/src/from.rs
Normal file
539
crates/nu-derive-value/src/from.rs
Normal file
|
@ -0,0 +1,539 @@
|
|||
use convert_case::Casing;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
|
||||
};
|
||||
|
||||
use crate::attributes::{self, ContainerAttributes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FromValue;
|
||||
type DeriveError = super::error::DeriveError<FromValue>;
|
||||
|
||||
/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums.
|
||||
///
|
||||
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
|
||||
///
|
||||
/// This function directs the `FromValue` trait derivation to the correct implementation based on
|
||||
/// the input type:
|
||||
/// - For structs: [`derive_struct_from_value`]
|
||||
/// - For enums: [`derive_enum_from_value`]
|
||||
/// - Unions are not supported and will return an error.
|
||||
pub fn derive_from_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
|
||||
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
|
||||
match input.data {
|
||||
Data::Struct(data_struct) => Ok(derive_struct_from_value(
|
||||
input.ident,
|
||||
data_struct,
|
||||
input.generics,
|
||||
input.attrs,
|
||||
)?),
|
||||
Data::Enum(data_enum) => Ok(derive_enum_from_value(
|
||||
input.ident,
|
||||
data_enum,
|
||||
input.generics,
|
||||
input.attrs,
|
||||
)?),
|
||||
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements the `#[derive(FromValue)]` macro for structs.
|
||||
///
|
||||
/// This function ensures that the helper attribute is not used anywhere, as it is not supported for
|
||||
/// structs.
|
||||
/// Other than this, this function provides the impl signature for `FromValue`.
|
||||
/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the
|
||||
/// `FromValue::expected_type` is handled by [`struct_expected_type`].
|
||||
fn derive_struct_from_value(
|
||||
ident: Ident,
|
||||
data: DataStruct,
|
||||
generics: Generics,
|
||||
attrs: Vec<Attribute>,
|
||||
) -> Result<TokenStream2, DeriveError> {
|
||||
attributes::deny(&attrs)?;
|
||||
attributes::deny_fields(&data.fields)?;
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
let from_value_impl = struct_from_value(&data);
|
||||
let expected_type_impl = struct_expected_type(&data.fields);
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
|
||||
#from_value_impl
|
||||
#expected_type_impl
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Implements `FromValue::from_value` for structs.
|
||||
///
|
||||
/// This function constructs the `from_value` function for structs.
|
||||
/// The implementation is straightforward as most of the heavy lifting is handled by
|
||||
/// `parse_value_via_fields`, and this function only needs to construct the signature around it.
|
||||
///
|
||||
/// For structs with named fields, this constructs a large return type where each field
|
||||
/// contains the implementation for that specific field.
|
||||
/// In structs with unnamed fields, a [`VecDeque`](std::collections::VecDeque) is used to load each
|
||||
/// field one after another, and the result is used to construct the tuple.
|
||||
/// For unit structs, this only checks if the input value is `Value::Nothing`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// These examples show what the macro would generate.
|
||||
///
|
||||
/// Struct with named fields:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Pet {
|
||||
/// name: String,
|
||||
/// age: u8,
|
||||
/// favorite_toy: Option<String>,
|
||||
/// }
|
||||
///
|
||||
/// impl nu_protocol::FromValue for Pet {
|
||||
/// fn from_value(
|
||||
/// v: nu_protocol::Value
|
||||
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
|
||||
/// let span = v.span();
|
||||
/// let mut record = v.into_record()?;
|
||||
/// std::result::Result::Ok(Pet {
|
||||
/// name: <String as nu_protocol::FromValue>::from_value(
|
||||
/// record
|
||||
/// .remove("name")
|
||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
/// col_name: std::string::ToString::to_string("name"),
|
||||
/// span: std::option::Option::None,
|
||||
/// src_span: span
|
||||
/// })?,
|
||||
/// )?,
|
||||
/// age: <u8 as nu_protocol::FromValue>::from_value(
|
||||
/// record
|
||||
/// .remove("age")
|
||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
/// col_name: std::string::ToString::to_string("age"),
|
||||
/// span: std::option::Option::None,
|
||||
/// src_span: span
|
||||
/// })?,
|
||||
/// )?,
|
||||
/// favorite_toy: <Option<String> as nu_protocol::FromValue>::from_value(
|
||||
/// record
|
||||
/// .remove("favorite_toy")
|
||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
/// col_name: std::string::ToString::to_string("favorite_toy"),
|
||||
/// span: std::option::Option::None,
|
||||
/// src_span: span
|
||||
/// })?,
|
||||
/// )?,
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Struct with unnamed fields:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Color(u8, u8, u8);
|
||||
///
|
||||
/// impl nu_protocol::FromValue for Color {
|
||||
/// fn from_value(
|
||||
/// v: nu_protocol::Value
|
||||
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
|
||||
/// let span = v.span();
|
||||
/// let list = v.into_list()?;
|
||||
/// let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
|
||||
/// std::result::Result::Ok(Self(
|
||||
/// {
|
||||
/// <u8 as nu_protocol::FromValue>::from_value(
|
||||
/// deque
|
||||
/// .pop_front()
|
||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
/// col_name: std::string::ToString::to_string(&0),
|
||||
/// span: std::option::Option::None,
|
||||
/// src_span: span
|
||||
/// })?,
|
||||
/// )?
|
||||
/// },
|
||||
/// {
|
||||
/// <u8 as nu_protocol::FromValue>::from_value(
|
||||
/// deque
|
||||
/// .pop_front()
|
||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
/// col_name: std::string::ToString::to_string(&1),
|
||||
/// span: std::option::Option::None,
|
||||
/// src_span: span
|
||||
/// })?,
|
||||
/// )?
|
||||
/// },
|
||||
/// {
|
||||
/// <u8 as nu_protocol::FromValue>::from_value(
|
||||
/// deque
|
||||
/// .pop_front()
|
||||
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
/// col_name: std::string::ToString::to_string(&2),
|
||||
/// span: std::option::Option::None,
|
||||
/// src_span: span
|
||||
/// })?,
|
||||
/// )?
|
||||
/// }
|
||||
/// ))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Unit struct:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Unicorn;
|
||||
///
|
||||
/// impl nu_protocol::FromValue for Unicorn {
|
||||
/// fn from_value(
|
||||
/// v: nu_protocol::Value
|
||||
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
|
||||
/// match v {
|
||||
/// nu_protocol::Value::Nothing {..} => Ok(Self),
|
||||
/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
|
||||
/// to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::expected_type()),
|
||||
/// from_type: std::string::ToString::to_string(&v.get_type()),
|
||||
/// span: v.span(),
|
||||
/// help: std::option::Option::None
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn struct_from_value(data: &DataStruct) -> TokenStream2 {
|
||||
let body = parse_value_via_fields(&data.fields, quote!(Self));
|
||||
quote! {
|
||||
fn from_value(
|
||||
v: nu_protocol::Value
|
||||
) -> std::result::Result<Self, nu_protocol::ShellError> {
|
||||
#body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `FromValue::expected_type` for structs.
|
||||
///
|
||||
/// This function constructs the `expected_type` function for structs.
|
||||
/// The type depends on the `fields`: named fields construct a record type with every key and type
|
||||
/// laid out.
|
||||
/// Unnamed fields construct a custom type with the name in the format like
|
||||
/// `list[type0, type1, type2]`.
|
||||
/// No fields expect the `Type::Nothing`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// These examples show what the macro would generate.
|
||||
///
|
||||
/// Struct with named fields:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Pet {
|
||||
/// name: String,
|
||||
/// age: u8,
|
||||
/// favorite_toy: Option<String>,
|
||||
/// }
|
||||
///
|
||||
/// impl nu_protocol::FromValue for Pet {
|
||||
/// fn expected_type() -> nu_protocol::Type {
|
||||
/// nu_protocol::Type::Record(
|
||||
/// std::vec![
|
||||
/// (
|
||||
/// std::string::ToString::to_string("name"),
|
||||
/// <String as nu_protocol::FromValue>::expected_type(),
|
||||
/// ),
|
||||
/// (
|
||||
/// std::string::ToString::to_string("age"),
|
||||
/// <u8 as nu_protocol::FromValue>::expected_type(),
|
||||
/// ),
|
||||
/// (
|
||||
/// std::string::ToString::to_string("favorite_toy"),
|
||||
/// <Option<String> as nu_protocol::FromValue>::expected_type(),
|
||||
/// )
|
||||
/// ].into_boxed_slice()
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Struct with unnamed fields:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Color(u8, u8, u8);
|
||||
///
|
||||
/// impl nu_protocol::FromValue for Color {
|
||||
/// fn expected_type() -> nu_protocol::Type {
|
||||
/// nu_protocol::Type::Custom(
|
||||
/// std::format!(
|
||||
/// "[{}, {}, {}]",
|
||||
/// <u8 as nu_protocol::FromValue>::expected_type(),
|
||||
/// <u8 as nu_protocol::FromValue>::expected_type(),
|
||||
/// <u8 as nu_protocol::FromValue>::expected_type()
|
||||
/// )
|
||||
/// .into_boxed_str()
|
||||
/// )
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Unit struct:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Unicorn;
|
||||
///
|
||||
/// impl nu_protocol::FromValue for Color {
|
||||
/// fn expected_type() -> nu_protocol::Type {
|
||||
/// nu_protocol::Type::Nothing
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn struct_expected_type(fields: &Fields) -> TokenStream2 {
|
||||
let ty = match fields {
|
||||
Fields::Named(fields) => {
|
||||
let fields = fields.named.iter().map(|field| {
|
||||
let ident = field.ident.as_ref().expect("named has idents");
|
||||
let ident_s = ident.to_string();
|
||||
let ty = &field.ty;
|
||||
quote! {(
|
||||
std::string::ToString::to_string(#ident_s),
|
||||
<#ty as nu_protocol::FromValue>::expected_type(),
|
||||
)}
|
||||
});
|
||||
quote!(nu_protocol::Type::Record(
|
||||
std::vec![#(#fields),*].into_boxed_slice()
|
||||
))
|
||||
}
|
||||
Fields::Unnamed(fields) => {
|
||||
let mut iter = fields.unnamed.iter();
|
||||
let fields = fields.unnamed.iter().map(|field| {
|
||||
let ty = &field.ty;
|
||||
quote!(<#ty as nu_protocol::FromValue>::expected_type())
|
||||
});
|
||||
let mut template = String::new();
|
||||
template.push('[');
|
||||
if iter.next().is_some() {
|
||||
template.push_str("{}")
|
||||
}
|
||||
iter.for_each(|_| template.push_str(", {}"));
|
||||
template.push(']');
|
||||
quote! {
|
||||
nu_protocol::Type::Custom(
|
||||
std::format!(
|
||||
#template,
|
||||
#(#fields),*
|
||||
)
|
||||
.into_boxed_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
Fields::Unit => quote!(nu_protocol::Type::Nothing),
|
||||
};
|
||||
|
||||
quote! {
|
||||
fn expected_type() -> nu_protocol::Type {
|
||||
#ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements the `#[derive(FromValue)]` macro for enums.
|
||||
///
|
||||
/// This function constructs the implementation of the `FromValue` trait for enums.
|
||||
/// It is designed to be on the same level as [`derive_struct_from_value`], even though this
|
||||
/// function only provides the impl signature for `FromValue`.
|
||||
/// The actual implementation for `FromValue::from_value` is handled by [`enum_from_value`].
|
||||
///
|
||||
/// Since variants are difficult to type with the current type system, this function uses the
|
||||
/// default implementation for `expected_type`.
|
||||
fn derive_enum_from_value(
|
||||
ident: Ident,
|
||||
data: DataEnum,
|
||||
generics: Generics,
|
||||
attrs: Vec<Attribute>,
|
||||
) -> Result<TokenStream2, DeriveError> {
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
let from_value_impl = enum_from_value(&data, &attrs)?;
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
|
||||
#from_value_impl
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Implements `FromValue::from_value` for enums.
|
||||
///
|
||||
/// This function constructs the `from_value` implementation for enums.
|
||||
/// It only accepts enums with unit variants, as it is currently unclear how other types of enums
|
||||
/// should be represented via a `Value`.
|
||||
/// This function checks that every field is a unit variant and constructs a match statement over
|
||||
/// all possible variants.
|
||||
/// The input value is expected to be a `Value::String` containing the name of the variant formatted
|
||||
/// as defined by the `#[nu_value(rename_all = "...")]` attribute.
|
||||
/// If no attribute is given, [`convert_case::Case::Snake`] is expected.
|
||||
///
|
||||
/// If no matching variant is found, `ShellError::CantConvert` is returned.
|
||||
///
|
||||
/// This is how such a derived implementation looks:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// enum Weather {
|
||||
/// Sunny,
|
||||
/// Cloudy,
|
||||
/// Raining
|
||||
/// }
|
||||
///
|
||||
/// impl nu_protocol::IntoValue for Weather {
|
||||
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
/// let span = v.span();
|
||||
/// let ty = v.get_type();
|
||||
///
|
||||
/// let s = v.into_string()?;
|
||||
/// match s.as_str() {
|
||||
/// "sunny" => std::result::Ok(Self::Sunny),
|
||||
/// "cloudy" => std::result::Ok(Self::Cloudy),
|
||||
/// "raining" => std::result::Ok(Self::Raining),
|
||||
/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
|
||||
/// to_type: std::string::ToString::to_string(
|
||||
/// &<Self as nu_protocol::FromValue>::expected_type()
|
||||
/// ),
|
||||
/// from_type: std::string::ToString::to_string(&ty),
|
||||
/// span: span,help: std::option::Option::None,
|
||||
/// }),
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result<TokenStream2, DeriveError> {
|
||||
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
|
||||
let arms: Vec<TokenStream2> = data
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
attributes::deny(&variant.attrs)?;
|
||||
let ident = &variant.ident;
|
||||
let ident_s = format!("{ident}")
|
||||
.as_str()
|
||||
.to_case(container_attrs.rename_all);
|
||||
match &variant.fields {
|
||||
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
|
||||
fields_span: fields.span(),
|
||||
}),
|
||||
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
|
||||
fields_span: fields.span(),
|
||||
}),
|
||||
Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))),
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
fn from_value(
|
||||
v: nu_protocol::Value
|
||||
) -> std::result::Result<Self, nu_protocol::ShellError> {
|
||||
let span = v.span();
|
||||
let ty = v.get_type();
|
||||
|
||||
let s = v.into_string()?;
|
||||
match s.as_str() {
|
||||
#(#arms,)*
|
||||
_ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
|
||||
to_type: std::string::ToString::to_string(
|
||||
&<Self as nu_protocol::FromValue>::expected_type()
|
||||
),
|
||||
from_type: std::string::ToString::to_string(&ty),
|
||||
span: span,
|
||||
help: std::option::Option::None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a `Value` into self.
|
||||
///
|
||||
/// This function handles the actual parsing of a `Value` into self.
|
||||
/// It takes two parameters: `fields` and `self_ident`.
|
||||
/// The `fields` parameter determines the expected type of `Value`: named fields expect a
|
||||
/// `Value::Record`, unnamed fields expect a `Value::List`, and a unit expects `Value::Nothing`.
|
||||
///
|
||||
/// For named fields, the `fields` parameter indicates which field in the record corresponds to
|
||||
/// which struct field.
|
||||
/// For both named and unnamed fields, it also helps cast the type into a `FromValue` type.
|
||||
/// This approach maintains
|
||||
/// [hygiene](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene).
|
||||
///
|
||||
/// The `self_ident` parameter is used to describe the identifier of the returned value.
|
||||
/// For structs, `Self` is usually sufficient, but for enums, `Self::Variant` may be needed in the
|
||||
/// future.
|
||||
///
|
||||
/// This function is more complex than the equivalent for `IntoValue` due to error handling
|
||||
/// requirements.
|
||||
/// For missing fields, `ShellError::CantFindColumn` is used, and for unit structs,
|
||||
/// `ShellError::CantConvert` is used.
|
||||
/// The implementation avoids local variables for fields to prevent accidental shadowing, ensuring
|
||||
/// that poorly named fields don't cause issues.
|
||||
/// While this style is not typically recommended in handwritten Rust, it is acceptable for code
|
||||
/// generation.
|
||||
fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenStream2 {
|
||||
match fields {
|
||||
Fields::Named(fields) => {
|
||||
let fields = fields.named.iter().map(|field| {
|
||||
// TODO: handle missing fields for Options as None
|
||||
let ident = field.ident.as_ref().expect("named has idents");
|
||||
let ident_s = ident.to_string();
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
#ident: <#ty as nu_protocol::FromValue>::from_value(
|
||||
record
|
||||
.remove(#ident_s)
|
||||
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
col_name: std::string::ToString::to_string(#ident_s),
|
||||
span: std::option::Option::None,
|
||||
src_span: span
|
||||
})?,
|
||||
)?
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
let span = v.span();
|
||||
let mut record = v.into_record()?;
|
||||
std::result::Result::Ok(#self_ident {#(#fields),*})
|
||||
}
|
||||
}
|
||||
Fields::Unnamed(fields) => {
|
||||
let fields = fields.unnamed.iter().enumerate().map(|(i, field)| {
|
||||
let ty = &field.ty;
|
||||
quote! {{
|
||||
<#ty as nu_protocol::FromValue>::from_value(
|
||||
deque
|
||||
.pop_front()
|
||||
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
|
||||
col_name: std::string::ToString::to_string(&#i),
|
||||
span: std::option::Option::None,
|
||||
src_span: span
|
||||
})?,
|
||||
)?
|
||||
}}
|
||||
});
|
||||
quote! {
|
||||
let span = v.span();
|
||||
let list = v.into_list()?;
|
||||
let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
|
||||
std::result::Result::Ok(#self_ident(#(#fields),*))
|
||||
}
|
||||
}
|
||||
Fields::Unit => quote! {
|
||||
match v {
|
||||
nu_protocol::Value::Nothing {..} => Ok(#self_ident),
|
||||
v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
|
||||
to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::expected_type()),
|
||||
from_type: std::string::ToString::to_string(&v.get_type()),
|
||||
span: v.span(),
|
||||
help: std::option::Option::None
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
266
crates/nu-derive-value/src/into.rs
Normal file
266
crates/nu-derive-value/src/into.rs
Normal file
|
@ -0,0 +1,266 @@
|
|||
use convert_case::Casing;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
|
||||
Index,
|
||||
};
|
||||
|
||||
use crate::attributes::{self, ContainerAttributes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IntoValue;
|
||||
type DeriveError = super::error::DeriveError<IntoValue>;
|
||||
|
||||
/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums.
|
||||
///
|
||||
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
|
||||
///
|
||||
/// This function directs the `IntoValue` trait derivation to the correct implementation based on
|
||||
/// the input type:
|
||||
/// - For structs: [`struct_into_value`]
|
||||
/// - For enums: [`enum_into_value`]
|
||||
/// - Unions are not supported and will return an error.
|
||||
pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
|
||||
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
|
||||
match input.data {
|
||||
Data::Struct(data_struct) => Ok(struct_into_value(
|
||||
input.ident,
|
||||
data_struct,
|
||||
input.generics,
|
||||
input.attrs,
|
||||
)?),
|
||||
Data::Enum(data_enum) => Ok(enum_into_value(
|
||||
input.ident,
|
||||
data_enum,
|
||||
input.generics,
|
||||
input.attrs,
|
||||
)?),
|
||||
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements the `#[derive(IntoValue)]` macro for structs.
|
||||
///
|
||||
/// Automatically derives the `IntoValue` trait for any struct where each field implements
|
||||
/// `IntoValue`.
|
||||
/// For structs with named fields, the derived implementation creates a `Value::Record` using the
|
||||
/// struct fields as keys.
|
||||
/// Each field value is converted using the `IntoValue::into_value` method.
|
||||
/// For structs with unnamed fields, this generates a `Value::List` with each field in the list.
|
||||
/// For unit structs, this generates `Value::Nothing`, because there is no data.
|
||||
///
|
||||
/// Note: The helper attribute `#[nu_value(...)]` is currently not allowed on structs.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// These examples show what the macro would generate.
|
||||
///
|
||||
/// Struct with named fields:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Pet {
|
||||
/// name: String,
|
||||
/// age: u8,
|
||||
/// favorite_toy: Option<String>,
|
||||
/// }
|
||||
///
|
||||
/// impl nu_protocol::IntoValue for Pet {
|
||||
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
/// nu_protocol::Value::record(nu_protocol::record! {
|
||||
/// "name" => nu_protocol::IntoValue::into_value(self.name, span),
|
||||
/// "age" => nu_protocol::IntoValue::into_value(self.age, span),
|
||||
/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span),
|
||||
/// }, span)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Struct with unnamed fields:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Color(u8, u8, u8);
|
||||
///
|
||||
/// impl nu_protocol::IntoValue for Color {
|
||||
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
/// nu_protocol::Value::list(vec![
|
||||
/// nu_protocol::IntoValue::into_value(self.0, span),
|
||||
/// nu_protocol::IntoValue::into_value(self.1, span),
|
||||
/// nu_protocol::IntoValue::into_value(self.2, span),
|
||||
/// ], span)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Unit struct:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// struct Unicorn;
|
||||
///
|
||||
/// impl nu_protocol::IntoValue for Unicorn {
|
||||
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
/// nu_protocol::Value::nothing(span)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn struct_into_value(
|
||||
ident: Ident,
|
||||
data: DataStruct,
|
||||
generics: Generics,
|
||||
attrs: Vec<Attribute>,
|
||||
) -> Result<TokenStream2, DeriveError> {
|
||||
attributes::deny(&attrs)?;
|
||||
attributes::deny_fields(&data.fields)?;
|
||||
let record = match &data.fields {
|
||||
Fields::Named(fields) => {
|
||||
let accessor = fields
|
||||
.named
|
||||
.iter()
|
||||
.map(|field| field.ident.as_ref().expect("named has idents"))
|
||||
.map(|ident| quote!(self.#ident));
|
||||
fields_return_value(&data.fields, accessor)
|
||||
}
|
||||
Fields::Unnamed(fields) => {
|
||||
let accessor = fields
|
||||
.unnamed
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(n, _)| Index::from(n))
|
||||
.map(|index| quote!(self.#index));
|
||||
fields_return_value(&data.fields, accessor)
|
||||
}
|
||||
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
|
||||
};
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
|
||||
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
#record
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Implements the `#[derive(IntoValue)]` macro for enums.
|
||||
///
|
||||
/// This function implements the derive macro `IntoValue` for enums.
|
||||
/// Currently, only unit enum variants are supported as it is not clear how other types of enums
|
||||
/// should be represented in a `Value`.
|
||||
/// For simple enums, we represent the enum as a `Value::String`. For other types of variants, we return an error.
|
||||
/// The variant name will be case-converted as described by the `#[nu_value(rename_all = "...")]` helper attribute.
|
||||
/// If no attribute is used, the default is `case_convert::Case::Snake`.
|
||||
/// The implementation matches over all variants, uses the appropriate variant name, and constructs a `Value::String`.
|
||||
///
|
||||
/// This is how such a derived implementation looks:
|
||||
/// ```rust
|
||||
/// #[derive(IntoValue)]
|
||||
/// enum Weather {
|
||||
/// Sunny,
|
||||
/// Cloudy,
|
||||
/// Raining
|
||||
/// }
|
||||
///
|
||||
/// impl nu_protocol::IntoValue for Weather {
|
||||
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
/// match self {
|
||||
/// Self::Sunny => nu_protocol::Value::string("sunny", span),
|
||||
/// Self::Cloudy => nu_protocol::Value::string("cloudy", span),
|
||||
/// Self::Raining => nu_protocol::Value::string("raining", span),
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn enum_into_value(
|
||||
ident: Ident,
|
||||
data: DataEnum,
|
||||
generics: Generics,
|
||||
attrs: Vec<Attribute>,
|
||||
) -> Result<TokenStream2, DeriveError> {
|
||||
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
|
||||
let arms: Vec<TokenStream2> = data
|
||||
.variants
|
||||
.into_iter()
|
||||
.map(|variant| {
|
||||
attributes::deny(&variant.attrs)?;
|
||||
let ident = variant.ident;
|
||||
let ident_s = format!("{ident}")
|
||||
.as_str()
|
||||
.to_case(container_attrs.rename_all);
|
||||
match &variant.fields {
|
||||
// In the future we can implement more complexe enums here.
|
||||
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
|
||||
fields_span: fields.span(),
|
||||
}),
|
||||
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
|
||||
fields_span: fields.span(),
|
||||
}),
|
||||
Fields::Unit => {
|
||||
Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span)))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
Ok(quote! {
|
||||
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
|
||||
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
match self {
|
||||
#(#arms,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Constructs the final `Value` that the macro generates.
|
||||
///
|
||||
/// This function handles the construction of the final `Value` that the macro generates.
|
||||
/// It is currently only used for structs but may be used for enums in the future.
|
||||
/// The function takes two parameters: the `fields`, which allow iterating over each field of a data
|
||||
/// type, and the `accessor`.
|
||||
/// The fields determine whether we need to generate a `Value::Record`, `Value::List`, or
|
||||
/// `Value::Nothing`.
|
||||
/// For named fields, they are also directly used to generate the record key.
|
||||
///
|
||||
/// The `accessor` parameter generalizes how the data is accessed.
|
||||
/// For named fields, this is usually the name of the fields preceded by `self` in a struct, and
|
||||
/// maybe something else for enums.
|
||||
/// For unnamed fields, this should be an iterator similar to the one with named fields, but
|
||||
/// accessing tuple fields, so we get `self.n`.
|
||||
/// For unit structs, this parameter is ignored.
|
||||
/// By using the accessor like this, we can have the same code for structs and enums with data
|
||||
/// variants in the future.
|
||||
fn fields_return_value(
|
||||
fields: &Fields,
|
||||
accessor: impl Iterator<Item = impl ToTokens>,
|
||||
) -> TokenStream2 {
|
||||
match fields {
|
||||
Fields::Named(fields) => {
|
||||
let items: Vec<TokenStream2> = fields
|
||||
.named
|
||||
.iter()
|
||||
.zip(accessor)
|
||||
.map(|(field, accessor)| {
|
||||
let ident = field.ident.as_ref().expect("named has idents");
|
||||
let field = ident.to_string();
|
||||
quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))
|
||||
})
|
||||
.collect();
|
||||
quote! {
|
||||
nu_protocol::Value::record(nu_protocol::record! {
|
||||
#(#items),*
|
||||
}, span)
|
||||
}
|
||||
}
|
||||
Fields::Unnamed(fields) => {
|
||||
let items =
|
||||
fields.unnamed.iter().zip(accessor).map(
|
||||
|(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)),
|
||||
);
|
||||
quote!(nu_protocol::Value::list(std::vec![#(#items),*], span))
|
||||
}
|
||||
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
|
||||
}
|
||||
}
|
69
crates/nu-derive-value/src/lib.rs
Normal file
69
crates/nu-derive-value/src/lib.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
//! Macro implementations of `#[derive(FromValue, IntoValue)]`.
|
||||
//!
|
||||
//! As this crate is a [`proc_macro`] crate, it is only allowed to export
|
||||
//! [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html).
|
||||
//! Therefore, it only exports [`IntoValue`] and [`FromValue`].
|
||||
//!
|
||||
//! To get documentation for other functions and types used in this crate, run
|
||||
//! `cargo doc -p nu-derive-value --document-private-items`.
|
||||
//!
|
||||
//! This crate uses a lot of
|
||||
//! [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html)
|
||||
//! as `TokenStream2` to allow testing the behavior of the macros directly, including the output
|
||||
//! token stream or if the macro errors as expected.
|
||||
//! The tests for functionality can be found in `nu_protocol::value::test_derive`.
|
||||
//!
|
||||
//! This documentation is often less reference-heavy than typical Rust documentation.
|
||||
//! This is because this crate is a dependency for `nu_protocol`, and linking to it would create a
|
||||
//! cyclic dependency.
|
||||
//! Also all examples in the documentation aren't tested as this crate cannot be compiled as a
|
||||
//! normal library very easily.
|
||||
//! This might change in the future if cargo allows building a proc-macro crate differently for
|
||||
//! `cfg(doctest)` as they are already doing for `cfg(test)`.
|
||||
//!
|
||||
//! The generated code from the derive macros tries to be as
|
||||
//! [hygienic](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene) as possible.
|
||||
//! This ensures that the macro can be called anywhere without requiring specific imports.
|
||||
//! This results in obtuse code, which isn't recommended for manual, handwritten Rust
|
||||
//! but ensures that no other code may influence this generated code or vice versa.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use proc_macro_error::{proc_macro_error, Diagnostic};
|
||||
|
||||
mod attributes;
|
||||
mod error;
|
||||
mod from;
|
||||
mod into;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
const HELPER_ATTRIBUTE: &str = "nu_value";
|
||||
|
||||
/// Derive macro generating an impl of the trait `IntoValue`.
|
||||
///
|
||||
/// For further information, see the docs on the trait itself.
|
||||
#[proc_macro_derive(IntoValue, attributes(nu_value))]
|
||||
#[proc_macro_error]
|
||||
pub fn derive_into_value(input: TokenStream) -> TokenStream {
|
||||
let input = TokenStream2::from(input);
|
||||
let output = match into::derive_into_value(input) {
|
||||
Ok(output) => output,
|
||||
Err(e) => Diagnostic::from(e).abort(),
|
||||
};
|
||||
TokenStream::from(output)
|
||||
}
|
||||
|
||||
/// Derive macro generating an impl of the trait `FromValue`.
|
||||
///
|
||||
/// For further information, see the docs on the trait itself.
|
||||
#[proc_macro_derive(FromValue, attributes(nu_value))]
|
||||
#[proc_macro_error]
|
||||
pub fn derive_from_value(input: TokenStream) -> TokenStream {
|
||||
let input = TokenStream2::from(input);
|
||||
let output = match from::derive_from_value(input) {
|
||||
Ok(output) => output,
|
||||
Err(e) => Diagnostic::from(e).abort(),
|
||||
};
|
||||
TokenStream::from(output)
|
||||
}
|
157
crates/nu-derive-value/src/tests.rs
Normal file
157
crates/nu-derive-value/src/tests.rs
Normal file
|
@ -0,0 +1,157 @@
|
|||
// These tests only check that the derive macros throw the relevant errors.
|
||||
// Functionality of the derived types is tested in nu_protocol::value::test_derive.
|
||||
|
||||
use crate::error::DeriveError;
|
||||
use crate::from::derive_from_value;
|
||||
use crate::into::derive_into_value;
|
||||
use quote::quote;
|
||||
|
||||
#[test]
|
||||
fn unsupported_unions() {
|
||||
let input = quote! {
|
||||
#[nu_value]
|
||||
union SomeUnion {
|
||||
f1: u32,
|
||||
f2: f32,
|
||||
}
|
||||
};
|
||||
|
||||
let from_res = derive_from_value(input.clone());
|
||||
assert!(
|
||||
matches!(from_res, Err(DeriveError::UnsupportedUnions)),
|
||||
"expected `DeriveError::UnsupportedUnions`, got {:?}",
|
||||
from_res
|
||||
);
|
||||
|
||||
let into_res = derive_into_value(input);
|
||||
assert!(
|
||||
matches!(into_res, Err(DeriveError::UnsupportedUnions)),
|
||||
"expected `DeriveError::UnsupportedUnions`, got {:?}",
|
||||
into_res
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_enums() {
|
||||
let input = quote! {
|
||||
#[nu_value(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
enum ComplexEnum {
|
||||
Unit,
|
||||
Unnamed(u32, f32),
|
||||
Named {
|
||||
u: u32,
|
||||
f: f32,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let from_res = derive_from_value(input.clone());
|
||||
assert!(
|
||||
matches!(from_res, Err(DeriveError::UnsupportedEnums { .. })),
|
||||
"expected `DeriveError::UnsupportedEnums`, got {:?}",
|
||||
from_res
|
||||
);
|
||||
|
||||
let into_res = derive_into_value(input);
|
||||
assert!(
|
||||
matches!(into_res, Err(DeriveError::UnsupportedEnums { .. })),
|
||||
"expected `DeriveError::UnsupportedEnums`, got {:?}",
|
||||
into_res
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unexpected_attribute() {
|
||||
let input = quote! {
|
||||
#[nu_value(what)]
|
||||
enum SimpleEnum {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
};
|
||||
|
||||
let from_res = derive_from_value(input.clone());
|
||||
assert!(
|
||||
matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })),
|
||||
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
|
||||
from_res
|
||||
);
|
||||
|
||||
let into_res = derive_into_value(input);
|
||||
assert!(
|
||||
matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })),
|
||||
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
|
||||
into_res
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deny_attribute_on_structs() {
|
||||
let input = quote! {
|
||||
#[nu_value]
|
||||
struct SomeStruct;
|
||||
};
|
||||
|
||||
let from_res = derive_from_value(input.clone());
|
||||
assert!(
|
||||
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
|
||||
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
|
||||
from_res
|
||||
);
|
||||
|
||||
let into_res = derive_into_value(input);
|
||||
assert!(
|
||||
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
|
||||
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
|
||||
into_res
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deny_attribute_on_fields() {
|
||||
let input = quote! {
|
||||
struct SomeStruct {
|
||||
#[nu_value]
|
||||
field: ()
|
||||
}
|
||||
};
|
||||
|
||||
let from_res = derive_from_value(input.clone());
|
||||
assert!(
|
||||
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
|
||||
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
|
||||
from_res
|
||||
);
|
||||
|
||||
let into_res = derive_into_value(input);
|
||||
assert!(
|
||||
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
|
||||
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
|
||||
into_res
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_attribute_value() {
|
||||
let input = quote! {
|
||||
#[nu_value(rename_all = "CrazY-CasE")]
|
||||
enum SimpleEnum {
|
||||
A,
|
||||
B
|
||||
}
|
||||
};
|
||||
|
||||
let from_res = derive_from_value(input.clone());
|
||||
assert!(
|
||||
matches!(from_res, Err(DeriveError::InvalidAttributeValue { .. })),
|
||||
"expected `DeriveError::InvalidAttributeValue`, got {:?}",
|
||||
from_res
|
||||
);
|
||||
|
||||
let into_res = derive_into_value(input);
|
||||
assert!(
|
||||
matches!(into_res, Err(DeriveError::InvalidAttributeValue { .. })),
|
||||
"expected `DeriveError::InvalidAttributeValue`, got {:?}",
|
||||
into_res
|
||||
);
|
||||
}
|
|
@ -3,7 +3,7 @@ use nu_protocol::{
|
|||
ast::{Argument, Call, Expr, Expression, RecordItem},
|
||||
debugger::WithoutDebug,
|
||||
engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
|
||||
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId,
|
||||
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, Spanned,
|
||||
SyntaxShape, Type, Value,
|
||||
};
|
||||
use std::{collections::HashMap, fmt::Write};
|
||||
|
@ -296,6 +296,28 @@ fn get_documentation(
|
|||
}
|
||||
|
||||
if let Some(result) = &example.result {
|
||||
let mut table_call = Call::new(Span::unknown());
|
||||
if example.example.ends_with("--collapse") {
|
||||
// collapse the result
|
||||
table_call.add_named((
|
||||
Spanned {
|
||||
item: "collapse".to_string(),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
// expand the result
|
||||
table_call.add_named((
|
||||
Spanned {
|
||||
item: "expand".to_string(),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
))
|
||||
}
|
||||
let table = engine_state
|
||||
.find_decl("table".as_bytes(), &[])
|
||||
.and_then(|decl_id| {
|
||||
|
@ -304,7 +326,7 @@ fn get_documentation(
|
|||
.run(
|
||||
engine_state,
|
||||
stack,
|
||||
&Call::new(Span::new(0, 0)),
|
||||
&table_call,
|
||||
PipelineData::Value(result.clone(), None),
|
||||
)
|
||||
.ok()
|
||||
|
|
|
@ -16,11 +16,13 @@ bench = false
|
|||
nu-utils = { path = "../nu-utils", version = "0.94.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.94.3" }
|
||||
nu-system = { path = "../nu-system", version = "0.94.3" }
|
||||
nu-derive-value = { path = "../nu-derive-value", version = "0.94.3" }
|
||||
|
||||
brotli = { workspace = true, optional = true }
|
||||
byte-unit = { version = "5.1", features = [ "serde" ] }
|
||||
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
|
||||
chrono-humanize = { workspace = true }
|
||||
convert_case = { workspace = true }
|
||||
fancy-regex = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
|
|
|
@ -557,12 +557,12 @@ pub enum ShellError {
|
|||
/// ## Resolution
|
||||
///
|
||||
/// Check the spelling of your column name. Did you forget to rename a column somewhere?
|
||||
#[error("Cannot find column")]
|
||||
#[error("Cannot find column '{col_name}'")]
|
||||
#[diagnostic(code(nu::shell::column_not_found))]
|
||||
CantFindColumn {
|
||||
col_name: String,
|
||||
#[label = "cannot find column '{col_name}'"]
|
||||
span: Span,
|
||||
span: Option<Span>,
|
||||
#[label = "value originates here"]
|
||||
src_span: Span,
|
||||
},
|
||||
|
|
|
@ -39,3 +39,5 @@ pub use span::*;
|
|||
pub use syntax_shape::*;
|
||||
pub use ty::*;
|
||||
pub use value::*;
|
||||
|
||||
pub use nu_derive_value::*;
|
||||
|
|
|
@ -1,37 +1,188 @@
|
|||
use crate::{
|
||||
ast::{CellPath, PathMember},
|
||||
engine::Closure,
|
||||
NuGlob, Range, Record, ShellError, Spanned, Value,
|
||||
NuGlob, Range, Record, ShellError, Spanned, Type, Value,
|
||||
};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
any,
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, VecDeque},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// A trait for loading a value from a [`Value`].
|
||||
///
|
||||
/// # Derivable
|
||||
/// This trait can be used with `#[derive]`.
|
||||
/// When derived on structs with named fields, it expects a [`Value::Record`] where each field of
|
||||
/// the struct maps to a corresponding field in the record.
|
||||
/// For structs with unnamed fields, it expects a [`Value::List`], and the fields are populated in
|
||||
/// the order they appear in the list.
|
||||
/// Unit structs expect a [`Value::Nothing`], as they contain no data.
|
||||
/// Attempting to convert from a non-matching `Value` type will result in an error.
|
||||
///
|
||||
/// Only enums with no fields may derive this trait.
|
||||
/// The expected value representation will be the name of the variant as a [`Value::String`].
|
||||
/// By default, variant names will be expected in ["snake_case"](convert_case::Case::Snake).
|
||||
/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum.
|
||||
/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported
|
||||
/// by specifying the case name followed by "case".
|
||||
/// Also all values for
|
||||
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid
|
||||
/// here.
|
||||
///
|
||||
/// ```
|
||||
/// # use nu_protocol::{FromValue, Value, ShellError};
|
||||
/// #[derive(FromValue, Debug, PartialEq)]
|
||||
/// #[nu_value(rename_all = "COBOL-CASE")]
|
||||
/// enum Bird {
|
||||
/// MountainEagle,
|
||||
/// ForestOwl,
|
||||
/// RiverDuck,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(),
|
||||
/// Bird::RiverDuck
|
||||
/// );
|
||||
/// ```
|
||||
pub trait FromValue: Sized {
|
||||
// TODO: instead of ShellError, maybe we could have a FromValueError that implements Into<ShellError>
|
||||
/// Loads a value from a [`Value`].
|
||||
///
|
||||
/// This method retrieves a value similarly to how strings are parsed using [`FromStr`].
|
||||
/// The operation might fail if the `Value` contains unexpected types or structures.
|
||||
fn from_value(v: Value) -> Result<Self, ShellError>;
|
||||
}
|
||||
|
||||
impl FromValue for Value {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
Ok(v)
|
||||
/// Expected `Value` type.
|
||||
///
|
||||
/// This is used to print out errors of what type of value is expected for conversion.
|
||||
/// Even if not used in [`from_value`](FromValue::from_value) this should still be implemented
|
||||
/// so that other implementations like `Option` or `Vec` can make use of it.
|
||||
/// It is advised to call this method in `from_value` to ensure that expected type in the error
|
||||
/// is consistent.
|
||||
///
|
||||
/// Unlike the default implementation, derived implementations explicitly reveal the concrete
|
||||
/// type, such as [`Type::Record`] or [`Type::List`], instead of an opaque type.
|
||||
fn expected_type() -> Type {
|
||||
Type::Custom(
|
||||
any::type_name::<Self>()
|
||||
.split(':')
|
||||
.last()
|
||||
.expect("str::split returns an iterator with at least one element")
|
||||
.to_string()
|
||||
.into_boxed_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<i64> {
|
||||
// Primitive Types
|
||||
|
||||
impl<T, const N: usize> FromValue for [T; N]
|
||||
where
|
||||
T: FromValue,
|
||||
{
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Int { val, .. } => Ok(Spanned { item: val, span }),
|
||||
Value::Filesize { val, .. } => Ok(Spanned { item: val, span }),
|
||||
Value::Duration { val, .. } => Ok(Spanned { item: val, span }),
|
||||
let v_ty = v.get_type();
|
||||
let vec = Vec::<T>::from_value(v)?;
|
||||
vec.try_into()
|
||||
.map_err(|err_vec: Vec<T>| ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v_ty.to_string(),
|
||||
span,
|
||||
help: Some(match err_vec.len().cmp(&N) {
|
||||
Ordering::Less => format!(
|
||||
"input list too short ({}), expected length of {N}, add missing values",
|
||||
err_vec.len()
|
||||
),
|
||||
Ordering::Equal => {
|
||||
unreachable!("conversion would have worked if the length would be the same")
|
||||
}
|
||||
Ordering::Greater => format!(
|
||||
"input list too long ({}), expected length of {N}, remove trailing values",
|
||||
err_vec.len()
|
||||
),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Custom(format!("list<{};{N}>", T::expected_type()).into_boxed_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for bool {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Bool { val, .. } => Ok(val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "int".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Bool
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for char {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
let v_ty = v.get_type();
|
||||
match v {
|
||||
Value::String { ref val, .. } => match char::from_str(val) {
|
||||
Ok(c) => Ok(c),
|
||||
Err(_) => Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v_ty.to_string(),
|
||||
span,
|
||||
help: Some("make the string only one char long".to_string()),
|
||||
}),
|
||||
},
|
||||
_ => Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v_ty.to_string(),
|
||||
span,
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::String
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for f32 {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
f64::from_value(v).map(|float| float as f32)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for f64 {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Float { val, .. } => Ok(val),
|
||||
Value::Int { val, .. } => Ok(val as f64),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Float
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for i64 {
|
||||
|
@ -42,128 +193,207 @@ impl FromValue for i64 {
|
|||
Value::Duration { val, .. } => Ok(val),
|
||||
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "int".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Int
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<f64> {
|
||||
macro_rules! impl_from_value_for_int {
|
||||
($type:ty) => {
|
||||
impl FromValue for $type {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
let int = i64::from_value(v)?;
|
||||
const MIN: i64 = <$type>::MIN as i64;
|
||||
const MAX: i64 = <$type>::MAX as i64;
|
||||
#[allow(overlapping_range_endpoints)] // calculating MIN-1 is not possible for i64::MIN
|
||||
#[allow(unreachable_patterns)] // isize might max out i64 number range
|
||||
<$type>::try_from(int).map_err(|_| match int {
|
||||
MIN..=MAX => unreachable!(
|
||||
"int should be within the valid range for {}",
|
||||
stringify!($type)
|
||||
),
|
||||
i64::MIN..=MIN => ShellError::GenericError {
|
||||
error: "Integer too small".to_string(),
|
||||
msg: format!("{int} is smaller than {}", <$type>::MIN),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
},
|
||||
MAX..=i64::MAX => ShellError::GenericError {
|
||||
error: "Integer too large".to_string(),
|
||||
msg: format!("{int} is larger than {}", <$type>::MAX),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
i64::expected_type()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_value_for_int!(i8);
|
||||
impl_from_value_for_int!(i16);
|
||||
impl_from_value_for_int!(i32);
|
||||
impl_from_value_for_int!(isize);
|
||||
|
||||
macro_rules! impl_from_value_for_uint {
|
||||
($type:ty, $max:expr) => {
|
||||
impl FromValue for $type {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
const MAX: i64 = $max;
|
||||
match v {
|
||||
Value::Int { val, .. }
|
||||
| Value::Filesize { val, .. }
|
||||
| Value::Duration { val, .. } => {
|
||||
match val {
|
||||
i64::MIN..=-1 => Err(ShellError::NeedsPositiveValue { span }),
|
||||
0..=MAX => Ok(val as $type),
|
||||
#[allow(unreachable_patterns)] // u64 will max out the i64 number range
|
||||
n => Err(ShellError::GenericError {
|
||||
error: "Integer too large".to_string(),
|
||||
msg: format!("{n} is larger than {MAX}"),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}),
|
||||
}
|
||||
}
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Custom("non-negative int".to_string().into_boxed_str())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Sadly we cannot implement FromValue for u8 without losing the impl of Vec<u8>,
|
||||
// Rust would find two possible implementations then, Vec<u8> and Vec<T = u8>,
|
||||
// and wouldn't compile.
|
||||
// The blanket implementation for Vec<T> is probably more useful than
|
||||
// implementing FromValue for u8.
|
||||
|
||||
impl_from_value_for_uint!(u16, u16::MAX as i64);
|
||||
impl_from_value_for_uint!(u32, u32::MAX as i64);
|
||||
impl_from_value_for_uint!(u64, i64::MAX); // u64::Max would be -1 as i64
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
impl_from_value_for_uint!(usize, i64::MAX);
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
impl_from_value_for_uint!(usize, usize::MAX as i64);
|
||||
|
||||
impl FromValue for () {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Nothing { .. } => Ok(()),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Nothing
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuple_from_value {
|
||||
($template:literal, $($t:ident:$n:tt),+) => {
|
||||
impl<$($t),+> FromValue for ($($t,)+) where $($t: FromValue,)+ {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Int { val, .. } => Ok(Spanned {
|
||||
item: val as f64,
|
||||
span,
|
||||
}),
|
||||
Value::Float { val, .. } => Ok(Spanned { item: val, span }),
|
||||
Value::List { vals, .. } => {
|
||||
let mut deque = VecDeque::from(vals);
|
||||
|
||||
Ok(($(
|
||||
{
|
||||
let v = deque.pop_front().ok_or_else(|| ShellError::CantFindColumn {
|
||||
col_name: $n.to_string(),
|
||||
span: None,
|
||||
src_span: span
|
||||
})?;
|
||||
$t::from_value(v)?
|
||||
},
|
||||
)*))
|
||||
},
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "float".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Custom(
|
||||
format!(
|
||||
$template,
|
||||
$($t::expected_type()),*
|
||||
)
|
||||
.into_boxed_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl FromValue for f64 {
|
||||
// Tuples in std are implemented for up to 12 elements, so we do it here too.
|
||||
tuple_from_value!("[{}]", T0:0);
|
||||
tuple_from_value!("[{}, {}]", T0:0, T1:1);
|
||||
tuple_from_value!("[{}, {}, {}]", T0:0, T1:1, T2:2);
|
||||
tuple_from_value!("[{}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10);
|
||||
tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11);
|
||||
|
||||
// Other std Types
|
||||
|
||||
impl FromValue for PathBuf {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Float { val, .. } => Ok(val),
|
||||
Value::Int { val, .. } => Ok(val as f64),
|
||||
Value::String { val, .. } => Ok(val.into()),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "float".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<usize> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Int { val, .. } => {
|
||||
if val.is_negative() {
|
||||
Err(ShellError::NeedsPositiveValue { span })
|
||||
} else {
|
||||
Ok(Spanned {
|
||||
item: val as usize,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
Value::Filesize { val, .. } => {
|
||||
if val.is_negative() {
|
||||
Err(ShellError::NeedsPositiveValue { span })
|
||||
} else {
|
||||
Ok(Spanned {
|
||||
item: val as usize,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
Value::Duration { val, .. } => {
|
||||
if val.is_negative() {
|
||||
Err(ShellError::NeedsPositiveValue { span })
|
||||
} else {
|
||||
Ok(Spanned {
|
||||
item: val as usize,
|
||||
span,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "non-negative int".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for usize {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Int { val, .. } => {
|
||||
if val.is_negative() {
|
||||
Err(ShellError::NeedsPositiveValue { span })
|
||||
} else {
|
||||
Ok(val as usize)
|
||||
}
|
||||
}
|
||||
Value::Filesize { val, .. } => {
|
||||
if val.is_negative() {
|
||||
Err(ShellError::NeedsPositiveValue { span })
|
||||
} else {
|
||||
Ok(val as usize)
|
||||
}
|
||||
}
|
||||
Value::Duration { val, .. } => {
|
||||
if val.is_negative() {
|
||||
Err(ShellError::NeedsPositiveValue { span })
|
||||
} else {
|
||||
Ok(val as usize)
|
||||
}
|
||||
}
|
||||
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "non-negative int".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
fn expected_type() -> Type {
|
||||
Type::String
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,176 +404,111 @@ impl FromValue for String {
|
|||
Value::CellPath { val, .. } => Ok(val.to_string()),
|
||||
Value::String { val, .. } => Ok(val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<String> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
Ok(Spanned {
|
||||
item: match v {
|
||||
Value::CellPath { val, .. } => val.to_string(),
|
||||
Value::String { val, .. } => val,
|
||||
v => {
|
||||
return Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
})
|
||||
}
|
||||
},
|
||||
span,
|
||||
})
|
||||
fn expected_type() -> Type {
|
||||
Type::String
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for NuGlob {
|
||||
// This impl is different from Vec<T> as it reads from Value::Binary and
|
||||
// Value::String instead of Value::List.
|
||||
// This also denies implementing FromValue for u8.
|
||||
impl FromValue for Vec<u8> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
// FIXME: we may want to fail a little nicer here
|
||||
match v {
|
||||
Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())),
|
||||
Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)),
|
||||
Value::Glob {
|
||||
val,
|
||||
no_expand: quoted,
|
||||
..
|
||||
} => {
|
||||
if quoted {
|
||||
Ok(NuGlob::DoNotExpand(val))
|
||||
} else {
|
||||
Ok(NuGlob::Expand(val))
|
||||
}
|
||||
}
|
||||
Value::Binary { val, .. } => Ok(val),
|
||||
Value::String { val, .. } => Ok(val.into_bytes()),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<NuGlob> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
Ok(Spanned {
|
||||
item: match v {
|
||||
Value::CellPath { val, .. } => NuGlob::Expand(val.to_string()),
|
||||
Value::String { val, .. } => NuGlob::DoNotExpand(val),
|
||||
Value::Glob {
|
||||
val,
|
||||
no_expand: quoted,
|
||||
..
|
||||
} => {
|
||||
if quoted {
|
||||
NuGlob::DoNotExpand(val)
|
||||
} else {
|
||||
NuGlob::Expand(val)
|
||||
}
|
||||
}
|
||||
v => {
|
||||
return Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
})
|
||||
}
|
||||
},
|
||||
span,
|
||||
})
|
||||
fn expected_type() -> Type {
|
||||
Type::Binary
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Vec<String> {
|
||||
// Blanket std Implementations
|
||||
|
||||
impl<T> FromValue for Option<T>
|
||||
where
|
||||
T: FromValue,
|
||||
{
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
// FIXME: we may want to fail a little nicer here
|
||||
match v {
|
||||
Value::List { vals, .. } => vals
|
||||
Value::Nothing { .. } => Ok(None),
|
||||
v => T::from_value(v).map(Option::Some),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
T::expected_type()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> FromValue for HashMap<String, V>
|
||||
where
|
||||
V: FromValue,
|
||||
{
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let record = v.into_record()?;
|
||||
let items: Result<Vec<(String, V)>, ShellError> = record
|
||||
.into_iter()
|
||||
.map(|val| match val {
|
||||
Value::String { val, .. } => Ok(val),
|
||||
c => Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
from_type: c.get_type().to_string(),
|
||||
span: c.span(),
|
||||
help: None,
|
||||
}),
|
||||
})
|
||||
.collect::<Result<Vec<String>, ShellError>>(),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
.map(|(k, v)| Ok((k, V::from_value(v)?)))
|
||||
.collect();
|
||||
Ok(HashMap::from_iter(items?))
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Record(vec![].into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Vec<Spanned<String>> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
// FIXME: we may want to fail a little nicer here
|
||||
match v {
|
||||
Value::List { vals, .. } => vals
|
||||
.into_iter()
|
||||
.map(|val| {
|
||||
let val_span = val.span();
|
||||
match val {
|
||||
Value::String { val, .. } => Ok(Spanned {
|
||||
item: val,
|
||||
span: val_span,
|
||||
}),
|
||||
c => Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
from_type: c.get_type().to_string(),
|
||||
span: c.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<Spanned<String>>, ShellError>>(),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "string".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Vec<bool> {
|
||||
impl<T> FromValue for Vec<T>
|
||||
where
|
||||
T: FromValue,
|
||||
{
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::List { vals, .. } => vals
|
||||
.into_iter()
|
||||
.map(|val| match val {
|
||||
Value::Bool { val, .. } => Ok(val),
|
||||
c => Err(ShellError::CantConvert {
|
||||
to_type: "bool".into(),
|
||||
from_type: c.get_type().to_string(),
|
||||
span: c.span(),
|
||||
help: None,
|
||||
}),
|
||||
})
|
||||
.collect::<Result<Vec<bool>, ShellError>>(),
|
||||
.map(|v| T::from_value(v))
|
||||
.collect::<Result<Vec<T>, ShellError>>(),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "bool".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::List(Box::new(T::expected_type()))
|
||||
}
|
||||
}
|
||||
|
||||
// Nu Types
|
||||
|
||||
impl FromValue for Value {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Any
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for CellPath {
|
||||
|
@ -372,36 +537,25 @@ impl FromValue for CellPath {
|
|||
}
|
||||
}
|
||||
x => Err(ShellError::CantConvert {
|
||||
to_type: "cell path".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: x.get_type().to_string(),
|
||||
span,
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for bool {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Bool { val, .. } => Ok(val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "bool".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
fn expected_type() -> Type {
|
||||
Type::CellPath
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<bool> {
|
||||
impl FromValue for Closure {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Bool { val, .. } => Ok(Spanned { item: val, span }),
|
||||
Value::Closure { val, .. } => Ok(*val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "bool".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
|
@ -415,28 +569,48 @@ impl FromValue for DateTime<FixedOffset> {
|
|||
match v {
|
||||
Value::Date { val, .. } => Ok(val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "date".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::Date
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<DateTime<FixedOffset>> {
|
||||
impl FromValue for NuGlob {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
// FIXME: we may want to fail a little nicer here
|
||||
match v {
|
||||
Value::Date { val, .. } => Ok(Spanned { item: val, span }),
|
||||
Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())),
|
||||
Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)),
|
||||
Value::Glob {
|
||||
val,
|
||||
no_expand: quoted,
|
||||
..
|
||||
} => {
|
||||
if quoted {
|
||||
Ok(NuGlob::DoNotExpand(val))
|
||||
} else {
|
||||
Ok(NuGlob::Expand(val))
|
||||
}
|
||||
}
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "date".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::String
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Range {
|
||||
|
@ -444,94 +618,16 @@ impl FromValue for Range {
|
|||
match v {
|
||||
Value::Range { val, .. } => Ok(*val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "range".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<Range> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Range { val, .. } => Ok(Spanned { item: *val, span }),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "range".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Vec<u8> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Binary { val, .. } => Ok(val),
|
||||
Value::String { val, .. } => Ok(val.into_bytes()),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "binary data".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<Vec<u8>> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Binary { val, .. } => Ok(Spanned { item: val, span }),
|
||||
Value::String { val, .. } => Ok(Spanned {
|
||||
item: val.into_bytes(),
|
||||
span,
|
||||
}),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "binary data".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Spanned<PathBuf> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::String { val, .. } => Ok(Spanned {
|
||||
item: val.into(),
|
||||
span,
|
||||
}),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "range".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Vec<Value> {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
// FIXME: we may want to fail a little nicer here
|
||||
match v {
|
||||
Value::List { vals, .. } => Ok(vals),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "Vector of values".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
fn expected_type() -> Type {
|
||||
Type::Range
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -540,7 +636,7 @@ impl FromValue for Record {
|
|||
match v {
|
||||
Value::Record { val, .. } => Ok(val.into_owned()),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "Record".into(),
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
|
@ -549,31 +645,39 @@ impl FromValue for Record {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromValue for Closure {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Closure { val, .. } => Ok(*val),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "Closure".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Blanket Nu Implementations
|
||||
|
||||
impl FromValue for Spanned<Closure> {
|
||||
impl<T> FromValue for Spanned<T>
|
||||
where
|
||||
T: FromValue,
|
||||
{
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
match v {
|
||||
Value::Closure { val, .. } => Ok(Spanned { item: *val, span }),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "Closure".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
Ok(Spanned {
|
||||
item: T::from_value(v)?,
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
T::expected_type()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{engine::Closure, FromValue, Record, Type};
|
||||
|
||||
#[test]
|
||||
fn expected_type_default_impl() {
|
||||
assert_eq!(
|
||||
Record::expected_type(),
|
||||
Type::Custom("Record".to_string().into_boxed_str())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Closure::expected_type(),
|
||||
Type::Custom("Closure".to_string().into_boxed_str())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
196
crates/nu-protocol/src/value/into_value.rs
Normal file
196
crates/nu-protocol/src/value/into_value.rs
Normal file
|
@ -0,0 +1,196 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Record, ShellError, Span, Value};
|
||||
|
||||
/// A trait for converting a value into a [`Value`].
|
||||
///
|
||||
/// This conversion is infallible, for fallible conversions use [`TryIntoValue`].
|
||||
///
|
||||
/// # Derivable
|
||||
/// This trait can be used with `#[derive]`.
|
||||
/// When derived on structs with named fields, the resulting value representation will use
|
||||
/// [`Value::Record`], where each field of the record corresponds to a field of the struct.
|
||||
/// For structs with unnamed fields, the value representation will be [`Value::List`], with all
|
||||
/// fields inserted into a list.
|
||||
/// Unit structs will be represented as [`Value::Nothing`] since they contain no data.
|
||||
///
|
||||
/// Only enums with no fields may derive this trait.
|
||||
/// The resulting value representation will be the name of the variant as a [`Value::String`].
|
||||
/// By default, variant names will be converted to ["snake_case"](convert_case::Case::Snake).
|
||||
/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum.
|
||||
/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported
|
||||
/// by specifying the case name followed by "case".
|
||||
/// Also all values for
|
||||
/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid
|
||||
/// here.
|
||||
///
|
||||
/// ```
|
||||
/// # use nu_protocol::{IntoValue, Value, Span};
|
||||
/// #[derive(IntoValue)]
|
||||
/// #[nu_value(rename_all = "COBOL-CASE")]
|
||||
/// enum Bird {
|
||||
/// MountainEagle,
|
||||
/// ForestOwl,
|
||||
/// RiverDuck,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Bird::RiverDuck.into_value(Span::unknown()),
|
||||
/// Value::test_string("RIVER-DUCK")
|
||||
/// );
|
||||
/// ```
|
||||
pub trait IntoValue: Sized {
|
||||
/// Converts the given value to a [`Value`].
|
||||
fn into_value(self, span: Span) -> Value;
|
||||
}
|
||||
|
||||
// Primitive Types
|
||||
|
||||
impl<T, const N: usize> IntoValue for [T; N]
|
||||
where
|
||||
T: IntoValue,
|
||||
{
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
Vec::from(self).into_value(span)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! primitive_into_value {
|
||||
($type:ty, $method:ident) => {
|
||||
primitive_into_value!($type => $type, $method);
|
||||
};
|
||||
|
||||
($type:ty => $as_type:ty, $method:ident) => {
|
||||
impl IntoValue for $type {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
Value::$method(<$as_type>::from(self), span)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
primitive_into_value!(bool, bool);
|
||||
primitive_into_value!(char, string);
|
||||
primitive_into_value!(f32 => f64, float);
|
||||
primitive_into_value!(f64, float);
|
||||
primitive_into_value!(i8 => i64, int);
|
||||
primitive_into_value!(i16 => i64, int);
|
||||
primitive_into_value!(i32 => i64, int);
|
||||
primitive_into_value!(i64, int);
|
||||
primitive_into_value!(u8 => i64, int);
|
||||
primitive_into_value!(u16 => i64, int);
|
||||
primitive_into_value!(u32 => i64, int);
|
||||
// u64 and usize may be truncated as Value only supports i64.
|
||||
|
||||
impl IntoValue for isize {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
Value::int(self as i64, span)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for () {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
Value::nothing(span)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuple_into_value {
|
||||
($($t:ident:$n:tt),+) => {
|
||||
impl<$($t),+> IntoValue for ($($t,)+) where $($t: IntoValue,)+ {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
let vals = vec![$(self.$n.into_value(span)),+];
|
||||
Value::list(vals, span)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tuples in std are implemented for up to 12 elements, so we do it here too.
|
||||
tuple_into_value!(T0:0);
|
||||
tuple_into_value!(T0:0, T1:1);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10);
|
||||
tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11);
|
||||
|
||||
// Other std Types
|
||||
|
||||
impl IntoValue for String {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
Value::string(self, span)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoValue for Vec<T>
|
||||
where
|
||||
T: IntoValue,
|
||||
{
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
Value::list(self.into_iter().map(|v| v.into_value(span)).collect(), span)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoValue for Option<T>
|
||||
where
|
||||
T: IntoValue,
|
||||
{
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
match self {
|
||||
Some(v) => v.into_value(span),
|
||||
None => Value::nothing(span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoValue for HashMap<String, V>
|
||||
where
|
||||
V: IntoValue,
|
||||
{
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
let mut record = Record::new();
|
||||
for (k, v) in self.into_iter() {
|
||||
// Using `push` is fine as a hashmaps have unique keys.
|
||||
// To ensure this uniqueness, we only allow hashmaps with strings as
|
||||
// keys and not keys which implement `Into<String>` or `ToString`.
|
||||
record.push(k, v.into_value(span));
|
||||
}
|
||||
Value::record(record, span)
|
||||
}
|
||||
}
|
||||
|
||||
// Nu Types
|
||||
|
||||
impl IntoValue for Value {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
self.with_span(span)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use this type for all the `into_value` methods that types implement but return a Result
|
||||
/// A trait for trying to convert a value into a `Value`.
|
||||
///
|
||||
/// Types like streams may fail while collecting the `Value`,
|
||||
/// for these types it is useful to implement a fallible variant.
|
||||
///
|
||||
/// This conversion is fallible, for infallible conversions use [`IntoValue`].
|
||||
/// All types that implement `IntoValue` will automatically implement this trait.
|
||||
pub trait TryIntoValue: Sized {
|
||||
// TODO: instead of ShellError, maybe we could have a IntoValueError that implements Into<ShellError>
|
||||
/// Tries to convert the given value into a `Value`.
|
||||
fn try_into_value(self, span: Span) -> Result<Value, ShellError>;
|
||||
}
|
||||
|
||||
impl<T> TryIntoValue for T
|
||||
where
|
||||
T: IntoValue,
|
||||
{
|
||||
fn try_into_value(self, span: Span) -> Result<Value, ShellError> {
|
||||
Ok(self.into_value(span))
|
||||
}
|
||||
}
|
|
@ -4,7 +4,10 @@ mod filesize;
|
|||
mod from;
|
||||
mod from_value;
|
||||
mod glob;
|
||||
mod into_value;
|
||||
mod range;
|
||||
#[cfg(test)]
|
||||
mod test_derive;
|
||||
|
||||
pub mod record;
|
||||
pub use custom_value::CustomValue;
|
||||
|
@ -12,6 +15,7 @@ pub use duration::*;
|
|||
pub use filesize::*;
|
||||
pub use from_value::FromValue;
|
||||
pub use glob::*;
|
||||
pub use into_value::{IntoValue, TryIntoValue};
|
||||
pub use range::{FloatRange, IntRange, Range};
|
||||
pub use record::Record;
|
||||
|
||||
|
@ -1089,7 +1093,7 @@ impl Value {
|
|||
} else {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: column_name.clone(),
|
||||
span: *origin_span,
|
||||
span: Some(*origin_span),
|
||||
src_span: span,
|
||||
});
|
||||
}
|
||||
|
@ -1126,7 +1130,7 @@ impl Value {
|
|||
} else {
|
||||
Err(ShellError::CantFindColumn {
|
||||
col_name: column_name.clone(),
|
||||
span: *origin_span,
|
||||
span: Some(*origin_span),
|
||||
src_span: val_span,
|
||||
})
|
||||
}
|
||||
|
@ -1136,7 +1140,7 @@ impl Value {
|
|||
}
|
||||
_ => Err(ShellError::CantFindColumn {
|
||||
col_name: column_name.clone(),
|
||||
span: *origin_span,
|
||||
span: Some(*origin_span),
|
||||
src_span: val_span,
|
||||
}),
|
||||
}
|
||||
|
@ -1237,7 +1241,7 @@ impl Value {
|
|||
v => {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
});
|
||||
}
|
||||
|
@ -1262,7 +1266,7 @@ impl Value {
|
|||
v => {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
});
|
||||
}
|
||||
|
@ -1342,7 +1346,7 @@ impl Value {
|
|||
} else {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v_span,
|
||||
});
|
||||
}
|
||||
|
@ -1351,7 +1355,7 @@ impl Value {
|
|||
v => {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
});
|
||||
}
|
||||
|
@ -1364,7 +1368,7 @@ impl Value {
|
|||
} else {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v_span,
|
||||
});
|
||||
}
|
||||
|
@ -1373,7 +1377,7 @@ impl Value {
|
|||
v => {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
});
|
||||
}
|
||||
|
@ -1427,7 +1431,7 @@ impl Value {
|
|||
if record.to_mut().remove(col_name).is_none() && !optional {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v_span,
|
||||
});
|
||||
}
|
||||
|
@ -1435,7 +1439,7 @@ impl Value {
|
|||
v => {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
});
|
||||
}
|
||||
|
@ -1447,7 +1451,7 @@ impl Value {
|
|||
if record.to_mut().remove(col_name).is_none() && !optional {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v_span,
|
||||
});
|
||||
}
|
||||
|
@ -1455,7 +1459,7 @@ impl Value {
|
|||
}
|
||||
v => Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
}),
|
||||
},
|
||||
|
@ -1504,7 +1508,7 @@ impl Value {
|
|||
} else if !optional {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v_span,
|
||||
});
|
||||
}
|
||||
|
@ -1512,7 +1516,7 @@ impl Value {
|
|||
v => {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
});
|
||||
}
|
||||
|
@ -1526,7 +1530,7 @@ impl Value {
|
|||
} else if !optional {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v_span,
|
||||
});
|
||||
}
|
||||
|
@ -1534,7 +1538,7 @@ impl Value {
|
|||
}
|
||||
v => Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.clone(),
|
||||
span: *span,
|
||||
span: Some(*span),
|
||||
src_span: v.span(),
|
||||
}),
|
||||
},
|
||||
|
|
386
crates/nu-protocol/src/value/test_derive.rs
Normal file
386
crates/nu-protocol/src/value/test_derive.rs
Normal file
|
@ -0,0 +1,386 @@
|
|||
use crate::{record, FromValue, IntoValue, Record, Span, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Make nu_protocol available in this namespace, consumers of this crate will
|
||||
// have this without such an export.
|
||||
// The derive macro fully qualifies paths to "nu_protocol".
|
||||
use crate as nu_protocol;
|
||||
|
||||
trait IntoTestValue {
|
||||
fn into_test_value(self) -> Value;
|
||||
}
|
||||
|
||||
impl<T> IntoTestValue for T
|
||||
where
|
||||
T: IntoValue,
|
||||
{
|
||||
fn into_test_value(self) -> Value {
|
||||
self.into_value(Span::test_data())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||
struct NamedFieldsStruct<T>
|
||||
where
|
||||
T: IntoValue + FromValue,
|
||||
{
|
||||
array: [u16; 4],
|
||||
bool: bool,
|
||||
char: char,
|
||||
f32: f32,
|
||||
f64: f64,
|
||||
i8: i8,
|
||||
i16: i16,
|
||||
i32: i32,
|
||||
i64: i64,
|
||||
isize: isize,
|
||||
u16: u16,
|
||||
u32: u32,
|
||||
unit: (),
|
||||
tuple: (u32, bool),
|
||||
some: Option<u32>,
|
||||
none: Option<u32>,
|
||||
vec: Vec<T>,
|
||||
string: String,
|
||||
hashmap: HashMap<String, u32>,
|
||||
nested: Nestee,
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||
struct Nestee {
|
||||
u32: u32,
|
||||
some: Option<u32>,
|
||||
none: Option<u32>,
|
||||
}
|
||||
|
||||
impl NamedFieldsStruct<u32> {
|
||||
fn make() -> Self {
|
||||
Self {
|
||||
array: [1, 2, 3, 4],
|
||||
bool: true,
|
||||
char: 'a',
|
||||
f32: std::f32::consts::PI,
|
||||
f64: std::f64::consts::E,
|
||||
i8: 127,
|
||||
i16: -32768,
|
||||
i32: 2147483647,
|
||||
i64: -9223372036854775808,
|
||||
isize: 2,
|
||||
u16: 65535,
|
||||
u32: 4294967295,
|
||||
unit: (),
|
||||
tuple: (1, true),
|
||||
some: Some(123),
|
||||
none: None,
|
||||
vec: vec![10, 20, 30],
|
||||
string: "string".to_string(),
|
||||
hashmap: HashMap::from_iter([("a".to_string(), 10), ("b".to_string(), 20)]),
|
||||
nested: Nestee {
|
||||
u32: 3,
|
||||
some: Some(42),
|
||||
none: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
Value::test_record(record! {
|
||||
"array" => Value::test_list(vec![
|
||||
Value::test_int(1),
|
||||
Value::test_int(2),
|
||||
Value::test_int(3),
|
||||
Value::test_int(4)
|
||||
]),
|
||||
"bool" => Value::test_bool(true),
|
||||
"char" => Value::test_string('a'),
|
||||
"f32" => Value::test_float(std::f32::consts::PI.into()),
|
||||
"f64" => Value::test_float(std::f64::consts::E),
|
||||
"i8" => Value::test_int(127),
|
||||
"i16" => Value::test_int(-32768),
|
||||
"i32" => Value::test_int(2147483647),
|
||||
"i64" => Value::test_int(-9223372036854775808),
|
||||
"isize" => Value::test_int(2),
|
||||
"u16" => Value::test_int(65535),
|
||||
"u32" => Value::test_int(4294967295),
|
||||
"unit" => Value::test_nothing(),
|
||||
"tuple" => Value::test_list(vec![
|
||||
Value::test_int(1),
|
||||
Value::test_bool(true)
|
||||
]),
|
||||
"some" => Value::test_int(123),
|
||||
"none" => Value::test_nothing(),
|
||||
"vec" => Value::test_list(vec![
|
||||
Value::test_int(10),
|
||||
Value::test_int(20),
|
||||
Value::test_int(30)
|
||||
]),
|
||||
"string" => Value::test_string("string"),
|
||||
"hashmap" => Value::test_record(record! {
|
||||
"a" => Value::test_int(10),
|
||||
"b" => Value::test_int(20)
|
||||
}),
|
||||
"nested" => Value::test_record(record! {
|
||||
"u32" => Value::test_int(3),
|
||||
"some" => Value::test_int(42),
|
||||
"none" => Value::test_nothing(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_fields_struct_into_value() {
|
||||
let expected = NamedFieldsStruct::value();
|
||||
let actual = NamedFieldsStruct::make().into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_fields_struct_from_value() {
|
||||
let expected = NamedFieldsStruct::make();
|
||||
let actual = NamedFieldsStruct::from_value(NamedFieldsStruct::value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_fields_struct_roundtrip() {
|
||||
let expected = NamedFieldsStruct::make();
|
||||
let actual =
|
||||
NamedFieldsStruct::from_value(NamedFieldsStruct::make().into_test_value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let expected = NamedFieldsStruct::value();
|
||||
let actual = NamedFieldsStruct::<u32>::from_value(NamedFieldsStruct::value())
|
||||
.unwrap()
|
||||
.into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_fields_struct_missing_value() {
|
||||
let value = Value::test_record(Record::new());
|
||||
let res: Result<NamedFieldsStruct<u32>, _> = NamedFieldsStruct::from_value(value);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_fields_struct_incorrect_type() {
|
||||
// Should work for every type that is not a record.
|
||||
let value = Value::test_nothing();
|
||||
let res: Result<NamedFieldsStruct<u32>, _> = NamedFieldsStruct::from_value(value);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||
struct UnnamedFieldsStruct<T>(u32, String, T)
|
||||
where
|
||||
T: IntoValue + FromValue;
|
||||
|
||||
impl UnnamedFieldsStruct<f64> {
|
||||
fn make() -> Self {
|
||||
UnnamedFieldsStruct(420, "Hello, tuple!".to_string(), 33.33)
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
Value::test_list(vec![
|
||||
Value::test_int(420),
|
||||
Value::test_string("Hello, tuple!"),
|
||||
Value::test_float(33.33),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_fields_struct_into_value() {
|
||||
let expected = UnnamedFieldsStruct::value();
|
||||
let actual = UnnamedFieldsStruct::make().into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_fields_struct_from_value() {
|
||||
let expected = UnnamedFieldsStruct::make();
|
||||
let value = UnnamedFieldsStruct::value();
|
||||
let actual = UnnamedFieldsStruct::from_value(value).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_fields_struct_roundtrip() {
|
||||
let expected = UnnamedFieldsStruct::make();
|
||||
let actual =
|
||||
UnnamedFieldsStruct::from_value(UnnamedFieldsStruct::make().into_test_value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let expected = UnnamedFieldsStruct::value();
|
||||
let actual = UnnamedFieldsStruct::<f64>::from_value(UnnamedFieldsStruct::value())
|
||||
.unwrap()
|
||||
.into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_fields_struct_missing_value() {
|
||||
let value = Value::test_list(vec![]);
|
||||
let res: Result<UnnamedFieldsStruct<f64>, _> = UnnamedFieldsStruct::from_value(value);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_fields_struct_incorrect_type() {
|
||||
// Should work for every type that is not a record.
|
||||
let value = Value::test_nothing();
|
||||
let res: Result<UnnamedFieldsStruct<f64>, _> = UnnamedFieldsStruct::from_value(value);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||
struct UnitStruct;
|
||||
|
||||
#[test]
|
||||
fn unit_struct_into_value() {
|
||||
let expected = Value::test_nothing();
|
||||
let actual = UnitStruct.into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_struct_from_value() {
|
||||
let expected = UnitStruct;
|
||||
let actual = UnitStruct::from_value(Value::test_nothing()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_struct_roundtrip() {
|
||||
let expected = UnitStruct;
|
||||
let actual = UnitStruct::from_value(UnitStruct.into_test_value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let expected = Value::test_nothing();
|
||||
let actual = UnitStruct::from_value(Value::test_nothing())
|
||||
.unwrap()
|
||||
.into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Debug, PartialEq)]
|
||||
enum Enum {
|
||||
AlphaOne,
|
||||
BetaTwo,
|
||||
CharlieThree,
|
||||
}
|
||||
|
||||
impl Enum {
|
||||
fn make() -> [Self; 3] {
|
||||
[Enum::AlphaOne, Enum::BetaTwo, Enum::CharlieThree]
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
Value::test_list(vec![
|
||||
Value::test_string("alpha_one"),
|
||||
Value::test_string("beta_two"),
|
||||
Value::test_string("charlie_three"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_into_value() {
|
||||
let expected = Enum::value();
|
||||
let actual = Enum::make().into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_from_value() {
|
||||
let expected = Enum::make();
|
||||
let actual = <[Enum; 3]>::from_value(Enum::value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_roundtrip() {
|
||||
let expected = Enum::make();
|
||||
let actual = <[Enum; 3]>::from_value(Enum::make().into_test_value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let expected = Enum::value();
|
||||
let actual = <[Enum; 3]>::from_value(Enum::value())
|
||||
.unwrap()
|
||||
.into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_unknown_variant() {
|
||||
let value = Value::test_string("delta_four");
|
||||
let res = Enum::from_value(value);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_incorrect_type() {
|
||||
// Should work for every type that is not a record.
|
||||
let value = Value::test_nothing();
|
||||
let res = Enum::from_value(value);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
// Generate the `Enum` from before but with all possible `rename_all` variants.
|
||||
macro_rules! enum_rename_all {
|
||||
($($ident:ident: $case:literal => [$a1:literal, $b2:literal, $c3:literal]),*) => {
|
||||
$(
|
||||
#[derive(Debug, PartialEq, IntoValue, FromValue)]
|
||||
#[nu_value(rename_all = $case)]
|
||||
enum $ident {
|
||||
AlphaOne,
|
||||
BetaTwo,
|
||||
CharlieThree
|
||||
}
|
||||
|
||||
impl $ident {
|
||||
fn make() -> [Self; 3] {
|
||||
[Self::AlphaOne, Self::BetaTwo, Self::CharlieThree]
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
Value::test_list(vec![
|
||||
Value::test_string($a1),
|
||||
Value::test_string($b2),
|
||||
Value::test_string($c3),
|
||||
])
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
#[test]
|
||||
fn enum_rename_all_into_value() {$({
|
||||
let expected = $ident::value();
|
||||
let actual = $ident::make().into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
})*}
|
||||
|
||||
#[test]
|
||||
fn enum_rename_all_from_value() {$({
|
||||
let expected = $ident::make();
|
||||
let actual = <[$ident; 3]>::from_value($ident::value()).unwrap();
|
||||
assert_eq!(expected, actual);
|
||||
})*}
|
||||
}
|
||||
}
|
||||
|
||||
enum_rename_all! {
|
||||
Upper: "UPPER CASE" => ["ALPHA ONE", "BETA TWO", "CHARLIE THREE"],
|
||||
Lower: "lower case" => ["alpha one", "beta two", "charlie three"],
|
||||
Title: "Title Case" => ["Alpha One", "Beta Two", "Charlie Three"],
|
||||
Camel: "camelCase" => ["alphaOne", "betaTwo", "charlieThree"],
|
||||
Pascal: "PascalCase" => ["AlphaOne", "BetaTwo", "CharlieThree"],
|
||||
Snake: "snake_case" => ["alpha_one", "beta_two", "charlie_three"],
|
||||
UpperSnake: "UPPER_SNAKE_CASE" => ["ALPHA_ONE", "BETA_TWO", "CHARLIE_THREE"],
|
||||
Kebab: "kebab-case" => ["alpha-one", "beta-two", "charlie-three"],
|
||||
Cobol: "COBOL-CASE" => ["ALPHA-ONE", "BETA-TWO", "CHARLIE-THREE"],
|
||||
Train: "Train-Case" => ["Alpha-One", "Beta-Two", "Charlie-Three"],
|
||||
Flat: "flatcase" => ["alphaone", "betatwo", "charliethree"],
|
||||
UpperFlat: "UPPERFLATCASE" => ["ALPHAONE", "BETATWO", "CHARLIETHREE"]
|
||||
}
|
|
@ -283,6 +283,8 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef<str>, with_std: bool) -> O
|
|||
if !with_std {
|
||||
command.arg("--no-std-lib");
|
||||
}
|
||||
// Use plain errors to help make error text matching more consistent
|
||||
command.args(["--error-style", "plain"]);
|
||||
command
|
||||
.arg(format!("-c {}", escape_quote_string(&commands)))
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -369,6 +371,8 @@ where
|
|||
.envs(envs)
|
||||
.arg("--commands")
|
||||
.arg(command)
|
||||
// Use plain errors to help make error text matching more consistent
|
||||
.args(["--error-style", "plain"])
|
||||
.arg("--config")
|
||||
.arg(temp_config_file)
|
||||
.arg("--env-config")
|
||||
|
|
|
@ -87,7 +87,7 @@ impl CustomValue for CoolCustomValue {
|
|||
} else {
|
||||
Err(ShellError::CantFindColumn {
|
||||
col_name: column_name,
|
||||
span: path_span,
|
||||
span: Some(path_span),
|
||||
src_span: self_span,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -19,4 +19,4 @@ bench = false
|
|||
nu-plugin = { path = "../nu-plugin", version = "0.94.3" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
|
||||
|
||||
git2 = "0.18"
|
||||
git2 = "0.19"
|
||||
|
|
|
@ -29,8 +29,10 @@ pub(crate) fn gather_commandline_args() -> (Vec<String>, String, Vec<String>) {
|
|||
}
|
||||
|
||||
let flag_value = match arg.as_ref() {
|
||||
"--commands" | "-c" | "--table-mode" | "-m" | "-e" | "--execute" | "--config"
|
||||
| "--env-config" | "-I" | "ide-ast" => args.next().map(|a| escape_quote_string(&a)),
|
||||
"--commands" | "-c" | "--table-mode" | "-m" | "--error-style" | "-e" | "--execute"
|
||||
| "--config" | "--env-config" | "-I" | "ide-ast" => {
|
||||
args.next().map(|a| escape_quote_string(&a))
|
||||
}
|
||||
#[cfg(feature = "plugin")]
|
||||
"--plugin-config" => args.next().map(|a| escape_quote_string(&a)),
|
||||
"--log-level" | "--log-target" | "--log-include" | "--log-exclude" | "--testbin"
|
||||
|
@ -102,6 +104,8 @@ pub(crate) fn parse_commandline_args(
|
|||
let execute = call.get_flag_expr("execute");
|
||||
let table_mode: Option<Value> =
|
||||
call.get_flag(engine_state, &mut stack, "table-mode")?;
|
||||
let error_style: Option<Value> =
|
||||
call.get_flag(engine_state, &mut stack, "error-style")?;
|
||||
let no_newline = call.get_named_arg("no-newline");
|
||||
|
||||
// ide flags
|
||||
|
@ -245,6 +249,7 @@ pub(crate) fn parse_commandline_args(
|
|||
ide_check,
|
||||
ide_ast,
|
||||
table_mode,
|
||||
error_style,
|
||||
no_newline,
|
||||
});
|
||||
}
|
||||
|
@ -278,6 +283,7 @@ pub(crate) struct NushellCliArgs {
|
|||
pub(crate) log_exclude: Option<Vec<Spanned<String>>>,
|
||||
pub(crate) execute: Option<Spanned<String>>,
|
||||
pub(crate) table_mode: Option<Value>,
|
||||
pub(crate) error_style: Option<Value>,
|
||||
pub(crate) no_newline: Option<Spanned<String>>,
|
||||
pub(crate) include_path: Option<Spanned<String>>,
|
||||
pub(crate) lsp: bool,
|
||||
|
@ -325,6 +331,12 @@ impl Command for Nu {
|
|||
"the table mode to use. rounded is default.",
|
||||
Some('m'),
|
||||
)
|
||||
.named(
|
||||
"error-style",
|
||||
SyntaxShape::String,
|
||||
"the error style to use (fancy or plain). default: fancy",
|
||||
None,
|
||||
)
|
||||
.switch("no-newline", "print the result for --commands(-c) without a newline", None)
|
||||
.switch(
|
||||
"no-config-file",
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
use log::trace;
|
||||
#[cfg(feature = "plugin")]
|
||||
use nu_cli::read_plugin_file;
|
||||
use nu_cli::{evaluate_commands, evaluate_file, evaluate_repl};
|
||||
use nu_cli::{evaluate_commands, evaluate_file, evaluate_repl, EvaluateCommandsOpts};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
report_error_new, PipelineData, Spanned,
|
||||
|
@ -114,8 +114,11 @@ pub(crate) fn run_commands(
|
|||
engine_state,
|
||||
&mut stack,
|
||||
input,
|
||||
parsed_nu_cli_args.table_mode,
|
||||
parsed_nu_cli_args.no_newline.is_some(),
|
||||
EvaluateCommandsOpts {
|
||||
table_mode: parsed_nu_cli_args.table_mode,
|
||||
error_style: parsed_nu_cli_args.error_style,
|
||||
no_newline: parsed_nu_cli_args.no_newline.is_some(),
|
||||
},
|
||||
) {
|
||||
report_error_new(engine_state, &err);
|
||||
std::process::exit(1);
|
||||
|
|
Loading…
Reference in New Issue
Block a user