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

The original design in the ticket was to use have the user still provide `Content-Type`, but I realized there isn't much value in that. It just requires more work for the user, and introduces another thing we need to validate (body type vs `Content-Type`). If the user needs a subset of `application/json` they can still override it. Otherwise, providing it ourselves is just more convenient. The other motivating factor behind is forms. The machinery that reqwest gives us to set form data automatically sets the header, so I think it makes sense to follow that lead.

Closes #242
  • Loading branch information
LucasPickering committed Jun 3, 2024
1 parent e1eac4c commit 5efb7e2
Show file tree
Hide file tree
Showing 30 changed files with 1,555 additions and 444 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 more convenient to write, because you no longer have to specify the `Content-Type` header yourself
- [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}}"
```
14 changes: 7 additions & 7 deletions docs/src/api/request_collection/chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ To use a chain in a template, reference it as `{{chains.<id>}}`.

## Fields

| Field | Type | Description | Default |
| -------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `source` | [`ChainSource`](./chain_source.md) | Source of the chained value | Required |
| `sensitive` | `boolean` | Should the value be hidden in the UI? | `false` |
| `selector` | [`JSONPath`](https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html) | Selector to transform/narrow down results in a chained value. See [Filtering & Querying](../../user_guide/filter_query.md) | `null` |
| `content_type` | [`ContentType`](./content_type.md) | Force content type. Not required for `request` and `file` chains, as long as the `Content-Type` header/file extension matches the data | |
| `trim` | [`ChainOutputTrim`](#chain-output-trim) | Trim whitespace from the rendered output | `none` |
| Field | Type | Description | Default |
| -------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `source` | [`ChainSource`](./chain_source.md) | Source of the chained value | Required |
| `sensitive` | `boolean` | Should the value be hidden in the UI? | `false` |
| `selector` | [`JSONPath`](https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html) | Selector to transform/narrow down results in a chained value. See [Filtering & Querying](../../user_guide/filter_query.md) | `null` |
| `content_type` | `string` | Force content type. Not required for `request` and `file` chains, as long as the `Content-Type` header/file extension matches the data. See [here](./recipe_body.md#supported-content-types) for a list of supported types. | |
| `trim` | [`ChainOutputTrim`](#chain-output-trim) | Trim whitespace from the rendered output | `none` |

See the [`ChainSource`](./chain_source.md) docs for detail on the different types of chainable values.

Expand Down
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
11 changes: 0 additions & 11 deletions docs/src/api/request_collection/content_type.md

This file was deleted.

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" }
```
49 changes: 49 additions & 0 deletions docs/src/api/request_collection/recipe_body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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. In this case, you'll probably want to explicitly set the `Content-Type` header to tell the server what kind of data you're sending. This may not be necessary though, depending on the server implementation.

## Supported Content Types

The following content types have first-class support. The meaning of each column is as follows:

- Variant - Tag used when defining a body of this type
- Type - Type of the associated value
- `Content-Type` - Value to insert for the `Content-Type` request header
- Additionally, this is the associated content type when determining how to [parse chained responses for the purposes of filtering/querying](../../user_guide/filter_query.md).
- File Extension - File extension(s) that map to this content type when determining how to [parse chained files for the purposes of filtering/querying](../../user_guide/filter_query.md)

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

## 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: Balthazar

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}}"
# Content-Type header will be set automatically based on the body type
body: !json { "name": "Balthazar" }
```
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
Loading

0 comments on commit 5efb7e2

Please sign in to comment.