diff --git a/Cargo.lock b/Cargo.lock index 9ff004f6f..98c268d56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,6 +1923,8 @@ dependencies = [ "nu-protocol", "nu-source", "nu_plugin_binaryview", + "nu_plugin_fetch", + "nu_plugin_post", "nu_plugin_ps", "nu_plugin_sys", "nu_plugin_textview", @@ -1953,7 +1955,6 @@ dependencies = [ "strip-ansi-escapes", "sublime_fuzzy", "subprocess", - "surf", "tempfile", "term", "termcolor", @@ -1963,7 +1964,6 @@ dependencies = [ "typetag", "umask", "unicode-xid", - "url", "which", ] @@ -2090,6 +2090,35 @@ dependencies = [ "rawkey", ] +[[package]] +name = "nu_plugin_fetch" +version = "0.1.0" +dependencies = [ + "futures-preview", + "nu-build", + "nu-errors", + "nu-protocol", + "nu-source", + "surf", + "url", +] + +[[package]] +name = "nu_plugin_post" +version = "0.1.0" +dependencies = [ + "base64 0.11.0", + "futures-preview", + "nu-build", + "nu-errors", + "nu-protocol", + "nu-source", + "num-traits 0.2.10", + "serde_json", + "surf", + "url", +] + [[package]] name = "nu_plugin_ps" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e91b6d15b..eeabe05a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ members = [ "crates/nu-source", "crates/nu_plugin_textview", "crates/nu_plugin_binaryview", + "crates/nu_plugin_fetch", + "crates/nu_plugin_post", "crates/nu_plugin_ps", "crates/nu_plugin_sys", "crates/nu-protocol", @@ -34,6 +36,8 @@ nu-errors = { version = "0.1.0", path = "./crates/nu-errors" } nu-parser = { version = "0.1.0", path = "./crates/nu-parser" } nu_plugin_textview = {version = "0.1.0", path = "./crates/nu_plugin_textview", optional=true} nu_plugin_binaryview = {version = "0.1.0", path = "./crates/nu_plugin_binaryview", optional=true} +nu_plugin_fetch = {version = "0.1.0", path = "./crates/nu_plugin_fetch", optional=true} +nu_plugin_post = {version = "0.1.0", path = "./crates/nu_plugin_post", optional=true} nu_plugin_ps = {version = "0.1.0", path = "./crates/nu_plugin_ps", optional=true} nu_plugin_sys = {version = "0.1.0", path = "./crates/nu_plugin_sys", optional=true} @@ -75,8 +79,6 @@ git2 = { version = "0.10.1", default_features = false } dirs = "2.0.2" glob = "0.3.0" ctrlc = "3.1.3" -surf = "1.0.3" -url = "2.1.0" roxmltree = "0.7.2" nom_locate = "1.0.0" nom-tracable = "0.4.1" diff --git a/crates/nu-protocol/src/return_value.rs b/crates/nu-protocol/src/return_value.rs index 42e948b19..ddca03093 100644 --- a/crates/nu-protocol/src/return_value.rs +++ b/crates/nu-protocol/src/return_value.rs @@ -9,6 +9,7 @@ pub enum CommandAction { Exit, Error(ShellError), EnterShell(String), + AutoConvert(Value, String), EnterValueShell(Value), EnterHelpShell(Value), PreviousShell, @@ -22,6 +23,9 @@ impl PrettyDebug for CommandAction { CommandAction::ChangePath(path) => b::typed("change path", b::description(path)), CommandAction::Exit => b::description("exit"), CommandAction::Error(_) => b::error("error"), + CommandAction::AutoConvert(_, extension) => { + b::typed("auto convert", b::description(extension)) + } CommandAction::EnterShell(s) => b::typed("enter shell", b::description(s)), CommandAction::EnterValueShell(v) => b::typed("enter value shell", v.pretty()), CommandAction::EnterHelpShell(v) => b::typed("enter help shell", v.pretty()), diff --git a/crates/nu_plugin_fetch/Cargo.toml b/crates/nu_plugin_fetch/Cargo.toml new file mode 100644 index 000000000..7051895d9 --- /dev/null +++ b/crates/nu_plugin_fetch/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nu_plugin_fetch" +version = "0.1.0" +authors = ["Yehuda Katz ", "Jonathan Turner ", "Andrés N. Robalino "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nu-protocol = { path = "../nu-protocol" } +nu-source = { path = "../nu-source" } +nu-errors = { path = "../nu-errors" } +futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] } +surf = "1.0.3" +url = "2.1.0" + +[build-dependencies] +nu-build = { version = "0.1.0", path = "../nu-build" } diff --git a/crates/nu_plugin_fetch/build.rs b/crates/nu_plugin_fetch/build.rs new file mode 100644 index 000000000..b7511cfc6 --- /dev/null +++ b/crates/nu_plugin_fetch/build.rs @@ -0,0 +1,3 @@ +fn main() -> Result<(), Box> { + nu_build::build() +} diff --git a/src/commands/fetch.rs b/crates/nu_plugin_fetch/src/main.rs similarity index 66% rename from src/commands/fetch.rs rename to crates/nu_plugin_fetch/src/main.rs index b98625f63..6192b18bd 100644 --- a/src/commands/fetch.rs +++ b/crates/nu_plugin_fetch/src/main.rs @@ -1,128 +1,117 @@ -use crate::commands::UnevaluatedCallInfo; -use crate::prelude::*; +use futures::executor::block_on; use mime::Mime; use nu_errors::ShellError; -use nu_protocol::{CallInfo, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::{AnchorLocation, Span}; +use nu_protocol::{ + serve_plugin, CallInfo, CommandAction, Plugin, ReturnSuccess, ReturnValue, Signature, + SyntaxShape, UntaggedValue, Value, +}; +use nu_source::{AnchorLocation, Span, Tag}; use std::path::PathBuf; use std::str::FromStr; use surf::mime; -pub struct Fetch; +struct Fetch { + path: Option, + has_raw: bool, +} -impl PerItemCommand for Fetch { - fn name(&self) -> &str { - "fetch" +impl Fetch { + fn new() -> Fetch { + Fetch { + path: None, + has_raw: false, + } } - fn signature(&self) -> Signature { - Signature::build(self.name()) + fn setup(&mut self, call_info: CallInfo) -> ReturnValue { + self.path = Some( + match call_info.args.nth(0).ok_or_else(|| { + ShellError::labeled_error( + "No file or directory specified", + "for command", + &call_info.name_tag, + ) + })? { + file => file.clone(), + }, + ); + + self.has_raw = call_info.args.has("raw"); + + ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value()) + } +} + +impl Plugin for Fetch { + fn config(&mut self) -> Result { + Ok(Signature::build("fetch") + .desc("Load from a URL into a cell, convert to table if possible (avoid by appending '--raw')") .required( "path", SyntaxShape::Path, "the URL to fetch the contents from", ) .switch("raw", "fetch contents as text rather than a table") + .filter()) } - fn usage(&self) -> &str { - "Load from a URL into a cell, convert to table if possible (avoid by appending '--raw')" + fn begin_filter(&mut self, callinfo: CallInfo) -> Result, ShellError> { + self.setup(callinfo)?; + Ok(vec![]) } - fn run( - &self, - call_info: &CallInfo, - registry: &CommandRegistry, - raw_args: &RawCommandArgs, - _input: Value, - ) -> Result { - run(call_info, registry, raw_args) + fn filter(&mut self, value: Value) -> Result, ShellError> { + Ok(vec![block_on(fetch_helper( + &self.path.clone().unwrap(), + self.has_raw, + value, + ))]) } } -fn run( - call_info: &CallInfo, - registry: &CommandRegistry, - raw_args: &RawCommandArgs, -) -> Result { - let path = match call_info.args.nth(0).ok_or_else(|| { - ShellError::labeled_error( - "No file or directory specified", - "for command", - &call_info.name_tag, - ) - })? { - file => file, - }; +fn main() { + serve_plugin(&mut Fetch::new()); +} + +async fn fetch_helper(path: &Value, has_raw: bool, row: Value) -> ReturnValue { let path_buf = path.as_path()?; let path_str = path_buf.display().to_string(); - let path_span = path.tag.span; - let has_raw = call_info.args.has("raw"); - let registry = registry.clone(); - let raw_args = raw_args.clone(); - let stream = async_stream! { - - let result = fetch(&path_str, path_span).await; - - if let Err(e) = result { - yield Err(e); - return; - } - let (file_extension, contents, contents_tag) = result.unwrap(); - - let file_extension = if has_raw { - None - } else { - // If the extension could not be determined via mimetype, try to use the path - // extension. Some file types do not declare their mimetypes (such as bson files). - file_extension.or(path_str.split('.').last().map(String::from)) - }; - - let tagged_contents = contents.retag(&contents_tag); - - if let Some(extension) = file_extension { - let command_name = format!("from-{}", extension); - if let Some(converter) = registry.get_command(&command_name) { - let new_args = RawCommandArgs { - host: raw_args.host, - ctrl_c: raw_args.ctrl_c, - shell_manager: raw_args.shell_manager, - call_info: UnevaluatedCallInfo { - args: nu_parser::hir::Call { - head: raw_args.call_info.args.head, - positional: None, - named: None, - span: Span::unknown() - }, - source: raw_args.call_info.source, - name_tag: raw_args.call_info.name_tag, - } - }; - let mut result = converter.run(new_args.with_input(vec![tagged_contents]), ®istry); - let result_vec: Vec> = result.drain_vec().await; - for res in result_vec { - match res { - Ok(ReturnSuccess::Value(Value { value: UntaggedValue::Table(list), ..})) => { - for l in list { - yield Ok(ReturnSuccess::Value(l)); - } - } - Ok(ReturnSuccess::Value(Value { value, .. })) => { - yield Ok(ReturnSuccess::Value(value.into_value(contents_tag.clone()))); - } - x => yield x, - } - } - } else { - yield ReturnSuccess::value(tagged_contents); - } - } else { - yield ReturnSuccess::value(tagged_contents); - } + //FIXME: this is a workaround because plugins don't yet support per-item iteration + let path_str = if path_str == "$it" { + let path_buf = row.as_path()?; + path_buf.display().to_string() + } else { + path_str }; - Ok(stream.to_output_stream()) + let path_span = path.tag.span; + + let result = fetch(&path_str, path_span).await; + + if let Err(e) = result { + return Err(e); + } + let (file_extension, contents, contents_tag) = result.unwrap(); + + let file_extension = if has_raw { + None + } else { + // If the extension could not be determined via mimetype, try to use the path + // extension. Some file types do not declare their mimetypes (such as bson files). + file_extension.or(path_str.split('.').last().map(String::from)) + }; + + let tagged_contents = contents.retag(&contents_tag); + + if let Some(extension) = file_extension { + return Ok(ReturnSuccess::Action(CommandAction::AutoConvert( + tagged_contents, + extension, + ))); + } else { + return ReturnSuccess::value(tagged_contents); + } } pub async fn fetch( diff --git a/crates/nu_plugin_post/Cargo.toml b/crates/nu_plugin_post/Cargo.toml new file mode 100644 index 000000000..d615c7493 --- /dev/null +++ b/crates/nu_plugin_post/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nu_plugin_post" +version = "0.1.0" +authors = ["Yehuda Katz ", "Jonathan Turner ", "Andrés N. Robalino "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nu-protocol = { path = "../nu-protocol" } +nu-source = { path = "../nu-source" } +nu-errors = { path = "../nu-errors" } +futures-preview = { version = "=0.3.0-alpha.19", features = ["compat", "io-compat"] } +surf = "1.0.3" +url = "2.1.0" +serde_json = "1.0.41" +base64 = "0.11" +num-traits = "0.2.8" + +[build-dependencies] +nu-build = { version = "0.1.0", path = "../nu-build" } diff --git a/crates/nu_plugin_post/build.rs b/crates/nu_plugin_post/build.rs new file mode 100644 index 000000000..b7511cfc6 --- /dev/null +++ b/crates/nu_plugin_post/build.rs @@ -0,0 +1,3 @@ +fn main() -> Result<(), Box> { + nu_build::build() +} diff --git a/src/commands/post.rs b/crates/nu_plugin_post/src/main.rs similarity index 57% rename from src/commands/post.rs rename to crates/nu_plugin_post/src/main.rs index ae2910fee..8e802a299 100644 --- a/src/commands/post.rs +++ b/crates/nu_plugin_post/src/main.rs @@ -1,30 +1,85 @@ -use crate::commands::UnevaluatedCallInfo; -use crate::prelude::*; use base64::encode; +use futures::executor::block_on; use mime::Mime; -use nu_errors::ShellError; +use nu_errors::{CoerceInto, ShellError}; use nu_protocol::{ - CallInfo, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, + serve_plugin, CallInfo, CommandAction, Plugin, Primitive, ReturnSuccess, ReturnValue, + Signature, SyntaxShape, UnspannedPathMember, UntaggedValue, Value, }; -use nu_source::AnchorLocation; +use nu_source::{AnchorLocation, Tag, TaggedItem}; +use num_traits::cast::ToPrimitive; use std::path::PathBuf; use std::str::FromStr; use surf::mime; +#[derive(Clone)] pub enum HeaderKind { ContentType(String), ContentLength(String), } -pub struct Post; +struct Post { + path: Option, + has_raw: bool, + body: Option, + user: Option, + password: Option, + headers: Vec, +} -impl PerItemCommand for Post { - fn name(&self) -> &str { - "post" +impl Post { + fn new() -> Post { + Post { + path: None, + has_raw: false, + body: None, + user: None, + password: None, + headers: vec![], + } } - fn signature(&self) -> Signature { - Signature::build(self.name()) + fn setup(&mut self, call_info: CallInfo) -> ReturnValue { + self.path = Some( + match call_info.args.nth(0).ok_or_else(|| { + ShellError::labeled_error( + "No file or directory specified", + "for command", + &call_info.name_tag, + ) + })? { + file => file.clone(), + }, + ); + + self.has_raw = call_info.args.has("raw"); + + self.body = match call_info.args.nth(1).ok_or_else(|| { + ShellError::labeled_error("No body specified", "for command", &call_info.name_tag) + })? { + file => Some(file.clone()), + }; + + self.user = call_info + .args + .get("user") + .map(|x| x.as_string().unwrap().to_string()); + + self.password = call_info + .args + .get("password") + .map(|x| x.as_string().unwrap().to_string()); + + self.headers = get_headers(&call_info)?; + + ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value()) + } +} + +impl Plugin for Post { + fn config(&mut self) -> Result { + Ok(Signature::build("post") + .desc("Post content to a url and retrieve data as a table if possible.") .required("path", SyntaxShape::Any, "the URL to post to") .required("body", SyntaxShape::Any, "the contents of the post body") .named("user", SyntaxShape::Any, "the username when authenticating") @@ -44,169 +99,85 @@ impl PerItemCommand for Post { "the length of the content being posted", ) .switch("raw", "return values as a string instead of a table") + .filter()) } - fn usage(&self) -> &str { - "Post content to a url and retrieve data as a table if possible." + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { + self.setup(call_info)?; + Ok(vec![]) } - fn run( - &self, - call_info: &CallInfo, - registry: &CommandRegistry, - raw_args: &RawCommandArgs, - _input: Value, - ) -> Result { - run(call_info, registry, raw_args) + fn filter(&mut self, row: Value) -> Result, ShellError> { + Ok(vec![block_on(post_helper( + &self.path.clone().unwrap(), + self.has_raw, + &self.body.clone().unwrap(), + self.user.clone(), + self.password.clone(), + &self.headers.clone(), + row, + ))]) } } -fn run( - call_info: &CallInfo, - registry: &CommandRegistry, - raw_args: &RawCommandArgs, -) -> Result { - let name_tag = call_info.name_tag.clone(); - let call_info = call_info.clone(); - let path = - match call_info.args.nth(0).ok_or_else(|| { - ShellError::labeled_error("No url specified", "for command", &name_tag) - })? { - file => file.clone(), - }; +fn main() { + serve_plugin(&mut Post::new()); +} + +async fn post_helper( + path: &Value, + has_raw: bool, + body: &Value, + user: Option, + password: Option, + headers: &[HeaderKind], + row: Value, +) -> ReturnValue { let path_tag = path.tag.clone(); - let body = - match call_info.args.nth(1).ok_or_else(|| { - ShellError::labeled_error("No body specified", "for command", &name_tag) - })? { - file => file.clone(), - }; let path_str = path.as_string()?.to_string(); - let has_raw = call_info.args.has("raw"); - let user = call_info - .args - .get("user") - .map(|x| x.as_string().unwrap().to_string()); - let password = call_info - .args - .get("password") - .map(|x| x.as_string().unwrap().to_string()); - let registry = registry.clone(); - let raw_args = raw_args.clone(); - let headers = get_headers(&call_info)?; + //FIXME: this is a workaround because plugins don't yet support per-item iteration + let path_str = if path_str == "$it" { + let path_buf = row.as_path()?; + path_buf.display().to_string() + } else { + path_str + }; - let stream = async_stream! { - let (file_extension, contents, contents_tag) = - post(&path_str, &body, user, password, &headers, path_tag.clone(), ®istry, &raw_args).await.unwrap(); - - let file_extension = if has_raw { - None + //FIXME: this is a workaround because plugins don't yet support per-item iteration + let body = if let Ok(x) = body.as_string() { + if x == "$it" { + &row } else { - // If the extension could not be determined via mimetype, try to use the path - // extension. Some file types do not declare their mimetypes (such as bson files). - file_extension.or(path_str.split('.').last().map(String::from)) - }; - - let tagged_contents = contents.into_value(&contents_tag); - - if let Some(extension) = file_extension { - let command_name = format!("from-{}", extension); - if let Some(converter) = registry.get_command(&command_name) { - let new_args = RawCommandArgs { - host: raw_args.host, - ctrl_c: raw_args.ctrl_c, - shell_manager: raw_args.shell_manager, - call_info: UnevaluatedCallInfo { - args: nu_parser::hir::Call { - head: raw_args.call_info.args.head, - positional: None, - named: None, - span: Span::unknown() - }, - source: raw_args.call_info.source, - name_tag: raw_args.call_info.name_tag, - } - }; - let mut result = converter.run(new_args.with_input(vec![tagged_contents]), ®istry); - let result_vec: Vec> = result.drain_vec().await; - for res in result_vec { - match res { - Ok(ReturnSuccess::Value(Value { value: UntaggedValue::Table(list), ..})) => { - for l in list { - yield Ok(ReturnSuccess::Value(l)); - } - } - Ok(ReturnSuccess::Value(Value { value, .. })) => { - yield Ok(ReturnSuccess::Value(Value { value, tag: contents_tag.clone() })); - } - x => yield x, - } - } - } else { - yield ReturnSuccess::value(tagged_contents); - } - } else { - yield ReturnSuccess::value(tagged_contents); + body } + } else { + body }; - Ok(stream.to_output_stream()) -} + let (file_extension, contents, contents_tag) = + post(&path_str, &body, user, password, &headers, path_tag.clone()) + .await + .unwrap(); -fn get_headers(call_info: &CallInfo) -> Result, ShellError> { - let mut headers = vec![]; - - match extract_header_value(&call_info, "content-type") { - Ok(h) => match h { - Some(ct) => headers.push(HeaderKind::ContentType(ct)), - None => {} - }, - Err(e) => { - return Err(e); - } + let file_extension = if has_raw { + None + } else { + // If the extension could not be determined via mimetype, try to use the path + // extension. Some file types do not declare their mimetypes (such as bson files). + file_extension.or(path_str.split('.').last().map(String::from)) }; - match extract_header_value(&call_info, "content-length") { - Ok(h) => match h { - Some(cl) => headers.push(HeaderKind::ContentLength(cl)), - None => {} - }, - Err(e) => { - return Err(e); - } - }; + let tagged_contents = contents.into_value(&contents_tag); - Ok(headers) -} - -fn extract_header_value(call_info: &CallInfo, key: &str) -> Result, ShellError> { - if call_info.args.has(key) { - let tagged = call_info.args.get(key); - let val = match tagged { - Some(Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - }) => s.clone(), - Some(Value { tag, .. }) => { - return Err(ShellError::labeled_error( - format!("{} not in expected format. Expected string.", key), - "post error", - tag, - )); - } - _ => { - return Err(ShellError::labeled_error( - format!("{} not in expected format. Expected string.", key), - "post error", - Tag::unknown(), - )); - } - }; - return Ok(Some(val)); + if let Some(extension) = file_extension { + return Ok(ReturnSuccess::Action(CommandAction::AutoConvert( + tagged_contents, + extension, + ))); + } else { + return ReturnSuccess::value(tagged_contents); } - - Ok(None) } pub async fn post( @@ -216,11 +187,7 @@ pub async fn post( password: Option, headers: &[HeaderKind], tag: Tag, - registry: &CommandRegistry, - raw_args: &RawCommandArgs, ) -> Result<(Option, UntaggedValue, Tag), ShellError> { - let registry = registry.clone(); - let raw_args = raw_args.clone(); if location.starts_with("http:") || location.starts_with("https:") { let login = match (user, password) { (Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))), @@ -256,59 +223,31 @@ pub async fn post( s.await } Value { value, tag } => { - if let Some(converter) = registry.get_command("to-json") { - let new_args = RawCommandArgs { - host: raw_args.host, - ctrl_c: raw_args.ctrl_c, - shell_manager: raw_args.shell_manager, - call_info: UnevaluatedCallInfo { - args: nu_parser::hir::Call { - head: raw_args.call_info.args.head, - positional: None, - named: None, - span: Span::unknown(), - }, - source: raw_args.call_info.source, - name_tag: raw_args.call_info.name_tag, - }, - }; - let mut result = converter.run( - new_args.with_input(vec![value.clone().into_value(tag.clone())]), - ®istry, - ); - let result_vec: Vec> = - result.drain_vec().await; - let mut result_string = String::new(); - for res in result_vec { - match res { - Ok(ReturnSuccess::Value(Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - })) => { - result_string.push_str(&s); - } - _ => { - return Err(ShellError::labeled_error( - "Save could not successfully save", - "unexpected data during save", - tag, - )); + match value_to_json_value(&value.clone().into_untagged_value()) { + Ok(json_value) => match serde_json::to_string(&json_value) { + Ok(result_string) => { + let mut s = surf::post(location).body_string(result_string); + + if let Some(login) = login { + s = s.set_header("Authorization", format!("Basic {}", login)); } + s.await } + _ => { + return Err(ShellError::labeled_error( + "Could not automatically convert table", + "needs manual conversion", + tag, + )); + } + }, + _ => { + return Err(ShellError::labeled_error( + "Could not automatically convert table", + "needs manual conversion", + tag, + )); } - - let mut s = surf::post(location).body_string(result_string); - - if let Some(login) = login { - s = s.set_header("Authorization", format!("Basic {}", login)); - } - s.await - } else { - return Err(ShellError::labeled_error( - "Could not automatically convert table", - "needs manual conversion", - tag, - )); } } }; @@ -456,3 +395,137 @@ pub async fn post( )) } } + +// FIXME FIXME FIXME +// Ultimately, we don't want to duplicate to-json here, but we need to because there isn't an easy way to call into it, yet +pub fn value_to_json_value(v: &Value) -> Result { + Ok(match &v.value { + UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b), + UntaggedValue::Primitive(Primitive::Bytes(b)) => serde_json::Value::Number( + serde_json::Number::from(b.to_u64().expect("What about really big numbers")), + ), + UntaggedValue::Primitive(Primitive::Duration(secs)) => { + serde_json::Value::Number(serde_json::Number::from(*secs)) + } + UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()), + UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null, + UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null, + UntaggedValue::Primitive(Primitive::Decimal(f)) => serde_json::Value::Number( + serde_json::Number::from_f64( + f.to_f64().expect("TODO: What about really big decimals?"), + ) + .unwrap(), + ), + UntaggedValue::Primitive(Primitive::Int(i)) => { + serde_json::Value::Number(serde_json::Number::from(CoerceInto::::coerce_into( + i.tagged(&v.tag), + "converting to JSON number", + )?)) + } + UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null, + UntaggedValue::Primitive(Primitive::Pattern(s)) => serde_json::Value::String(s.clone()), + UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()), + UntaggedValue::Primitive(Primitive::Line(s)) => serde_json::Value::String(s.clone()), + UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array( + path.iter() + .map(|x| match &x.unspanned { + UnspannedPathMember::String(string) => { + Ok(serde_json::Value::String(string.clone())) + } + UnspannedPathMember::Int(int) => Ok(serde_json::Value::Number( + serde_json::Number::from(CoerceInto::::coerce_into( + int.tagged(&v.tag), + "converting to JSON number", + )?), + )), + }) + .collect::, ShellError>>()?, + ), + UntaggedValue::Primitive(Primitive::Path(s)) => { + serde_json::Value::String(s.display().to_string()) + } + + UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?), + UntaggedValue::Error(e) => return Err(e.clone()), + UntaggedValue::Block(_) => serde_json::Value::Null, + UntaggedValue::Primitive(Primitive::Binary(b)) => serde_json::Value::Array( + b.iter() + .map(|x| { + serde_json::Value::Number(serde_json::Number::from_f64(*x as f64).unwrap()) + }) + .collect(), + ), + UntaggedValue::Row(o) => { + let mut m = serde_json::Map::new(); + for (k, v) in o.entries.iter() { + m.insert(k.clone(), value_to_json_value(v)?); + } + serde_json::Value::Object(m) + } + }) +} + +fn json_list(input: &Vec) -> Result, ShellError> { + let mut out = vec![]; + + for value in input { + out.push(value_to_json_value(value)?); + } + + Ok(out) +} + +fn get_headers(call_info: &CallInfo) -> Result, ShellError> { + let mut headers = vec![]; + + match extract_header_value(&call_info, "content-type") { + Ok(h) => match h { + Some(ct) => headers.push(HeaderKind::ContentType(ct)), + None => {} + }, + Err(e) => { + return Err(e); + } + }; + + match extract_header_value(&call_info, "content-length") { + Ok(h) => match h { + Some(cl) => headers.push(HeaderKind::ContentLength(cl)), + None => {} + }, + Err(e) => { + return Err(e); + } + }; + + Ok(headers) +} + +fn extract_header_value(call_info: &CallInfo, key: &str) -> Result, ShellError> { + if call_info.args.has(key) { + let tagged = call_info.args.get(key); + let val = match tagged { + Some(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => s.clone(), + Some(Value { tag, .. }) => { + return Err(ShellError::labeled_error( + format!("{} not in expected format. Expected string.", key), + "post error", + tag, + )); + } + _ => { + return Err(ShellError::labeled_error( + format!("{} not in expected format. Expected string.", key), + "post error", + Tag::unknown(), + )); + } + }; + return Ok(Some(val)); + } + + Ok(None) +} diff --git a/src/cli.rs b/src/cli.rs index 8f44c377e..0cbc6a7cf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -283,9 +283,7 @@ pub async fn cli() -> Result<(), Box> { whole_stream_command(Get), whole_stream_command(Histogram), per_item_command(Remove), - per_item_command(Fetch), per_item_command(Open), - per_item_command(Post), per_item_command(Where), per_item_command(Echo), per_item_command(Edit), diff --git a/src/commands.rs b/src/commands.rs index 5e7b3814f..84c89b716 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -25,7 +25,6 @@ pub(crate) mod env; #[allow(unused)] pub(crate) mod evaluate_by; pub(crate) mod exit; -pub(crate) mod fetch; pub(crate) mod first; pub(crate) mod from_bson; pub(crate) mod from_csv; @@ -58,7 +57,6 @@ pub(crate) mod open; pub(crate) mod pick; pub(crate) mod pivot; pub(crate) mod plugin; -pub(crate) mod post; pub(crate) mod prepend; pub(crate) mod prev; pub(crate) mod pwd; @@ -116,7 +114,6 @@ pub(crate) use env::Env; #[allow(unused)] pub(crate) use evaluate_by::EvaluateBy; pub(crate) use exit::Exit; -pub(crate) use fetch::Fetch; pub(crate) use first::First; pub(crate) use from_bson::FromBSON; pub(crate) use from_csv::FromCSV; @@ -150,7 +147,6 @@ pub(crate) use nth::Nth; pub(crate) use open::Open; pub(crate) use pick::Pick; pub(crate) use pivot::Pivot; -pub(crate) use post::Post; pub(crate) use prepend::Prepend; pub(crate) use prev::Previous; pub(crate) use pwd::PWD; diff --git a/src/commands/classified/internal.rs b/src/commands/classified/internal.rs index fb0add5ec..2042707c4 100644 --- a/src/commands/classified/internal.rs +++ b/src/commands/classified/internal.rs @@ -1,3 +1,4 @@ +use crate::commands::UnevaluatedCallInfo; use crate::prelude::*; use log::{log_enabled, trace}; use nu_errors::ShellError; @@ -27,7 +28,7 @@ pub(crate) async fn run_internal_command( context.run_command( internal_command, command.name_tag.clone(), - command.args, + command.args.clone(), &source, objects, ) @@ -52,6 +53,46 @@ pub(crate) async fn run_internal_command( context.error(err); break; } + CommandAction::AutoConvert(tagged_contents, extension) => { + let contents_tag = tagged_contents.tag.clone(); + let command_name = format!("from-{}", extension); + let command = command.clone(); + if let Some(converter) = context.registry.get_command(&command_name) { + let new_args = RawCommandArgs { + host: context.host.clone(), + ctrl_c: context.ctrl_c.clone(), + shell_manager: context.shell_manager.clone(), + call_info: UnevaluatedCallInfo { + args: nu_parser::hir::Call { + head: command.args.head, + positional: None, + named: None, + span: Span::unknown() + }, + source: source.clone(), + name_tag: command.name_tag, + } + }; + let mut result = converter.run(new_args.with_input(vec![tagged_contents]), &context.registry); + let result_vec: Vec> = result.drain_vec().await; + for res in result_vec { + match res { + Ok(ReturnSuccess::Value(Value { value: UntaggedValue::Table(list), ..})) => { + for l in list { + yield Ok(l); + } + } + Ok(ReturnSuccess::Value(Value { value, .. })) => { + yield Ok(value.into_value(contents_tag.clone())); + } + Err(e) => yield Err(e), + _ => {} + } + } + } else { + yield Ok(tagged_contents) + } + } CommandAction::EnterHelpShell(value) => { match value { Value { diff --git a/src/commands/command.rs b/src/commands/command.rs index 691a90bf1..ef5443a3e 100644 --- a/src/commands/command.rs +++ b/src/commands/command.rs @@ -117,6 +117,26 @@ impl CommandArgs { )) } + pub fn evaluate_once_with_scope( + self, + registry: &CommandRegistry, + scope: &Scope, + ) -> Result { + let host = self.host.clone(); + let ctrl_c = self.ctrl_c.clone(); + let shell_manager = self.shell_manager.clone(); + let input = self.input; + let call_info = self.call_info.evaluate(registry, scope)?; + + Ok(EvaluatedWholeStreamCommandArgs::new( + host, + ctrl_c, + shell_manager, + call_info, + input, + )) + } + pub fn source(&self) -> Text { self.call_info.source.clone() } diff --git a/src/commands/open.rs b/src/commands/open.rs index 1abc7c462..03a4a1abf 100644 --- a/src/commands/open.rs +++ b/src/commands/open.rs @@ -1,7 +1,8 @@ -use crate::commands::UnevaluatedCallInfo; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{CallInfo, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ + CallInfo, CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, +}; use nu_source::{AnchorLocation, Span}; use std::path::{Path, PathBuf}; @@ -29,19 +30,15 @@ impl PerItemCommand for Open { fn run( &self, call_info: &CallInfo, - registry: &CommandRegistry, + _registry: &CommandRegistry, raw_args: &RawCommandArgs, _input: Value, ) -> Result { - run(call_info, registry, raw_args) + run(call_info, raw_args) } } -fn run( - call_info: &CallInfo, - registry: &CommandRegistry, - raw_args: &RawCommandArgs, -) -> Result { +fn run(call_info: &CallInfo, raw_args: &RawCommandArgs) -> Result { let shell_manager = &raw_args.shell_manager; let cwd = PathBuf::from(shell_manager.path()); let full_path = cwd; @@ -58,8 +55,6 @@ fn run( let path_str = path_buf.display().to_string(); let path_span = path.tag.span; let has_raw = call_info.args.has("raw"); - let registry = registry.clone(); - let raw_args = raw_args.clone(); let stream = async_stream! { @@ -82,41 +77,7 @@ fn run( let tagged_contents = contents.into_value(&contents_tag); if let Some(extension) = file_extension { - let command_name = format!("from-{}", extension); - if let Some(converter) = registry.get_command(&command_name) { - let new_args = RawCommandArgs { - host: raw_args.host, - ctrl_c: raw_args.ctrl_c, - shell_manager: raw_args.shell_manager, - call_info: UnevaluatedCallInfo { - args: nu_parser::hir::Call { - head: raw_args.call_info.args.head, - positional: None, - named: None, - span: Span::unknown() - }, - source: raw_args.call_info.source, - name_tag: raw_args.call_info.name_tag, - } - }; - let mut result = converter.run(new_args.with_input(vec![tagged_contents]), ®istry); - let result_vec: Vec> = result.drain_vec().await; - for res in result_vec { - match res { - Ok(ReturnSuccess::Value(Value { value: UntaggedValue::Table(list), ..})) => { - for l in list { - yield Ok(ReturnSuccess::Value(l)); - } - } - Ok(ReturnSuccess::Value(Value { value, .. })) => { - yield Ok(ReturnSuccess::Value(Value { value, tag: contents_tag.clone() })); - } - x => yield x, - } - } - } else { - yield ReturnSuccess::value(tagged_contents); - } + yield Ok(ReturnSuccess::Action(CommandAction::AutoConvert(tagged_contents, extension))) } else { yield ReturnSuccess::value(tagged_contents); } diff --git a/src/commands/plugin.rs b/src/commands/plugin.rs index c0051731f..9bb693bd0 100644 --- a/src/commands/plugin.rs +++ b/src/commands/plugin.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use derive_new::new; use log::trace; use nu_errors::ShellError; -use nu_protocol::{Primitive, ReturnSuccess, ReturnValue, Signature, UntaggedValue, Value}; +use nu_protocol::{Primitive, ReturnSuccess, ReturnValue, Scope, Signature, UntaggedValue, Value}; use serde::{self, Deserialize, Serialize}; use std::io::prelude::*; use std::io::BufReader; @@ -71,7 +71,10 @@ pub fn filter_plugin( ) -> Result { trace!("filter_plugin :: {}", path); - let args = args.evaluate_once(registry)?; + let args = args.evaluate_once_with_scope( + registry, + &Scope::it_value(UntaggedValue::string("$it").into_untagged_value()), + )?; let mut child = std::process::Command::new(path) .stdin(std::process::Stdio::piped()) diff --git a/src/context.rs b/src/context.rs index b6f0d2a81..128ac519e 100644 --- a/src/context.rs +++ b/src/context.rs @@ -71,8 +71,8 @@ impl CommandRegistry { #[derive(Clone)] pub struct Context { - registry: CommandRegistry, - host: Arc>>, + pub registry: CommandRegistry, + pub host: Arc>>, pub current_errors: Arc>>, pub ctrl_c: Arc, pub(crate) shell_manager: ShellManager,