diff --git a/Cargo.lock b/Cargo.lock index cd99727678..075a712032 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,6 +1563,7 @@ dependencies = [ "serde_bytes 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde_ini 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)", "shellexpand 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "subprocess 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 46fd25c2c6..cfe107e9be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ pin-utils = "0.1.0-alpha.4" num-bigint = { version = "0.2.3", features = ["serde"] } bigdecimal = { version = "0.1.0", features = ["serde"] } natural = "0.3.0" +serde_urlencoded = "0.6.1" neso = { version = "0.5.0", optional = true } crossterm = { version = "0.10.2", optional = true } diff --git a/src/cli.rs b/src/cli.rs index c02b919066..8ed2b9bd55 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -239,6 +239,7 @@ pub async fn cli() -> Result<(), Box> { whole_stream_command(ToDB), whole_stream_command(ToTOML), whole_stream_command(ToTSV), + whole_stream_command(ToURL), whole_stream_command(ToYAML), whole_stream_command(SortBy), whole_stream_command(Tags), @@ -253,6 +254,7 @@ pub async fn cli() -> Result<(), Box> { whole_stream_command(FromDB), whole_stream_command(FromSQLite), whole_stream_command(FromTOML), + whole_stream_command(FromURL), whole_stream_command(FromXML), whole_stream_command(FromYAML), whole_stream_command(FromYML), diff --git a/src/commands.rs b/src/commands.rs index 5f9b0e5d5e..72c07e38e6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -24,6 +24,7 @@ pub(crate) mod from_json; pub(crate) mod from_sqlite; pub(crate) mod from_toml; pub(crate) mod from_tsv; +pub(crate) mod from_url; pub(crate) mod from_xml; pub(crate) mod from_yaml; pub(crate) mod get; @@ -60,6 +61,7 @@ pub(crate) mod to_json; pub(crate) mod to_sqlite; pub(crate) mod to_toml; pub(crate) mod to_tsv; +pub(crate) mod to_url; pub(crate) mod to_yaml; pub(crate) mod trim; pub(crate) mod version; @@ -91,6 +93,7 @@ pub(crate) use from_sqlite::FromDB; pub(crate) use from_sqlite::FromSQLite; pub(crate) use from_toml::FromTOML; pub(crate) use from_tsv::FromTSV; +pub(crate) use from_url::FromURL; pub(crate) use from_xml::FromXML; pub(crate) use from_yaml::FromYAML; pub(crate) use from_yaml::FromYML; @@ -128,6 +131,7 @@ pub(crate) use to_sqlite::ToDB; pub(crate) use to_sqlite::ToSQLite; pub(crate) use to_toml::ToTOML; pub(crate) use to_tsv::ToTSV; +pub(crate) use to_url::ToURL; pub(crate) use to_yaml::ToYAML; pub(crate) use trim::Trim; pub(crate) use version::Version; diff --git a/src/commands/from_url.rs b/src/commands/from_url.rs new file mode 100644 index 0000000000..81113a83d4 --- /dev/null +++ b/src/commands/from_url.rs @@ -0,0 +1,85 @@ +use crate::commands::WholeStreamCommand; +use crate::data::{Primitive, TaggedDictBuilder, Value}; +use crate::prelude::*; + +pub struct FromURL; + +impl WholeStreamCommand for FromURL { + fn name(&self) -> &str { + "from-url" + } + + fn signature(&self) -> Signature { + Signature::build("from-url") + } + + fn usage(&self) -> &str { + "Parse url-encoded string as a table." + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + from_url(args, registry) + } +} + +fn from_url(args: CommandArgs, registry: &CommandRegistry) -> Result { + let args = args.evaluate_once(registry)?; + let tag = args.name_tag(); + let input = args.input; + + let stream = async_stream_block! { + let values: Vec> = input.values.collect().await; + + let mut concat_string = String::new(); + let mut latest_tag: Option = None; + + for value in values { + let value_tag = value.tag(); + latest_tag = Some(value_tag); + match value.item { + Value::Primitive(Primitive::String(s)) => { + concat_string.push_str(&s); + } + _ => yield Err(ShellError::labeled_error_with_secondary( + "Expected a string from pipeline", + "requires string input", + tag, + "value originates from here", + value_tag, + )), + + } + } + + let result = serde_urlencoded::from_str::>(&concat_string); + + match result { + Ok(result) => { + let mut row = TaggedDictBuilder::new(tag); + + for (k,v) in result { + row.insert(k, Value::string(v)); + } + + yield ReturnSuccess::value(row.into_tagged_value()); + } + _ => { + if let Some(last_tag) = latest_tag { + yield Err(ShellError::labeled_error_with_secondary( + "String not compatible with url-encoding", + "input not url-encoded", + tag, + "value originates from here", + last_tag, + )); + } + } + } + }; + + Ok(stream.to_output_stream()) +} diff --git a/src/commands/to_url.rs b/src/commands/to_url.rs new file mode 100644 index 0000000000..d98a765a29 --- /dev/null +++ b/src/commands/to_url.rs @@ -0,0 +1,85 @@ +use crate::commands::WholeStreamCommand; +use crate::data::Value; +use crate::prelude::*; + +pub struct ToURL; + +impl WholeStreamCommand for ToURL { + fn name(&self) -> &str { + "to-url" + } + + fn signature(&self) -> Signature { + Signature::build("to-url") + } + + fn usage(&self) -> &str { + "Convert table into url-encoded text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + to_url(args, registry) + } +} + +fn to_url(args: CommandArgs, registry: &CommandRegistry) -> Result { + let args = args.evaluate_once(registry)?; + let tag = args.name_tag(); + let input = args.input; + + let stream = async_stream_block! { + let input: Vec> = input.values.collect().await; + + for value in input { + match value { + Tagged { item: Value::Row(row), .. } => { + let mut row_vec = vec![]; + for (k,v) in row.entries { + match v.as_string() { + Ok(s) => { + row_vec.push((k.clone(), s)); + } + _ => { + yield Err(ShellError::labeled_error_with_secondary( + "Expected table with string values", + "requires table with strings", + tag, + "value originates from here", + v.tag, + )) + } + } + } + + match serde_urlencoded::to_string(row_vec) { + Ok(s) => { + yield ReturnSuccess::value(Value::string(s).tagged(tag)); + } + _ => { + yield Err(ShellError::labeled_error( + "Failed to convert to url-encoded", + "cannot url-encode", + tag, + )) + } + } + } + Tagged { tag: value_tag, .. } => { + yield Err(ShellError::labeled_error_with_secondary( + "Expected a table from pipeline", + "requires table input", + tag, + "value originates from here", + value_tag, + )) + } + } + } + }; + + Ok(stream.to_output_stream()) +} diff --git a/src/utils.rs b/src/utils.rs index 703f277254..6b1318f9e8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -464,6 +464,10 @@ mod tests { loc: fixtures().join("sample.ini"), at: 0 }, + Res { + loc: fixtures().join("sample.url"), + at: 0 + }, Res { loc: fixtures().join("sgml_description.json"), at: 0 diff --git a/tests/filters_test.rs b/tests/filters_test.rs index b08115a9c0..f994fa4494 100644 --- a/tests/filters_test.rs +++ b/tests/filters_test.rs @@ -423,6 +423,22 @@ fn can_convert_table_to_yaml_text_and_from_yaml_text_back_into_table() { assert_eq!(actual, "nushell"); } +#[test] +fn can_encode_and_decode_urlencoding() { + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open sample.url + | to-url + | from-url + | get cheese + | echo $it + "# + )); + + assert_eq!(actual, "comté"); +} + #[test] fn can_sort_by_column() { let actual = nu!( diff --git a/tests/fixtures/formats/sample.url b/tests/fixtures/formats/sample.url new file mode 100644 index 0000000000..361d70dbb6 --- /dev/null +++ b/tests/fixtures/formats/sample.url @@ -0,0 +1 @@ +bread=baguette&cheese=comt%C3%A9&meat=ham&fat=butter \ No newline at end of file