provide a common implementation for query string conversions in url join and url build-query (#14173)

Addresses one of the points in #14162

# Description

Factors out part of the `url::build_query::to_url` function into a
separate function `url::query::record_to_qs()`, which is then used in
both `url::build_query` and `url::join`.

# User-Facing Changes

Like with `url build-query` (after #14073), `url join` will allow list
values in `params` and behavior of two commands will be same.

```nushell
> {a: ["one", "two"], b: "three"} | url build-query
"a=one&a=two&b=three"

> {scheme: "http", host: "host", params: {a: ["one", "two"], b: "three"}} | url join 
"http://host?a=one&a=two&b=three"
```

# Tests + Formatting

Added an example to `url join` for the new behavior.
This commit is contained in:
Bahex 2024-10-29 14:33:14 +03:00 committed by GitHub
parent 9ebaa737aa
commit 719d9aa83c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 55 deletions

View file

@ -1,5 +1,7 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use super::query::record_to_query_string;
#[derive(Clone)] #[derive(Clone)]
pub struct SubCommand; pub struct SubCommand;
@ -38,12 +40,12 @@ impl Command for SubCommand {
result: Some(Value::test_string("foo=1&bar=2")), result: Some(Value::test_string("foo=1&bar=2")),
}, },
Example { Example {
description: "Outputs a query string representing the contents of this record", 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"#,
result: Some(Value::test_string("a=AT%26T&b=AT+T")), result: Some(Value::test_string("a=AT%26T&b=AT+T")),
}, },
Example { Example {
description: "Outputs a query string representing the contents of this record", description: "Outputs a query string representing the contents of this record, \"exploding\" the list into multiple parameters",
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")),
}, },
@ -68,48 +70,7 @@ fn to_url(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
.map(move |value| { .map(move |value| {
let span = value.span(); let span = value.span();
match value { match value {
Value::Record { ref val, .. } => { Value::Record { ref val, .. } => record_to_query_string(val, span, head),
let mut row_vec = vec![];
for (k, v) in &**val {
match v {
Value::List { ref vals, .. } => {
for v_item in vals {
row_vec.push((
k.clone(),
v_item.coerce_string().map_err(|_| {
ShellError::UnsupportedInput {
msg: "Expected a record with list of string values"
.to_string(),
input: "value originates from here".into(),
msg_span: head,
input_span: span,
}
})?,
));
}
}
_ => row_vec.push((
k.clone(),
v.coerce_string()
.map_err(|_| ShellError::UnsupportedInput {
msg:
"Expected a record with string or list of string values"
.to_string(),
input: "value originates from here".into(),
msg_span: head,
input_span: span,
})?,
)),
}
}
serde_urlencoded::to_string(row_vec).map_err(|_| ShellError::CantConvert {
to_type: "URL".into(),
from_type: value.get_type().to_string(),
span: head,
help: None,
})
}
// Propagate existing errors // Propagate existing errors
Value::Error { error, .. } => Err(*error), Value::Error { error, .. } => Err(*error),
other => Err(ShellError::UnsupportedInput { other => Err(ShellError::UnsupportedInput {

View file

@ -1,5 +1,7 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use super::query::record_to_query_string;
#[derive(Clone)] #[derive(Clone)]
pub struct SubCommand; pub struct SubCommand;
@ -27,7 +29,7 @@ impl Command for SubCommand {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
description: "Outputs a url representing the contents of this record", description: "Outputs a url representing the contents of this record, `params` and `query` fields must be equivalent",
example: r#"{ example: r#"{
"scheme": "http", "scheme": "http",
"username": "", "username": "",
@ -47,6 +49,21 @@ impl Command for SubCommand {
"http://www.pixiv.net/member_illust.php?mode=medium&illust_id=99260204", "http://www.pixiv.net/member_illust.php?mode=medium&illust_id=99260204",
)), )),
}, },
Example {
description: "Outputs a url representing the contents of this record, \"exploding\" the list in `params` into multiple parameters",
example: r#"{
"scheme": "http",
"username": "user",
"password": "pwd",
"host": "www.pixiv.net",
"port": "1234",
"params": {a: ["one", "two"], b: "three"},
"fragment": ""
} | url join"#,
result: Some(Value::test_string(
"http://user:pwd@www.pixiv.net:1234?a=one&a=two&b=three",
)),
},
Example { Example {
description: "Outputs a url representing the contents of this record", description: "Outputs a url representing the contents of this record",
example: r#"{ example: r#"{
@ -178,16 +195,8 @@ impl UrlComponents {
if key == "params" { if key == "params" {
return match value { return match value {
Value::Record { val, .. } => { Value::Record { ref val, .. } => {
let mut qs = val let mut qs = record_to_query_string(val, value_span, span)?;
.into_owned()
.into_iter()
.map(|(k, v)| match v.coerce_into_string() {
Ok(val) => Ok(format!("{k}={val}")),
Err(err) => Err(err),
})
.collect::<Result<Vec<String>, ShellError>>()?
.join("&");
qs = if !qs.trim().is_empty() { qs = if !qs.trim().is_empty() {
format!("?{qs}") format!("?{qs}")

View file

@ -3,6 +3,7 @@ mod decode;
mod encode; mod encode;
mod join; mod join;
mod parse; mod parse;
mod query;
mod url_; mod url_;
pub use self::parse::SubCommand as UrlParse; pub use self::parse::SubCommand as UrlParse;

View file

@ -0,0 +1,44 @@
use nu_protocol::{Record, ShellError, Span, Type, Value};
pub fn record_to_query_string(
record: &Record,
span: Span,
head: Span,
) -> Result<String, ShellError> {
let mut row_vec = vec![];
for (k, v) in record {
match v {
Value::List { ref vals, .. } => {
for v_item in vals {
row_vec.push((
k.as_str(),
v_item
.coerce_str()
.map_err(|_| ShellError::UnsupportedInput {
msg: "Expected a record with list of string values".to_string(),
input: "value originates from here".into(),
msg_span: head,
input_span: span,
})?,
));
}
}
_ => row_vec.push((
k.as_str(),
v.coerce_str().map_err(|_| ShellError::UnsupportedInput {
msg: "Expected a record with string or list of string values".to_string(),
input: "value originates from here".into(),
msg_span: head,
input_span: span,
})?,
)),
}
}
serde_urlencoded::to_string(row_vec).map_err(|_| ShellError::CantConvert {
to_type: "URL".into(),
from_type: Type::record().to_string(),
span: head,
help: None,
})
}