Skip to content

Commit ea017d1

Browse files
Bahexfdncred
authored andcommitted
add table params support to url join and url build-query (nushell#14239)
Add `table<key, value>` support to `url join` for the `params` field, and as input to `url build-query` nushell#14162 # Description ```nushell { "scheme": "http", "username": "usr", "password": "pwd", "host": "localhost", "params": [ ["key", "value"]; ["par_1", "aaa"], ["par_2", "bbb"], ["par_1", "ccc"], ["par_2", "ddd"], ], "port": "1234", } | url join ``` ``` http://usr:pwd@localhost:1234?par_1=aaa&par_2=bbb&par_1=ccc&par_2=ddd ``` --- ```nushell [ ["key", "value"]; ["par_1", "aaa"], ["par_2", "bbb"], ["par_1", "ccc"], ["par_2", "ddd"], ] | url build-query ``` ``` par_1=aaa&par_2=bbb&par_1=ccc&par_2=ddd ``` # User-Facing Changes ## `url build-query` - can no longer accept one row table input as if it were a record --------- Co-authored-by: Darren Schroeder <[email protected]>
1 parent d1f1055 commit ea017d1

File tree

4 files changed

+196
-69
lines changed

4 files changed

+196
-69
lines changed

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

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use nu_engine::command_prelude::*;
22

3-
use super::query::record_to_query_string;
3+
use super::query::{record_to_query_string, table_to_query_string};
44

55
#[derive(Clone)]
66
pub struct SubCommand;
@@ -14,7 +14,10 @@ impl Command for SubCommand {
1414
Signature::build("url build-query")
1515
.input_output_types(vec![
1616
(Type::record(), Type::String),
17-
(Type::table(), Type::String),
17+
(
18+
Type::Table([("key".into(), Type::Any), ("value".into(), Type::Any)].into()),
19+
Type::String,
20+
),
1821
])
1922
.category(Category::Network)
2023
}
@@ -34,11 +37,6 @@ impl Command for SubCommand {
3437
example: r#"{ mode:normal userid:31415 } | url build-query"#,
3538
result: Some(Value::test_string("mode=normal&userid=31415")),
3639
},
37-
Example {
38-
description: "Outputs a query string representing the contents of this 1-row table",
39-
example: r#"[[foo bar]; ["1" "2"]] | url build-query"#,
40-
result: Some(Value::test_string("foo=1&bar=2")),
41-
},
4240
Example {
4341
description: "Outputs a query string representing the contents of this record, with a value that needs to be url-encoded",
4442
example: r#"{a:"AT&T", b: "AT T"} | url build-query"#,
@@ -49,6 +47,11 @@ impl Command for SubCommand {
4947
example: r#"{a: ["one", "two"], b: "three"} | url build-query"#,
5048
result: Some(Value::test_string("a=one&a=two&b=three")),
5149
},
50+
Example {
51+
description: "Outputs a query string representing the contents of this table containing key-value pairs",
52+
example: r#"[[key, value]; [a, one], [a, two], [b, three], [a, four]] | url build-query"#,
53+
result: Some(Value::test_string("a=one&a=two&b=three&a=four")),
54+
},
5255
]
5356
}
5457

@@ -60,32 +63,25 @@ impl Command for SubCommand {
6063
input: PipelineData,
6164
) -> Result<PipelineData, ShellError> {
6265
let head = call.head;
63-
to_url(input, head)
66+
let input_span = input.span().unwrap_or(head);
67+
let value = input.into_value(input_span)?;
68+
let span = value.span();
69+
let output = match value {
70+
Value::Record { ref val, .. } => record_to_query_string(val, span, head),
71+
Value::List { ref vals, .. } => table_to_query_string(vals, span, head),
72+
// Propagate existing errors
73+
Value::Error { error, .. } => Err(*error),
74+
other => Err(ShellError::UnsupportedInput {
75+
msg: "Expected a record or table from pipeline".to_string(),
76+
input: "value originates from here".into(),
77+
msg_span: head,
78+
input_span: other.span(),
79+
}),
80+
};
81+
Ok(Value::string(output?, head).into_pipeline_data())
6482
}
6583
}
6684

67-
fn to_url(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
68-
let output: Result<String, ShellError> = input
69-
.into_iter()
70-
.map(move |value| {
71-
let span = value.span();
72-
match value {
73-
Value::Record { ref val, .. } => record_to_query_string(val, span, head),
74-
// Propagate existing errors
75-
Value::Error { error, .. } => Err(*error),
76-
other => Err(ShellError::UnsupportedInput {
77-
msg: "Expected a table from pipeline".to_string(),
78-
input: "value originates from here".into(),
79-
msg_span: head,
80-
input_span: other.span(),
81-
}),
82-
}
83-
})
84-
.collect();
85-
86-
Ok(Value::string(output?, head).into_pipeline_data())
87-
}
88-
8985
#[cfg(test)]
9086
mod test {
9187
use super::*;

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

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use nu_engine::command_prelude::*;
22

3-
use super::query::record_to_query_string;
3+
use super::query::{record_to_query_string, table_to_query_string};
44

55
#[derive(Clone)]
66
pub struct SubCommand;
@@ -112,7 +112,7 @@ impl Command for SubCommand {
112112
.into_owned()
113113
.into_iter()
114114
.try_fold(UrlComponents::new(), |url, (k, v)| {
115-
url.add_component(k, v, span, engine_state)
115+
url.add_component(k, v, head, engine_state)
116116
});
117117

118118
url_components?.to_url(span)
@@ -155,7 +155,7 @@ impl UrlComponents {
155155
self,
156156
key: String,
157157
value: Value,
158-
span: Span,
158+
head: Span,
159159
engine_state: &EngineState,
160160
) -> Result<Self, ShellError> {
161161
let value_span = value.span();
@@ -194,40 +194,41 @@ impl UrlComponents {
194194
}
195195

196196
if key == "params" {
197-
return match value {
198-
Value::Record { ref val, .. } => {
199-
let mut qs = record_to_query_string(val, value_span, span)?;
200-
201-
qs = if !qs.trim().is_empty() {
202-
format!("?{qs}")
203-
} else {
204-
qs
205-
};
206-
207-
if let Some(q) = self.query {
208-
if q != qs {
209-
// if query is present it means that also query_span is set.
210-
return Err(ShellError::IncompatibleParameters {
211-
left_message: format!("Mismatch, qs from params is: {qs}"),
212-
left_span: value_span,
213-
right_message: format!("instead query is: {q}"),
214-
right_span: self.query_span.unwrap_or(Span::unknown()),
215-
});
216-
}
217-
}
218-
219-
Ok(Self {
220-
query: Some(qs),
221-
params_span: Some(value_span),
222-
..self
197+
let mut qs = match value {
198+
Value::Record { ref val, .. } => record_to_query_string(val, value_span, head)?,
199+
Value::List { ref vals, .. } => table_to_query_string(vals, value_span, head)?,
200+
Value::Error { error, .. } => return Err(*error),
201+
other => {
202+
return Err(ShellError::IncompatibleParametersSingle {
203+
msg: String::from("Key params has to be a record or a table"),
204+
span: other.span(),
223205
})
224206
}
225-
Value::Error { error, .. } => Err(*error),
226-
other => Err(ShellError::IncompatibleParametersSingle {
227-
msg: String::from("Key params has to be a record"),
228-
span: other.span(),
229-
}),
230207
};
208+
209+
qs = if !qs.trim().is_empty() {
210+
format!("?{qs}")
211+
} else {
212+
qs
213+
};
214+
215+
if let Some(q) = self.query {
216+
if q != qs {
217+
// if query is present it means that also query_span is set.
218+
return Err(ShellError::IncompatibleParameters {
219+
left_message: format!("Mismatch, query string from params is: {qs}"),
220+
left_span: value_span,
221+
right_message: format!("instead query is: {q}"),
222+
right_span: self.query_span.unwrap_or(Span::unknown()),
223+
});
224+
}
225+
}
226+
227+
return Ok(Self {
228+
query: Some(qs),
229+
params_span: Some(value_span),
230+
..self
231+
});
231232
}
232233

233234
// apart from port and params all other keys are strings.
@@ -267,7 +268,7 @@ impl UrlComponents {
267268
return Err(ShellError::IncompatibleParameters {
268269
left_message: format!("Mismatch, query param is: {s}"),
269270
left_span: value_span,
270-
right_message: format!("instead qs from params is: {q}"),
271+
right_message: format!("instead query string from params is: {q}"),
271272
right_span: self.params_span.unwrap_or(Span::unknown()),
272273
});
273274
}
@@ -293,7 +294,7 @@ impl UrlComponents {
293294
&ShellError::GenericError {
294295
error: format!("'{key}' is not a valid URL field"),
295296
msg: format!("remove '{key}' col from input record"),
296-
span: Some(span),
297+
span: Some(value_span),
297298
help: None,
298299
inner: vec![],
299300
},

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::borrow::Cow;
2+
13
use nu_protocol::{IntoValue, Record, ShellError, Span, Type, Value};
24

35
pub fn record_to_query_string(
@@ -43,6 +45,52 @@ pub fn record_to_query_string(
4345
})
4446
}
4547

48+
pub fn table_to_query_string(
49+
table: &[Value],
50+
span: Span,
51+
head: Span,
52+
) -> Result<String, ShellError> {
53+
let row_vec = table
54+
.iter()
55+
.map(|val| match val {
56+
Value::Record { val, internal_span } => key_value_from_record(val, *internal_span),
57+
_ => Err(ShellError::UnsupportedInput {
58+
msg: "expected a table".into(),
59+
input: "not a table, contains non-record values".into(),
60+
msg_span: head,
61+
input_span: span,
62+
}),
63+
})
64+
.collect::<Result<Vec<_>, ShellError>>()?;
65+
66+
serde_urlencoded::to_string(row_vec).map_err(|_| ShellError::CantConvert {
67+
to_type: "URL".into(),
68+
from_type: Type::table().to_string(),
69+
span: head,
70+
help: None,
71+
})
72+
}
73+
74+
fn key_value_from_record(record: &Record, span: Span) -> Result<(Cow<str>, Cow<str>), ShellError> {
75+
let key = record
76+
.get("key")
77+
.ok_or_else(|| ShellError::CantFindColumn {
78+
col_name: "key".into(),
79+
span: None,
80+
src_span: span,
81+
})?
82+
.coerce_str()?;
83+
let value = record
84+
.get("value")
85+
.ok_or_else(|| ShellError::CantFindColumn {
86+
col_name: "value".into(),
87+
span: None,
88+
src_span: span,
89+
})?
90+
.coerce_str()?;
91+
Ok((key, value))
92+
}
93+
4694
pub fn query_string_to_table(query: &str, head: Span, span: Span) -> Result<Value, ShellError> {
4795
let params = serde_urlencoded::from_str::<Vec<(String, String)>>(query)
4896
.map_err(|_| ShellError::UnsupportedInput {

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

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ fn url_join_with_different_query_and_params() {
156156

157157
assert!(actual
158158
.err
159-
.contains("Mismatch, qs from params is: ?par_1=aaab&par_2=bbb"));
159+
.contains("Mismatch, query string from params is: ?par_1=aaab&par_2=bbb"));
160160
assert!(actual
161161
.err
162162
.contains("instead query is: ?par_1=aaa&par_2=bbb"));
@@ -183,7 +183,7 @@ fn url_join_with_different_query_and_params() {
183183
.contains("Mismatch, query param is: par_1=aaa&par_2=bbb"));
184184
assert!(actual
185185
.err
186-
.contains("instead qs from params is: ?par_1=aaab&par_2=bbb"));
186+
.contains("instead query string from params is: ?par_1=aaab&par_2=bbb"));
187187
}
188188

189189
#[test]
@@ -201,7 +201,9 @@ fn url_join_with_invalid_params() {
201201
"#
202202
));
203203

204-
assert!(actual.err.contains("Key params has to be a record"));
204+
assert!(actual
205+
.err
206+
.contains("Key params has to be a record or a table"));
205207
}
206208

207209
#[test]
@@ -346,3 +348,83 @@ fn url_join_with_empty_params() {
346348

347349
assert_eq!(actual.out, "https://localhost/foo");
348350
}
351+
352+
#[test]
353+
fn url_join_with_list_in_params() {
354+
let actual = nu!(pipeline(
355+
r#"
356+
{
357+
"scheme": "http",
358+
"username": "usr",
359+
"password": "pwd",
360+
"host": "localhost",
361+
"params": {
362+
"par_1": "aaa",
363+
"par_2": ["bbb", "ccc"]
364+
},
365+
"port": "1234",
366+
} | url join
367+
"#
368+
));
369+
370+
assert_eq!(
371+
actual.out,
372+
"http://usr:pwd@localhost:1234?par_1=aaa&par_2=bbb&par_2=ccc"
373+
);
374+
}
375+
376+
#[test]
377+
fn url_join_with_params_table() {
378+
let actual = nu!(pipeline(
379+
r#"
380+
{
381+
"scheme": "http",
382+
"username": "usr",
383+
"password": "pwd",
384+
"host": "localhost",
385+
"params": [
386+
["key", "value"];
387+
["par_1", "aaa"],
388+
["par_2", "bbb"],
389+
["par_1", "ccc"],
390+
["par_2", "ddd"],
391+
],
392+
"port": "1234",
393+
} | url join
394+
"#
395+
));
396+
397+
assert_eq!(
398+
actual.out,
399+
"http://usr:pwd@localhost:1234?par_1=aaa&par_2=bbb&par_1=ccc&par_2=ddd"
400+
);
401+
}
402+
403+
#[test]
404+
fn url_join_with_params_invalid_table() {
405+
let actual = nu!(pipeline(
406+
r#"
407+
{
408+
"scheme": "http",
409+
"username": "usr",
410+
"password": "pwd",
411+
"host": "localhost",
412+
"params": (
413+
[
414+
["key", "value"];
415+
["par_1", "aaa"],
416+
["par_2", "bbb"],
417+
["par_1", "ccc"],
418+
["par_2", "ddd"],
419+
] ++ ["not a record"]
420+
),
421+
"port": "1234",
422+
} | url join
423+
"#
424+
));
425+
426+
assert!(actual.err.contains("expected a table"));
427+
assert!(actual
428+
.err
429+
.contains("not a table, contains non-record values"));
430+
}

0 commit comments

Comments
 (0)