mirror of
https://github.com/nushell/nushell
synced 2024-12-26 21:13:19 +00:00
add table params support to url join
and url build-query
(#14239)
Add `table<key, value>` support to `url join` for the `params` field, and as input to `url build-query` #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 <343840+fdncred@users.noreply.github.com>
This commit is contained in:
parent
cc0259bbed
commit
c7e128eed1
4 changed files with 196 additions and 69 deletions
|
@ -1,6 +1,6 @@
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
use super::query::record_to_query_string;
|
use super::query::{record_to_query_string, table_to_query_string};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SubCommand;
|
pub struct SubCommand;
|
||||||
|
@ -14,7 +14,10 @@ impl Command for SubCommand {
|
||||||
Signature::build("url build-query")
|
Signature::build("url build-query")
|
||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::record(), Type::String),
|
(Type::record(), Type::String),
|
||||||
(Type::table(), Type::String),
|
(
|
||||||
|
Type::Table([("key".into(), Type::Any), ("value".into(), Type::Any)].into()),
|
||||||
|
Type::String,
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.category(Category::Network)
|
.category(Category::Network)
|
||||||
}
|
}
|
||||||
|
@ -34,11 +37,6 @@ impl Command for SubCommand {
|
||||||
example: r#"{ mode:normal userid:31415 } | url build-query"#,
|
example: r#"{ mode:normal userid:31415 } | url build-query"#,
|
||||||
result: Some(Value::test_string("mode=normal&userid=31415")),
|
result: Some(Value::test_string("mode=normal&userid=31415")),
|
||||||
},
|
},
|
||||||
Example {
|
|
||||||
description: "Outputs a query string representing the contents of this 1-row table",
|
|
||||||
example: r#"[[foo bar]; ["1" "2"]] | url build-query"#,
|
|
||||||
result: Some(Value::test_string("foo=1&bar=2")),
|
|
||||||
},
|
|
||||||
Example {
|
Example {
|
||||||
description: "Outputs a query string representing the contents of this record, with a value that needs to be url-encoded",
|
description: "Outputs a query string representing the contents of this record, with a value that needs to be url-encoded",
|
||||||
example: r#"{a:"AT&T", b: "AT T"} | url build-query"#,
|
example: r#"{a:"AT&T", b: "AT T"} | url build-query"#,
|
||||||
|
@ -49,6 +47,11 @@ impl Command for SubCommand {
|
||||||
example: r#"{a: ["one", "two"], b: "three"} | url build-query"#,
|
example: r#"{a: ["one", "two"], b: "three"} | url build-query"#,
|
||||||
result: Some(Value::test_string("a=one&a=two&b=three")),
|
result: Some(Value::test_string("a=one&a=two&b=three")),
|
||||||
},
|
},
|
||||||
|
Example {
|
||||||
|
description: "Outputs a query string representing the contents of this table containing key-value pairs",
|
||||||
|
example: r#"[[key, value]; [a, one], [a, two], [b, three], [a, four]] | url build-query"#,
|
||||||
|
result: Some(Value::test_string("a=one&a=two&b=three&a=four")),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,31 +63,24 @@ impl Command for SubCommand {
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
to_url(input, head)
|
let input_span = input.span().unwrap_or(head);
|
||||||
}
|
let value = input.into_value(input_span)?;
|
||||||
}
|
|
||||||
|
|
||||||
fn to_url(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
|
|
||||||
let output: Result<String, ShellError> = input
|
|
||||||
.into_iter()
|
|
||||||
.map(move |value| {
|
|
||||||
let span = value.span();
|
let span = value.span();
|
||||||
match value {
|
let output = match value {
|
||||||
Value::Record { ref val, .. } => record_to_query_string(val, span, head),
|
Value::Record { ref val, .. } => record_to_query_string(val, span, head),
|
||||||
|
Value::List { ref vals, .. } => table_to_query_string(vals, span, head),
|
||||||
// Propagate existing errors
|
// Propagate existing errors
|
||||||
Value::Error { error, .. } => Err(*error),
|
Value::Error { error, .. } => Err(*error),
|
||||||
other => Err(ShellError::UnsupportedInput {
|
other => Err(ShellError::UnsupportedInput {
|
||||||
msg: "Expected a table from pipeline".to_string(),
|
msg: "Expected a record or table from pipeline".to_string(),
|
||||||
input: "value originates from here".into(),
|
input: "value originates from here".into(),
|
||||||
msg_span: head,
|
msg_span: head,
|
||||||
input_span: other.span(),
|
input_span: other.span(),
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Value::string(output?, head).into_pipeline_data())
|
Ok(Value::string(output?, head).into_pipeline_data())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
use super::query::record_to_query_string;
|
use super::query::{record_to_query_string, table_to_query_string};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SubCommand;
|
pub struct SubCommand;
|
||||||
|
@ -112,7 +112,7 @@ impl Command for SubCommand {
|
||||||
.into_owned()
|
.into_owned()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_fold(UrlComponents::new(), |url, (k, v)| {
|
.try_fold(UrlComponents::new(), |url, (k, v)| {
|
||||||
url.add_component(k, v, span, engine_state)
|
url.add_component(k, v, head, engine_state)
|
||||||
});
|
});
|
||||||
|
|
||||||
url_components?.to_url(span)
|
url_components?.to_url(span)
|
||||||
|
@ -155,7 +155,7 @@ impl UrlComponents {
|
||||||
self,
|
self,
|
||||||
key: String,
|
key: String,
|
||||||
value: Value,
|
value: Value,
|
||||||
span: Span,
|
head: Span,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
) -> Result<Self, ShellError> {
|
) -> Result<Self, ShellError> {
|
||||||
let value_span = value.span();
|
let value_span = value.span();
|
||||||
|
@ -194,9 +194,17 @@ impl UrlComponents {
|
||||||
}
|
}
|
||||||
|
|
||||||
if key == "params" {
|
if key == "params" {
|
||||||
return match value {
|
let mut qs = match value {
|
||||||
Value::Record { ref val, .. } => {
|
Value::Record { ref val, .. } => record_to_query_string(val, value_span, head)?,
|
||||||
let mut qs = record_to_query_string(val, value_span, span)?;
|
Value::List { ref vals, .. } => table_to_query_string(vals, value_span, head)?,
|
||||||
|
Value::Error { error, .. } => return Err(*error),
|
||||||
|
other => {
|
||||||
|
return Err(ShellError::IncompatibleParametersSingle {
|
||||||
|
msg: String::from("Key params has to be a record or a table"),
|
||||||
|
span: other.span(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
qs = if !qs.trim().is_empty() {
|
qs = if !qs.trim().is_empty() {
|
||||||
format!("?{qs}")
|
format!("?{qs}")
|
||||||
|
@ -208,7 +216,7 @@ impl UrlComponents {
|
||||||
if q != qs {
|
if q != qs {
|
||||||
// if query is present it means that also query_span is set.
|
// if query is present it means that also query_span is set.
|
||||||
return Err(ShellError::IncompatibleParameters {
|
return Err(ShellError::IncompatibleParameters {
|
||||||
left_message: format!("Mismatch, qs from params is: {qs}"),
|
left_message: format!("Mismatch, query string from params is: {qs}"),
|
||||||
left_span: value_span,
|
left_span: value_span,
|
||||||
right_message: format!("instead query is: {q}"),
|
right_message: format!("instead query is: {q}"),
|
||||||
right_span: self.query_span.unwrap_or(Span::unknown()),
|
right_span: self.query_span.unwrap_or(Span::unknown()),
|
||||||
|
@ -216,18 +224,11 @@ impl UrlComponents {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
return Ok(Self {
|
||||||
query: Some(qs),
|
query: Some(qs),
|
||||||
params_span: Some(value_span),
|
params_span: Some(value_span),
|
||||||
..self
|
..self
|
||||||
})
|
});
|
||||||
}
|
|
||||||
Value::Error { error, .. } => Err(*error),
|
|
||||||
other => Err(ShellError::IncompatibleParametersSingle {
|
|
||||||
msg: String::from("Key params has to be a record"),
|
|
||||||
span: other.span(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apart from port and params all other keys are strings.
|
// apart from port and params all other keys are strings.
|
||||||
|
@ -267,7 +268,7 @@ impl UrlComponents {
|
||||||
return Err(ShellError::IncompatibleParameters {
|
return Err(ShellError::IncompatibleParameters {
|
||||||
left_message: format!("Mismatch, query param is: {s}"),
|
left_message: format!("Mismatch, query param is: {s}"),
|
||||||
left_span: value_span,
|
left_span: value_span,
|
||||||
right_message: format!("instead qs from params is: {q}"),
|
right_message: format!("instead query string from params is: {q}"),
|
||||||
right_span: self.params_span.unwrap_or(Span::unknown()),
|
right_span: self.params_span.unwrap_or(Span::unknown()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -293,7 +294,7 @@ impl UrlComponents {
|
||||||
&ShellError::GenericError {
|
&ShellError::GenericError {
|
||||||
error: format!("'{key}' is not a valid URL field"),
|
error: format!("'{key}' is not a valid URL field"),
|
||||||
msg: format!("remove '{key}' col from input record"),
|
msg: format!("remove '{key}' col from input record"),
|
||||||
span: Some(span),
|
span: Some(value_span),
|
||||||
help: None,
|
help: None,
|
||||||
inner: vec![],
|
inner: vec![],
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use nu_protocol::{IntoValue, Record, ShellError, Span, Type, Value};
|
use nu_protocol::{IntoValue, Record, ShellError, Span, Type, Value};
|
||||||
|
|
||||||
pub fn record_to_query_string(
|
pub fn record_to_query_string(
|
||||||
|
@ -43,6 +45,52 @@ pub fn record_to_query_string(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn table_to_query_string(
|
||||||
|
table: &[Value],
|
||||||
|
span: Span,
|
||||||
|
head: Span,
|
||||||
|
) -> Result<String, ShellError> {
|
||||||
|
let row_vec = table
|
||||||
|
.iter()
|
||||||
|
.map(|val| match val {
|
||||||
|
Value::Record { val, internal_span } => key_value_from_record(val, *internal_span),
|
||||||
|
_ => Err(ShellError::UnsupportedInput {
|
||||||
|
msg: "expected a table".into(),
|
||||||
|
input: "not a table, contains non-record values".into(),
|
||||||
|
msg_span: head,
|
||||||
|
input_span: span,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, ShellError>>()?;
|
||||||
|
|
||||||
|
serde_urlencoded::to_string(row_vec).map_err(|_| ShellError::CantConvert {
|
||||||
|
to_type: "URL".into(),
|
||||||
|
from_type: Type::table().to_string(),
|
||||||
|
span: head,
|
||||||
|
help: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_value_from_record(record: &Record, span: Span) -> Result<(Cow<str>, Cow<str>), ShellError> {
|
||||||
|
let key = record
|
||||||
|
.get("key")
|
||||||
|
.ok_or_else(|| ShellError::CantFindColumn {
|
||||||
|
col_name: "key".into(),
|
||||||
|
span: None,
|
||||||
|
src_span: span,
|
||||||
|
})?
|
||||||
|
.coerce_str()?;
|
||||||
|
let value = record
|
||||||
|
.get("value")
|
||||||
|
.ok_or_else(|| ShellError::CantFindColumn {
|
||||||
|
col_name: "value".into(),
|
||||||
|
span: None,
|
||||||
|
src_span: span,
|
||||||
|
})?
|
||||||
|
.coerce_str()?;
|
||||||
|
Ok((key, value))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn query_string_to_table(query: &str, head: Span, span: Span) -> Result<Value, ShellError> {
|
pub fn query_string_to_table(query: &str, head: Span, span: Span) -> Result<Value, ShellError> {
|
||||||
let params = serde_urlencoded::from_str::<Vec<(String, String)>>(query)
|
let params = serde_urlencoded::from_str::<Vec<(String, String)>>(query)
|
||||||
.map_err(|_| ShellError::UnsupportedInput {
|
.map_err(|_| ShellError::UnsupportedInput {
|
||||||
|
|
|
@ -156,7 +156,7 @@ fn url_join_with_different_query_and_params() {
|
||||||
|
|
||||||
assert!(actual
|
assert!(actual
|
||||||
.err
|
.err
|
||||||
.contains("Mismatch, qs from params is: ?par_1=aaab&par_2=bbb"));
|
.contains("Mismatch, query string from params is: ?par_1=aaab&par_2=bbb"));
|
||||||
assert!(actual
|
assert!(actual
|
||||||
.err
|
.err
|
||||||
.contains("instead query is: ?par_1=aaa&par_2=bbb"));
|
.contains("instead query is: ?par_1=aaa&par_2=bbb"));
|
||||||
|
@ -183,7 +183,7 @@ fn url_join_with_different_query_and_params() {
|
||||||
.contains("Mismatch, query param is: par_1=aaa&par_2=bbb"));
|
.contains("Mismatch, query param is: par_1=aaa&par_2=bbb"));
|
||||||
assert!(actual
|
assert!(actual
|
||||||
.err
|
.err
|
||||||
.contains("instead qs from params is: ?par_1=aaab&par_2=bbb"));
|
.contains("instead query string from params is: ?par_1=aaab&par_2=bbb"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -201,7 +201,9 @@ fn url_join_with_invalid_params() {
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(actual.err.contains("Key params has to be a record"));
|
assert!(actual
|
||||||
|
.err
|
||||||
|
.contains("Key params has to be a record or a table"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -346,3 +348,83 @@ fn url_join_with_empty_params() {
|
||||||
|
|
||||||
assert_eq!(actual.out, "https://localhost/foo");
|
assert_eq!(actual.out, "https://localhost/foo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_join_with_list_in_params() {
|
||||||
|
let actual = nu!(pipeline(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"scheme": "http",
|
||||||
|
"username": "usr",
|
||||||
|
"password": "pwd",
|
||||||
|
"host": "localhost",
|
||||||
|
"params": {
|
||||||
|
"par_1": "aaa",
|
||||||
|
"par_2": ["bbb", "ccc"]
|
||||||
|
},
|
||||||
|
"port": "1234",
|
||||||
|
} | url join
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual.out,
|
||||||
|
"http://usr:pwd@localhost:1234?par_1=aaa&par_2=bbb&par_2=ccc"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_join_with_params_table() {
|
||||||
|
let actual = nu!(pipeline(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual.out,
|
||||||
|
"http://usr:pwd@localhost:1234?par_1=aaa&par_2=bbb&par_1=ccc&par_2=ddd"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn url_join_with_params_invalid_table() {
|
||||||
|
let actual = nu!(pipeline(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"scheme": "http",
|
||||||
|
"username": "usr",
|
||||||
|
"password": "pwd",
|
||||||
|
"host": "localhost",
|
||||||
|
"params": (
|
||||||
|
[
|
||||||
|
["key", "value"];
|
||||||
|
["par_1", "aaa"],
|
||||||
|
["par_2", "bbb"],
|
||||||
|
["par_1", "ccc"],
|
||||||
|
["par_2", "ddd"],
|
||||||
|
] ++ ["not a record"]
|
||||||
|
),
|
||||||
|
"port": "1234",
|
||||||
|
} | url join
|
||||||
|
"#
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected a table"));
|
||||||
|
assert!(actual
|
||||||
|
.err
|
||||||
|
.contains("not a table, contains non-record values"));
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue