Skip to content

Commit 9d13a6b

Browse files
BahexNotTheDr01ds
authored andcommitted
Url split query (nushell#14211)
Addresses the following points from nushell#14162 > - There is no built-in counterpart to url build-query for splitting a query string There is `from url`, which, due to naming, is a little hard to discover and suffers from the following point > - url parse can create records with duplicate keys > - url parse's params should either: > - ~group the same keys into a list.~ > - instead of a record, be a key-value table. (table<key: string, value: string>) # Description ## `url split-query` Counterpart to `url build-query`, splits a url encoded query string to key value pairs, represented as `table<key: string, value: string>` ``` > "a=one&a=two&b=three" | url split-query ╭───┬─────┬───────╮ │ # │ key │ value │ ├───┼─────┼───────┤ │ 0 │ a │ one │ │ 1 │ a │ two │ │ 2 │ b │ three │ ╰───┴─────┴───────╯ ``` ## `url parse` The output's `param` field is now a table as well, mirroring the new `url split-query` ``` > 'http://localhost?a=one&a=two&b=three' | url parse ╭──────────┬─────────────────────╮ │ scheme │ http │ │ username │ │ │ password │ │ │ host │ localhost │ │ port │ │ │ path │ / │ │ query │ a=one&a=two&b=three │ │ fragment │ │ │ │ ╭───┬─────┬───────╮ │ │ params │ │ # │ key │ value │ │ │ │ ├───┼─────┼───────┤ │ │ │ │ 0 │ a │ one │ │ │ │ │ 1 │ a │ two │ │ │ │ │ 2 │ b │ three │ │ │ │ ╰───┴─────┴───────╯ │ ╰──────────┴─────────────────────╯ ``` # User-Facing Changes - `url parse`'s output has the mentioned change, which is backwards incompatible.
1 parent 4e9b60f commit 9d13a6b

File tree

6 files changed

+201
-56
lines changed

6 files changed

+201
-56
lines changed

crates/nu-command/src/default_context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
387387
HttpOptions,
388388
Url,
389389
UrlBuildQuery,
390+
UrlSplitQuery,
390391
UrlDecode,
391392
UrlEncode,
392393
UrlJoin,

crates/nu-command/src/network/url/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ mod encode;
44
mod join;
55
mod parse;
66
mod query;
7+
mod split_query;
78
mod url_;
89

910
pub use self::parse::SubCommand as UrlParse;
1011
pub use build_query::SubCommand as UrlBuildQuery;
1112
pub use decode::SubCommand as UrlDecode;
1213
pub use encode::SubCommand as UrlEncode;
1314
pub use join::SubCommand as UrlJoin;
15+
pub use split_query::SubCommand as UrlSplitQuery;
1416
pub use url_::Url;

crates/nu-command/src/network/url/parse.rs

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use nu_engine::command_prelude::*;
22
use nu_protocol::Config;
33
use url::Url;
44

5+
use super::query::query_string_to_table;
6+
57
#[derive(Clone)]
68
pub struct SubCommand;
79

@@ -53,21 +55,22 @@ impl Command for SubCommand {
5355
fn examples(&self) -> Vec<Example> {
5456
vec![Example {
5557
description: "Parses a url",
56-
example: "'http://user123:[email protected]:8081/foo/bar?param1=section&p2=&f[name]=vldc#hello' | url parse",
58+
example: "'http://user123:[email protected]:8081/foo/bar?param1=section&p2=&f[name]=vldc&f[no]=42#hello' | url parse",
5759
result: Some(Value::test_record(record! {
5860
"scheme" => Value::test_string("http"),
5961
"username" => Value::test_string("user123"),
6062
"password" => Value::test_string("pass567"),
6163
"host" => Value::test_string("www.example.com"),
6264
"port" => Value::test_string("8081"),
6365
"path" => Value::test_string("/foo/bar"),
64-
"query" => Value::test_string("param1=section&p2=&f[name]=vldc"),
66+
"query" => Value::test_string("param1=section&p2=&f[name]=vldc&f[no]=42"),
6567
"fragment" => Value::test_string("hello"),
66-
"params" => Value::test_record(record! {
67-
"param1" => Value::test_string("section"),
68-
"p2" => Value::test_string(""),
69-
"f[name]" => Value::test_string("vldc"),
70-
}),
68+
"params" => Value::test_list(vec![
69+
Value::test_record(record! {"key" => Value::test_string("param1"), "value" => Value::test_string("section") }),
70+
Value::test_record(record! {"key" => Value::test_string("p2"), "value" => Value::test_string("") }),
71+
Value::test_record(record! {"key" => Value::test_string("f[name]"), "value" => Value::test_string("vldc") }),
72+
Value::test_record(record! {"key" => Value::test_string("f[no]"), "value" => Value::test_string("42") }),
73+
]),
7174
})),
7275
}]
7376
}
@@ -80,54 +83,41 @@ fn get_url_string(value: &Value, config: &Config) -> String {
8083
fn parse(value: Value, head: Span, config: &Config) -> Result<PipelineData, ShellError> {
8184
let url_string = get_url_string(&value, config);
8285

83-
let result_url = Url::parse(url_string.as_str());
84-
8586
// This is the span of the original string, not the call head.
8687
let span = value.span();
8788

88-
match result_url {
89-
Ok(url) => {
90-
let params =
91-
serde_urlencoded::from_str::<Vec<(String, String)>>(url.query().unwrap_or(""));
92-
match params {
93-
Ok(result) => {
94-
let params = result
95-
.into_iter()
96-
.map(|(k, v)| (k, Value::string(v, head)))
97-
.collect();
98-
99-
let port = url.port().map(|p| p.to_string()).unwrap_or_default();
100-
101-
let record = record! {
102-
"scheme" => Value::string(url.scheme(), head),
103-
"username" => Value::string(url.username(), head),
104-
"password" => Value::string(url.password().unwrap_or(""), head),
105-
"host" => Value::string(url.host_str().unwrap_or(""), head),
106-
"port" => Value::string(port, head),
107-
"path" => Value::string(url.path(), head),
108-
"query" => Value::string(url.query().unwrap_or(""), head),
109-
"fragment" => Value::string(url.fragment().unwrap_or(""), head),
110-
"params" => Value::record(params, head),
111-
};
112-
113-
Ok(PipelineData::Value(Value::record(record, head), None))
114-
}
115-
_ => Err(ShellError::UnsupportedInput {
116-
msg: "String not compatible with url-encoding".to_string(),
117-
input: "value originates from here".into(),
118-
msg_span: head,
119-
input_span: span,
120-
}),
121-
}
122-
}
123-
Err(_e) => Err(ShellError::UnsupportedInput {
124-
msg: "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com"
125-
.to_string(),
89+
let url = Url::parse(url_string.as_str()).map_err(|_| ShellError::UnsupportedInput {
90+
msg: "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com"
91+
.to_string(),
92+
input: "value originates from here".into(),
93+
msg_span: head,
94+
input_span: span,
95+
})?;
96+
97+
let params = query_string_to_table(url.query().unwrap_or(""), head, span).map_err(|_| {
98+
ShellError::UnsupportedInput {
99+
msg: "String not compatible with url-encoding".to_string(),
126100
input: "value originates from here".into(),
127101
msg_span: head,
128102
input_span: span,
129-
}),
130-
}
103+
}
104+
})?;
105+
106+
let port = url.port().map(|p| p.to_string()).unwrap_or_default();
107+
108+
let record = record! {
109+
"scheme" => Value::string(url.scheme(), head),
110+
"username" => Value::string(url.username(), head),
111+
"password" => Value::string(url.password().unwrap_or(""), head),
112+
"host" => Value::string(url.host_str().unwrap_or(""), head),
113+
"port" => Value::string(port, head),
114+
"path" => Value::string(url.path(), head),
115+
"query" => Value::string(url.query().unwrap_or(""), head),
116+
"fragment" => Value::string(url.fragment().unwrap_or(""), head),
117+
"params" => params,
118+
};
119+
120+
Ok(PipelineData::Value(Value::record(record, head), None))
131121
}
132122

133123
#[cfg(test)]

crates/nu-command/src/network/url/query.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use nu_protocol::{Record, ShellError, Span, Type, Value};
1+
use nu_protocol::{IntoValue, Record, ShellError, Span, Type, Value};
22

33
pub fn record_to_query_string(
44
record: &Record,
@@ -42,3 +42,26 @@ pub fn record_to_query_string(
4242
help: None,
4343
})
4444
}
45+
46+
pub fn query_string_to_table(query: &str, head: Span, span: Span) -> Result<Value, ShellError> {
47+
let params = serde_urlencoded::from_str::<Vec<(String, String)>>(query)
48+
.map_err(|_| ShellError::UnsupportedInput {
49+
msg: "String not compatible with url-encoding".to_string(),
50+
input: "value originates from here".into(),
51+
msg_span: head,
52+
input_span: span,
53+
})?
54+
.into_iter()
55+
.map(|(key, value)| {
56+
Value::record(
57+
nu_protocol::record! {
58+
"key" => key.into_value(head),
59+
"value" => value.into_value(head)
60+
},
61+
head,
62+
)
63+
})
64+
.collect::<Vec<_>>();
65+
66+
Ok(Value::list(params, head))
67+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use nu_engine::command_prelude::*;
2+
3+
use super::query::query_string_to_table;
4+
5+
#[derive(Clone)]
6+
pub struct SubCommand;
7+
8+
impl Command for SubCommand {
9+
fn name(&self) -> &str {
10+
"url split-query"
11+
}
12+
13+
fn signature(&self) -> Signature {
14+
Signature::build("url split-query")
15+
.input_output_types(vec![(
16+
Type::String,
17+
Type::Table([("key".into(), Type::String), ("value".into(), Type::String)].into()),
18+
)])
19+
.category(Category::Network)
20+
}
21+
22+
fn description(&self) -> &str {
23+
"Converts query string into table applying percent-decoding."
24+
}
25+
26+
fn search_terms(&self) -> Vec<&str> {
27+
vec!["convert", "record", "table"]
28+
}
29+
30+
fn examples(&self) -> Vec<Example> {
31+
vec![
32+
Example {
33+
description: "Outputs a table representing the contents of this query string",
34+
example: r#""mode=normal&userid=31415" | url split-query"#,
35+
result: Some(Value::test_list(vec![
36+
Value::test_record(record!{
37+
"key" => Value::test_string("mode"),
38+
"value" => Value::test_string("normal"),
39+
}),
40+
Value::test_record(record!{
41+
"key" => Value::test_string("userid"),
42+
"value" => Value::test_string("31415"),
43+
})
44+
])),
45+
},
46+
Example {
47+
description: "Outputs a table representing the contents of this query string, url-decoding the values",
48+
example: r#""a=AT%26T&b=AT+T" | url split-query"#,
49+
result: Some(Value::test_list(vec![
50+
Value::test_record(record!{
51+
"key" => Value::test_string("a"),
52+
"value" => Value::test_string("AT&T"),
53+
}),
54+
Value::test_record(record!{
55+
"key" => Value::test_string("b"),
56+
"value" => Value::test_string("AT T"),
57+
}),
58+
])),
59+
},
60+
Example {
61+
description: "Outputs a table representing the contents of this query string",
62+
example: r#""a=one&a=two&b=three" | url split-query"#,
63+
result: Some(Value::test_list(vec![
64+
Value::test_record(record!{
65+
"key" => Value::test_string("a"),
66+
"value" => Value::test_string("one"),
67+
}),
68+
Value::test_record(record!{
69+
"key" => Value::test_string("a"),
70+
"value" => Value::test_string("two"),
71+
}),
72+
Value::test_record(record!{
73+
"key" => Value::test_string("b"),
74+
"value" => Value::test_string("three"),
75+
}),
76+
])),
77+
},
78+
]
79+
}
80+
81+
fn run(
82+
&self,
83+
engine_state: &EngineState,
84+
stack: &mut Stack,
85+
call: &Call,
86+
input: PipelineData,
87+
) -> Result<PipelineData, ShellError> {
88+
let value = input.into_value(call.head)?;
89+
let span = value.span();
90+
let query = value.to_expanded_string("", &stack.get_config(engine_state));
91+
let table = query_string_to_table(&query, call.head, span)?;
92+
Ok(PipelineData::Value(table, None))
93+
}
94+
}
95+
96+
#[cfg(test)]
97+
mod test {
98+
use super::*;
99+
100+
#[test]
101+
fn test_examples() {
102+
use crate::test_examples;
103+
104+
test_examples(SubCommand {})
105+
}
106+
}

crates/nu-command/tests/commands/url/parse.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fn url_parse_simple() {
1515
path: '/',
1616
query: '',
1717
fragment: '',
18-
params: {}
18+
params: []
1919
}
2020
"#
2121
));
@@ -37,7 +37,7 @@ fn url_parse_with_port() {
3737
path: '/',
3838
query: '',
3939
fragment: '',
40-
params: {}
40+
params: []
4141
}
4242
"#
4343
));
@@ -60,7 +60,7 @@ fn url_parse_with_path() {
6060
path: '/def/ghj',
6161
query: '',
6262
fragment: '',
63-
params: {}
63+
params: []
6464
}
6565
"#
6666
));
@@ -83,7 +83,30 @@ fn url_parse_with_params() {
8383
path: '/def/ghj',
8484
query: 'param1=11&param2=',
8585
fragment: '',
86-
params: {param1: '11', param2: ''}
86+
params: [[key, value]; ["param1", "11"], ["param2", ""]]
87+
}
88+
"#
89+
));
90+
91+
assert_eq!(actual.out, "true");
92+
}
93+
94+
#[test]
95+
fn url_parse_with_duplicate_params() {
96+
let actual = nu!(pipeline(
97+
r#"
98+
("http://www.abc.com:8811/def/ghj?param1=11&param2=&param1=22"
99+
| url parse)
100+
== {
101+
scheme: 'http',
102+
username: '',
103+
password: '',
104+
host: 'www.abc.com',
105+
port: '8811',
106+
path: '/def/ghj',
107+
query: 'param1=11&param2=&param1=22',
108+
fragment: '',
109+
params: [[key, value]; ["param1", "11"], ["param2", ""], ["param1", "22"]]
87110
}
88111
"#
89112
));
@@ -106,7 +129,7 @@ fn url_parse_with_fragment() {
106129
path: '/def/ghj',
107130
query: 'param1=11&param2=',
108131
fragment: 'hello-fragment',
109-
params: {param1: '11', param2: ''}
132+
params: [[key, value]; ["param1", "11"], ["param2", ""]]
110133
}
111134
"#
112135
));
@@ -129,7 +152,7 @@ fn url_parse_with_username_and_password() {
129152
path: '/def/ghj',
130153
query: 'param1=11&param2=',
131154
fragment: 'hello-fragment',
132-
params: {param1: '11', param2: ''}
155+
params: [[key, value]; ["param1", "11"], ["param2", ""]]
133156
}
134157
"#
135158
));

0 commit comments

Comments
 (0)