Skip to content

Commit

Permalink
Add !json variant for structured bodies
Browse files Browse the repository at this point in the history
- Add !json tag to `body` field of recipes
- !json bodies can contain any data. Strings will be treated as templates

An important design decision here is whether or not we should automatically set the `Content-Type` header. In the issue I wrote that we wouldn't, but during implementation I played around with both options. In the end I went with what was laid out in the issue. The pros and cons of setting it automatically:

- Convenient, reduces boilerplate

- It may be incorrect, e.g. if the user actually wants `application/geo+json`
- Implicit behavior can be confusing
- No way to un-set it (you could overwrite the header with an empty value but you can't omit it entirely)
- For more complicated types (e.g. forms), we may want to rely on the header to tells us more about the body, which is incompatible with the reverse

Another reason to leave it explicit for now is it's a non-breaking change to add the implicit behavior later.

Closes #242
  • Loading branch information
LucasPickering committed Jun 3, 2024
1 parent e1eac4c commit 2a9b2c4
Show file tree
Hide file tree
Showing 28 changed files with 1,457 additions and 425 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- JSON bodies can now be defined with the `!json` tag [#242](https://github.com/LucasPickering/slumber/issues/242)
- This should make JSON requests a bit more more convenient to write
- [See docs](https://slumber.lucaspickering.me/book/api/request_collection/recipe_body.html)
- Templates can now render binary values in certain contexts
- [See docs](https://slumber.lucaspickering.me/book/user_guide/templates.html#binary-templates) for more info
- [See docs](https://slumber.lucaspickering.me/book/user_guide/templates.html#binary-templates)

### Changed

Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ rust-version = "1.76.0"

[dependencies]
anyhow = {version = "^1.0.75", features = ["backtrace"]}
async-recursion = "1.1.1"
async-trait = "^0.1.73"
bytes = {version = "1.5.0", features = ["serde"]}
bytesize = {version = "1.3.0", default-features = false}
Expand All @@ -31,7 +32,6 @@ mime = "^0.3.17"
nom = "7.1.3"
notify = {version = "^6.1.1", default-features = false, features = ["macos_fsevent"]}
open = "5.1.1"
pretty_assertions = "1.4.0"
ratatui = {version = "^0.26.0", features = ["serde", "unstable-rendered-line-info"]}
reqwest = {version = "^0.12.4", default-features = false, features = ["rustls-tls"]}
rmp-serde = "^1.1.2"
Expand All @@ -51,6 +51,7 @@ uuid = {version = "^1.4.1", default-features = false, features = ["serde", "v4"]

[dev-dependencies]
mockito = {version = "1.4.0", default-features = false}
pretty_assertions = "1.4.0"
rstest = {version = "0.19.0", default-features = false}
serde_test = "1.0.176"

Expand Down
10 changes: 5 additions & 5 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
- [Request Collection](./api/request_collection/index.md)
- [Profile](./api/request_collection/profile.md)
- [Request Recipe](./api/request_collection/request_recipe.md)
- [Authentication](./api/request_collection/authentication.md)
- [Chain](./api/request_collection/chain.md)
- [Chain Source](./api/request_collection/chain_source.md)
- [Template](./api/request_collection/template.md)
- [Content Type](./api/request_collection/content_type.md)
- [Authentication](./api/request_collection/authentication.md)
- [Recipe Body](./api/request_collection/recipe_body.md)
- [Chain](./api/request_collection/chain.md)
- [Chain Source](./api/request_collection/chain_source.md)
- [Template](./api/request_collection/template.md)
- [Configuration](./api/configuration/index.md)
- [Input Bindings](./api/configuration/input_bindings.md)
- [Theme](./api/configuration/theme.md)
Expand Down
14 changes: 6 additions & 8 deletions docs/src/api/request_collection/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Authentication provides shortcuts for common HTTP authentication schemes. It pop

## Variants

| Variant | Type | Value |
| -------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `basic` | [`Basic Authentication`](#basic-authentication) | [Basic authentication](https://swagger.io/docs/specification/authentication/basic-authentication/) credentials |
| `bearer` | `string` | [Bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/) |
| Variant | Type | Value |
| --------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `!basic` | [`Basic Authentication`](#basic-authentication) | [Basic authentication](https://swagger.io/docs/specification/authentication/basic-authentication/) credentials |
| `!bearer` | `string` | [Bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/) |

### Basic Authentication

Expand All @@ -26,8 +26,7 @@ requests:
create_fish: !request
method: POST
url: "{{host}}/fishes"
body: >
{"kind": "barracuda", "name": "Jimmy"}
body: !json { "kind": "barracuda", "name": "Jimmy" }
authentication: !basic
username: user
password: pass
Expand All @@ -41,7 +40,6 @@ requests:
create_fish: !request
method: POST
url: "{{host}}/fishes"
body: >
{"kind": "barracuda", "name": "Jimmy"}
body: !json { "kind": "barracuda", "name": "Jimmy" }
authentication: !bearer "{{chains.token}}"
```
12 changes: 6 additions & 6 deletions docs/src/api/request_collection/chain_source.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ message: Enter Password
## Variants
| Variant | Type | Description |
| --------- | ---------------------------------- | --------------------------------------------------------------- |
| `request` | [`ChainSource::Request`](#request) | Body of the most recent response for a specific request recipe. |
| `command` | [`ChainSource::Command`](#command) | Stdout of the executed command |
| `file` | [`ChainSource::File`](#file) | Contents of the file |
| `prompt` | [`ChainSource::Prompt`](#prompt) | Value entered by the user |
| Variant | Type | Description |
| ---------- | ---------------------------------- | --------------------------------------------------------------- |
| `!request` | [`ChainSource::Request`](#request) | Body of the most recent response for a specific request recipe. |
| `!command` | [`ChainSource::Command`](#command) | Stdout of the executed command |
| `!file` | [`ChainSource::File`](#file) | Contents of the file |
| `!prompt` | [`ChainSource::Prompt`](#prompt) | Value entered by the user |

### Request

Expand Down
12 changes: 5 additions & 7 deletions docs/src/api/request_collection/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,21 @@ chains:
recipe: login
selector: $.token

# Use YAML anchors for de-duplication (Anything under .ignore is ignored)
# Use YAML anchors for de-duplication (Anything under .ignore will not trigger an error for unknown fields)
.ignore:
base: &base
headers:
Accept: application/json
Content-Type: application/json

requests:
login: !request
<<: *base
method: POST
url: "{{host}}/anything/login"
body: |
{
body:
!json {
"username": "{{chains.username}}",
"password": "{{chains.password}}"
"password": "{{chains.password}}",
}

# Folders can be used to keep your recipes organized
Expand All @@ -92,6 +91,5 @@ requests:
method: PUT
url: "{{host}}/anything/current-user"
authentication: !bearer "{{chains.auth_token}}"
body: >
{"username": "Kenny"}
body: !json { "username": "Kenny" }
```
46 changes: 46 additions & 0 deletions docs/src/api/request_collection/recipe_body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Recipe Body

There are a variety of ways to define the body of your request. Slumber supports structured bodies for a fixed set of known content types (see table below). In addition, you can pass any [`Template`](./template.md) to render any text or binary data. This may not be necessary though, depending on the server implementation.

## Supported Content Types

The following content types have first-class support. All other bodies types must be specified as raw text/binary.

| Variant | Type | Description |
| ------- | ---- | ---------------------------------------------------------------- |
| `!json` | Any | Structured JSON body, where all strings are treated as templates |

> Note: Unlike some other HTTP clients, Slumber does **not** automatically set the `Content-Type` header for you. In general you'll want to include that in your request recipe, to tell the server the type of the content you're sending. While this may be inconvenient, it's not possible for Slumber to always know the correct header value, and Slumber's design generally prefers explicitness over convenience.
## Examples

```yaml
chains:
image:
source: !file
path: ./fish.png

requests:
text_body: !request
method: POST
url: "{{host}}/fishes/{{fish_id}}/name"
headers:
Content-Type: text/plain
body: Alfonso

binary_body: !request
method: POST
url: "{{host}}/fishes/{{fish_id}}/image"
headers:
Content-Type: image/jpg
body: "{{chains.fish_image}}"

json_body: !request
method: POST
url: "{{host}}/fishes/{{fish_id}}"
headers:
Content-Type: application/json
body: !json { "id": "{{fish_id}}", "name": "Alfonso" }
# This is equivalent to:
# body: '{"id": "{{fish_id}}", "name": "Alfonso"}'
```
12 changes: 5 additions & 7 deletions docs/src/api/request_collection/request_recipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The tag for a recipe is `!request` (see examples).
| `query` | [`mapping[string, Template]`](./template.md) | HTTP request query parameters | `{}` |
| `headers` | [`mapping[string, Template]`](./template.md) | HTTP request headers | `{}` |
| `authentication` | [`Authentication`](./authentication.md) | Authentication scheme | `null` |
| `body` | [`Template`](./template.md) | HTTP request body | `null` |
| `body` | [`RecipeBody`](./recipe_body.md) | HTTP request body | `null` |

## Folder Fields

Expand All @@ -39,22 +39,20 @@ recipes:
url: "{{host}}/anything/login"
headers:
accept: application/json
content-type: application/json
query:
root_access: yes_please
body: |
{
body:
!json {
"username": "{{chains.username}}",
"password": "{{chains.password}}"
"password": "{{chains.password}}",
}
fish: !folder
name: Users
requests:
create_fish: !request
method: POST
url: "{{host}}/fishes"
body: >
{"kind": "barracuda", "name": "Jimmy"}
body: !json { "kind": "barracuda", "name": "Jimmy" }

list_fish: !request
method: GET
Expand Down
3 changes: 1 addition & 2 deletions docs/src/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ requests:
create_fish: !request
method: POST
url: "{{host}}/fishes"
body: >
{"kind": "barracuda", "name": "Jimmy"}
body: !json { "kind": "barracuda", "name": "Jimmy" }
list_fish: !request
method: GET
Expand Down
14 changes: 7 additions & 7 deletions docs/src/user_guide/chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ requests:
login: !request
method: POST
url: "https://myfishes.fish/login"
body: |
{
body:
!json {
"username": "{{chains.username}}",
"password": "{{chains.password}}"
"password": "{{chains.password}}",
}

get_user: !request
Expand Down Expand Up @@ -99,17 +99,17 @@ chains:
recipe: login
auth_token:
source: !command
command: [ "cut", "-d':'", "-f2" ]
command: ["cut", "-d':'", "-f2"]
stdin: "{{chains.auth_token_raw}}"
requests:
login: !request
method: POST
url: "https://myfishes.fish/login"
body: |
{
body:
!json {
"username": "{{chains.username}}",
"password": "{{chains.password}}"
"password": "{{chains.password}}",
}
get_user: !request
Expand Down
9 changes: 5 additions & 4 deletions docs/src/user_guide/filter_query.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ We'll use these credentials to log in and get an API token, so the second data s
```yaml
chains:
username:
# Slumber knows how to query this file based on its extension
source: !file
path: ./creds.json
selector: $.user
Expand All @@ -43,10 +44,10 @@ requests:
login: !request
method: POST
url: "https://myfishes.fish/anything/login"
body: |
{
body:
!json {
"username": "{{chains.username}}",
"password": "{{chains.password}}"
"password": "{{chains.password}}",
}

get_user: !request
Expand Down Expand Up @@ -84,7 +85,7 @@ requests:
login: !request
method: POST
url: "https://myfishes.fish/anything/login"
body: |
body: !json
{
"username": "{{chains.username}}",
"password": "{{chains.password}}"
Expand Down
5 changes: 2 additions & 3 deletions docs/src/user_guide/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ requests:
url: "{{host}}/fishes"
headers:
<<: *headers_base
Content-Type: application/json
body: >
{"kind": "barracuda", "name": "Jimmy"}
Host: myfishes.fish
body: !json { "kind": "barracuda", "name": "Jimmy" }
```
3 changes: 1 addition & 2 deletions docs/src/user_guide/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ requests:
create_fish: !request
method: POST
url: "{{host}}/fishes"
body: >
{"kind": "barracuda", "name": "Jimmy"}
body: !json { "kind": "barracuda", "name": "Jimmy" }
get_fish: !request
method: GET
Expand Down
15 changes: 4 additions & 11 deletions slumber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ chains:
source: !request
recipe: login
trigger: !expire 12h
selector: $.headers["X-Amzn-Trace-Id"]
selector: $.data

.ignore:
base: &base
Expand All @@ -46,12 +46,8 @@ requests:
fast: no_thanks
headers:
Accept: application/json
Content-Type: application/json
body: |
{
"username": "{{username}}",
"password": "{{chains.password}}"
}
body:
!json { "username": "{{username}}", "password": "{{chains.password}}" }

users: !folder
name: Users
Expand All @@ -75,10 +71,7 @@ requests:
name: Modify User
method: PUT
url: "{{host}}/anything/{{user_guid}}"
body: |
{
"username": "new username"
}
body: !json { "username": "new username" }

get_image: !request
headers:
Expand Down
Loading

0 comments on commit 2a9b2c4

Please sign in to comment.