Skip to content

Commit

Permalink
Add multipart/form-data support
Browse files Browse the repository at this point in the history
Add a new structured body type !form_multipart, which allows sending forms with binary data. This is a bit incomplete, we need to support streaming bodies in #256.

Closes #243
  • Loading branch information
LucasPickering committed Jun 9, 2024
1 parent eeaf1f1 commit e073e68
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Structured bodies can now be defined with tags on the `body` field of a recipe, making it more convenient to construct bodies of common types. Supported types are:
- `!json` [#242](https://github.com/LucasPickering/slumber/issues/242)
- `!form_urlencoded` [#244](https://github.com/LucasPickering/slumber/issues/244)
- `!form_multipart` [#243](https://github.com/LucasPickering/slumber/issues/243)
- [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
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ nom = "7.1.3"
notify = {version = "^6.1.1", default-features = false, features = ["macos_fsevent"]}
open = "5.1.1"
ratatui = {version = "^0.26.0", features = ["serde", "unstable-rendered-line-info"]}
reqwest = {version = "^0.12.4", default-features = false, features = ["rustls-tls"]}
reqwest = {version = "^0.12.4", default-features = false, features = ["multipart", "rustls-tls"]}
rmp-serde = "^1.1.2"
rusqlite = {version = "^0.31.0", default-features = false, features = ["bundled", "chrono", "uuid"]}
rusqlite_migration = "^1.2.0"
Expand Down
24 changes: 20 additions & 4 deletions docs/src/api/request_collection/recipe_body.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ In addition, you can pass any [`Template`](./template.md) to render any text or

The following content types have first-class support. Slumber will automatically set the `Content-Type` header to the specified value, but you can override this simply by providing your own value for the header.

| Variant | Type | `Content-Type` | Description |
| ------------------ | -------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `!json` | Any | `application/json` | Structured JSON body; all strings are treated as templates |
| `!form_urlencoded` | [`mapping[string, Template]`](./template.md) | `application/x-www-form-urlencoded` | URL-encoded form data; [see here for more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST |
| Variant | Type | `Content-Type` | Description |
| ------------------ | -------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `!json` | Any | `application/json` | Structured JSON body; all strings are treated as templates |
| `!form_urlencoded` | [`mapping[string, Template]`](./template.md) | `application/x-www-form-urlencoded` | URL-encoded form data; [see here for more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) |
| `!form_multipart` | [`mapping[string, Template]`](./template.md) | `multipart/form-data` | Binary form data; [see here for more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) |

## Examples

Expand Down Expand Up @@ -41,4 +42,19 @@ requests:
url: "{{host}}/fishes/{{fish_id}}"
# Content-Type header will be set automatically based on the body type
body: !json { "name": "Alfonso" }

urlencoded_body: !request
method: POST
url: "{{host}}/fishes/{{fish_id}}"
# Content-Type header will be set automatically based on the body type
body: !form_urlencoded
name: Alfonso

multipart_body: !request
method: POST
url: "{{host}}/fishes/{{fish_id}}"
# Content-Type header will be set automatically based on the body type
body: !form_multipart
name: Alfonso
image: "{{chains.fish_image}}"
```
11 changes: 11 additions & 0 deletions slumber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ chains:
recipe: login
trigger: !expire 12h
selector: $.data
image:
source: !file
path: ./static/slumber.png

.ignore:
base: &base
Expand Down Expand Up @@ -81,6 +84,14 @@ requests:
method: GET
url: "{{host}}/image"

upload_image: !request
name: Upload Image
method: POST
url: "{{host}}/anything/image"
body: !form_multipart
filename: "logo.png"
image: "{{chains.image}}"

delay: !request
<<: *base
name: Delay
Expand Down
21 changes: 18 additions & 3 deletions src/collection/cereal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,12 @@ impl RecipeBody {
const STRUCT_NAME: &'static str = "RecipeBody";
const VARIANT_JSON: &'static str = "json";
const VARIANT_FORM_URLENCODED: &'static str = "form_urlencoded";
const ALL_VARIANTS: &'static [&'static str] =
&[Self::VARIANT_JSON, Self::VARIANT_FORM_URLENCODED];
const VARIANT_FORM_MULTIPART: &'static str = "form_multipart";
const ALL_VARIANTS: &'static [&'static str] = &[
Self::VARIANT_JSON,
Self::VARIANT_FORM_URLENCODED,
Self::VARIANT_FORM_MULTIPART,
];
}

/// Custom serialization for RecipeBody, so the `Raw` variant serializes as a
Expand All @@ -255,6 +259,13 @@ impl Serialize for RecipeBody {
Self::VARIANT_FORM_URLENCODED,
value,
),
RecipeBody::FormMultipart(value) => serializer
.serialize_newtype_variant(
Self::STRUCT_NAME,
3,
Self::VARIANT_FORM_MULTIPART,
value,
),
}
}
}
Expand Down Expand Up @@ -316,6 +327,9 @@ impl<'de> Deserialize<'de> for RecipeBody {
RecipeBody::VARIANT_FORM_URLENCODED => {
Ok(RecipeBody::FormUrlencoded(value.newtype_variant()?))
}
RecipeBody::VARIANT_FORM_MULTIPART => {
Ok(RecipeBody::FormMultipart(value.newtype_variant()?))
}
other => Err(A::Error::unknown_variant(
other,
RecipeBody::ALL_VARIANTS,
Expand Down Expand Up @@ -519,7 +533,8 @@ mod tests {
tag: Tag::new("raw"),
value: "{{user_id}}".into()
})),
"unknown variant `raw`, expected `json` or `form_urlencoded`",
"unknown variant `raw`, expected one of \
`json`, `form_urlencoded`, `form_multipart`",
)]
#[case::form_urlencoded_wrong_type(
serde_yaml::Value::Tagged(Box::new(TaggedValue{
Expand Down
19 changes: 3 additions & 16 deletions src/collection/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use derive_more::{Deref, Display, From, FromStr};
use equivalent::Equivalent;
use indexmap::IndexMap;
use itertools::Itertools;
use mime::Mime;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use strum::{EnumIter, IntoEnumIterator};
Expand Down Expand Up @@ -296,22 +295,10 @@ pub enum RecipeBody {
Raw(Template),
/// Strutured JSON, which will be stringified and sent as text
Json(JsonBody),
/// `application/x-www-form-urlencoded` fields
/// `application/x-www-form-urlencoded` fields. Values must be strings
FormUrlencoded(IndexMap<String, Template>),
}

impl RecipeBody {
/// Get the MIME type of this body. For raw bodies we have no idea, but for
/// structured bodies we can make a very educated guess
pub fn mime(&self) -> Option<Mime> {
match self {
RecipeBody::Raw(_) => None,
RecipeBody::Json(_) => Some(mime::APPLICATION_JSON),
RecipeBody::FormUrlencoded(_) => {
Some(mime::APPLICATION_WWW_FORM_URLENCODED)
}
}
}
/// `multipart/form-data` fields. Values can be binary
FormMultipart(IndexMap<String, Template>),
}

#[cfg(test)]
Expand Down
Loading

0 comments on commit e073e68

Please sign in to comment.