Skip to content

Commit

Permalink
Finish support for multiple query params
Browse files Browse the repository at this point in the history
- Fix deserialization and add tests
- Fix param toggling in TUI
- Fix persistence (mostly)
- Update changelog
- Update docs
  • Loading branch information
LucasPickering committed Jun 6, 2024
1 parent 5d003ee commit ae99cef
Show file tree
Hide file tree
Showing 23 changed files with 312 additions and 86 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- `!json` [#242](https://github.com/LucasPickering/slumber/issues/242)
- `!form_urlencoded` [#244](https://github.com/LucasPickering/slumber/issues/244)
- [See docs](https://slumber.lucaspickering.me/book/api/request_collection/recipe_body.html) for usage instructions
- Support multiple instances of the same query param [#245](https://github.com/LucasPickering/slumber/issues/245) (@maksimowiczm)
- Query params can now be defined as a list of `<param>=<value>` entries
- [See docs](https://slumber.lucaspickering.me/book/api/request_collection/query_parameters.html)
- Templates can now render binary values in certain contexts
- [See docs](https://slumber.lucaspickering.me/book/user_guide/templates.html#binary-templates)

Expand Down
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [Request Collection](./api/request_collection/index.md)
- [Profile](./api/request_collection/profile.md)
- [Request Recipe](./api/request_collection/request_recipe.md)
- [Query Parameters](./api/request_collection/query_parameters.md)
- [Authentication](./api/request_collection/authentication.md)
- [Recipe Body](./api/request_collection/recipe_body.md)
- [Chain](./api/request_collection/chain.md)
Expand Down
32 changes: 32 additions & 0 deletions docs/src/api/request_collection/query_parameters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Query Parameters

Query parameters are a component of a request URL. They provide additional information to the server about a request. In a request recipe, query parameters can be defined in one of two formats:

- Mapping of `key: value`
- List of strings, in the format `<key>=<value>`

The mapping format is typically more readable, but the list format allows you to define the same query parameter multiple times. In either format, **the key is treated as a plain string but the value is treated as a template**.

> Note: If you need to include a `=` in your parameter _name_, you'll need to use the mapping format. That means there is currently no support for multiple instances of a parameter with `=` in the name. This is very unlikely to be a restriction in the real world, but if you need support for this please [open an issue](https://github.com/LucasPickering/slumber/issues/new/choose).
## Examples

```yaml
recipes:
get_fishes_mapping: !request
method: GET
url: "{{host}}/get"
query:
big: true
color: red
name: "{{name}}"

get_fishes_list: !request
method: GET
url: "{{host}}/get"
query:
- big=true
- color=red
- color=blue
- name={{name}}
```
6 changes: 3 additions & 3 deletions docs/src/api/request_collection/request_recipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The tag for a recipe is `!request` (see examples).
| `name` | `string` | Descriptive name to use in the UI | Value of key in parent |
| `method` | `string` | HTTP request method | Required |
| `url` | [`Template`](./template.md) | HTTP request URL | Required |
| `query` | [`mapping[string, Template]`](./template.md) | HTTP request query parameters | `{}` |
| `query` | [`QueryParameters`](./query_parameters.md) | URL query parameters | `{}` |
| `headers` | [`mapping[string, Template]`](./template.md) | HTTP request headers | `{}` |
| `authentication` | [`Authentication`](./authentication.md) | Authentication scheme | `null` |
| `body` | [`RecipeBody`](./recipe_body.md) | HTTP request body | `null` |
Expand All @@ -40,7 +40,7 @@ recipes:
headers:
accept: application/json
query:
root_access: yes_please
- root_access=yes_please
body:
!json {
"username": "{{chains.username}}",
Expand All @@ -58,5 +58,5 @@ recipes:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
```
2 changes: 1 addition & 1 deletion docs/src/cli/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
```
```sh
Expand Down
2 changes: 1 addition & 1 deletion docs/src/cli/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
```
```sh
Expand Down
2 changes: 1 addition & 1 deletion docs/src/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
```

This request collection uses [templates](./user_guide//templates.md) and [profiles](./api/request_collection/profile.md) allow you to dynamically change the target host.
4 changes: 2 additions & 2 deletions docs/src/user_guide/filter_query.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ requests:
method: GET
url: "https://myfishes.fish/anything/current-user"
query:
auth: "{{chains.auth_token}}"
- auth={{chains.auth_token}}
```
While this example simple extracts inner fields, JSONPath can be used for much more powerful transformations. See the [JSONPath docs](https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html) or [this JSONPath editor](https://jsonpath.com/) for more examples.
Expand Down Expand Up @@ -95,7 +95,7 @@ requests:
method: GET
url: "https://myfishes.fish/anything/current-user"
query:
auth: "{{chains.auth_token}}"
- auth={{chains.auth_token}}
```

You can use this capability to manipulate responses via `grep`, `awk`, or any other program you like.
Expand Down
6 changes: 3 additions & 3 deletions docs/src/user_guide/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
headers:
Accept: application/json
authentication: !bearer "{{chains.token}}"
Expand Down Expand Up @@ -65,7 +65,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true

get_fish: !request
<<: *request_base
Expand Down Expand Up @@ -102,7 +102,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
get_fish: !request
<<: *request_base
Expand Down
2 changes: 1 addition & 1 deletion docs/src/user_guide/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ requests:
method: GET
url: "{{host}}/fishes"
query:
big: true
- big=true
```
Now you can easily select which host to hit. In the TUI, this is done via the Profile list. In the CLI, use the `--profile` option:
Expand Down
7 changes: 4 additions & 3 deletions slumber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ requests:
method: POST
url: "{{host}}/anything/login"
query:
sudo: yes_please
fast: no_thanks
- sudo=yes_please
- fast=no_thanks
- fast=actually_maybe
headers:
Accept: application/json
body: !form_urlencoded
Expand All @@ -58,7 +59,7 @@ requests:
method: GET
url: "{{host}}/get"
query:
foo: bar
- foo=bar

get_user: !request
<<: *base
Expand Down
21 changes: 11 additions & 10 deletions src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,10 @@ mod tests {
.into(),
)),
authentication: None,
query: indexmap! {
"sudo".into() => "yes_please".into(),
"fast".into() => "no_thanks".into(),
},
query: vec![
("sudo".into(), "yes_please".into()),
("fast".into(), "no_thanks".into()),
],
headers: indexmap! {
"Accept".into() => "application/json".into(),
},
Expand All @@ -479,18 +479,19 @@ mod tests {
name: Some("Get User".into()),
method: Method::Get,
url: "{{host}}/anything/{{user_guid}}".into(),

body: None,
authentication: None,
query: indexmap! {},
query: vec![
("value".into(), "{{field1}}".into()),
("value".into(), "{{field2}}".into()),
],
headers: indexmap! {},
}),
RecipeNode::Recipe(Recipe {
id: "json_body".into(),
name: Some("Modify User".into()),
method: Method::Put,
url: "{{host}}/anything/{{user_guid}}".into(),

body: Some(RecipeBody::Json(
json!({
"username": "new username"
Expand All @@ -500,7 +501,7 @@ mod tests {
authentication: Some(Authentication::Bearer(
"{{chains.auth_token}}".into(),
)),
query: indexmap! {},
query: vec![],
headers: indexmap! {
"Accept".into() => "application/json".into(),
},
Expand All @@ -518,7 +519,7 @@ mod tests {
username: "{{username}}".into(),
password: Some("{{password}}".into()),
}),
query: indexmap! {},
query: vec![],
headers: indexmap! {
"Accept".into() => "application/json".into(),
},
Expand All @@ -533,7 +534,7 @@ mod tests {
"username".into() => "new username".into()
})),
authentication: None,
query: indexmap! {},
query: vec![],
headers: indexmap! {
"Accept".into() => "application/json".into(),
},
Expand Down
80 changes: 63 additions & 17 deletions src/collection/cereal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::{
},
template::Template,
};
use indexmap::IndexMap;
use serde::{
de::{EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
Expand Down Expand Up @@ -107,7 +106,8 @@ where
s.parse().map_err(D::Error::custom)
}

/// Custom deserializer for query parameters.
/// Deserialize query parameters from either a sequence of `key=value` or a
/// map of `key: value`
pub fn deserialize_query_parameters<'de, D>(
deserializer: D,
) -> Result<Vec<(String, Template)>, D::Error>
Expand All @@ -123,45 +123,49 @@ where
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter.write_str("sequence of <param>=<value> or map")
formatter.write_str("sequence of \"<param>=<value>\" or map")
}

fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut query: Vec<(String, Template)> = vec![];
let mut query: Vec<(String, Template)> =
Vec::with_capacity(seq.size_hint().unwrap_or(5));
while let Some(value) = seq.next_element::<String>()? {
let (key, value) = value.split_once('=').ok_or_else(|| {
Error::custom("Query parameters must be in the form `<param>=<value>`")
})?;

if key.is_empty() {
let (param, value) =
value.split_once('=').ok_or_else(|| {
Error::custom(
"Query parameters must be in the form \
`\"<param>=<value>\"`",
)
})?;

if param.is_empty() {
return Err(Error::custom(
"Query parameter key cannot be empty",
"Query parameter name cannot be empty",
));
}

let key = key.to_string();
let key = param.to_string();
let value = Template::try_from(value.to_string())
.map_err(Error::custom)?;

query.push((key, value));
}

Ok(query.into_iter().collect())
Ok(query)
}

fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut query: IndexMap<String, Template> = IndexMap::new();
let mut query: Vec<(String, Template)> =
Vec::with_capacity(map.size_hint().unwrap_or(5));
while let Some((key, value)) = map.next_entry()? {
query.insert(key, value);
query.push((key, value));
}

Ok(query.into_iter().collect())
Ok(query)
}
}

Expand Down Expand Up @@ -534,6 +538,48 @@ mod tests {
);
}

/// Test deserializing query parameters from list or mapping form
#[rstest]
#[case::list(
&[
Token::Seq { len: None },
Token::Str("param={{value}}"),
Token::Str("param=value"),
Token::SeqEnd,
],
vec![("param", "{{value}}"), ("param", "value")]
)]
#[case::map(
&[
Token::Map { len: None },
Token::Str("param"),
Token::Str("{{value}}"),
Token::MapEnd,
],
vec![("param", "{{value}}")]
)]
fn test_deserialize_query_parameters(
#[case] tokens: &[Token],
#[case] expected: Vec<(&str, &str)>,
) {
#[derive(Debug, PartialEq, Deserialize)]
#[serde(transparent)]
struct Wrap(
#[serde(deserialize_with = "deserialize_query_parameters")]
Vec<(String, Template)>,
);

assert_de_tokens::<Wrap>(
&Wrap(
expected
.into_iter()
.map(|(param, value)| (param.into(), value.into()))
.collect(),
),
tokens,
);
}

/// A wrapper that forces serde_test to use our custom serialize/deserialize
/// functions
#[derive(Debug, PartialEq, Serialize, Deserialize)]
Expand Down
Loading

0 comments on commit ae99cef

Please sign in to comment.