Skip to content

Commit a7907ee

Browse files
authored
Feature openapi_extensions with helper functions for JSON responses and requestBodies (#240)
* Add openapi_extensions with some helper functions for JSON requestBodies and JSON responses. * Implement traits for RequestBody and Response and add tests for each implementation. * Add docs
1 parent 45e3eb4 commit a7907ee

File tree

7 files changed

+336
-28
lines changed

7 files changed

+336
-28
lines changed

.github/workflows/build.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
test:
1818
strategy:
1919
matrix:
20-
testset:
20+
testset:
2121
- utoipa
2222
- utoipa-gen
2323
- utoipa-swagger-ui
@@ -38,7 +38,7 @@ jobs:
3838
~/.cargo/git/db/
3939
target/
4040
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
41-
41+
4242
- name: Resolve changed paths
4343
id: changes
4444
run: |
@@ -61,7 +61,7 @@ jobs:
6161
- name: Run tests
6262
run: |
6363
if [[ "${{ matrix.testset }}" == "utoipa" ]] && [[ ${{ steps.changes.outputs.root_changed }} == true ]]; then
64-
cargo test --features uuid
64+
cargo test --features uuid,openapi_extensions
6565
cargo test --test path_response_derive_test_no_serde_json --no-default-features
6666
cargo test --test component_derive_no_serde_json --no-default-features
6767
cargo test --test path_derive_actix --test path_parameter_derive_actix --features actix_extras

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ yaml = ["serde_yaml"]
3333
uuid = ["utoipa-gen/uuid"]
3434
time = ["utoipa-gen/time"]
3535
smallvec = ["utoipa-gen/smallvec"]
36+
openapi_extensions = []
3637

3738
[dependencies]
3839
serde = { version = "1.0", features = ["derive"] }

README.md

+19-17
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@ Rust if auto generation is not your flavor or does not fit your purpose.
2222
Long term goal of the library is to be the place to go when OpenAPI documentation is needed in Rust
2323
codebase.
2424

25-
Utoipa is framework agnostic and could be used together with any web framework or even without one. While
26-
being portable and standalone one of it's key aspects is simple integration with web frameworks.
25+
Utoipa is framework agnostic and could be used together with any web framework or even without one. While
26+
being portable and standalone one of it's key aspects is simple integration with web frameworks.
2727

2828
## Choose your flavor and document your API with ice cold IPA
2929

3030
Existing [examples](./examples) for following frameworks:
3131

32-
* **[actix-web](https://github.com/actix/actix-web)**
32+
* **[actix-web](https://github.com/actix/actix-web)**
3333
* **[axum](https://github.com/tokio-rs/axum)**
3434
* **[warp](https://github.com/seanmonstar/warp)**
3535
* **[tide](https://github.com/http-rs/tide)**
3636
* **[rocket](https://github.com/SergioBenitez/Rocket)**
3737

38-
Even if there is no example for your favourite framework `utoipa` can be used with any
38+
Even if there is no example for your favourite framework `utoipa` can be used with any
3939
web framework which supports decorating functions with macros similarly to **warp** and **tide** examples.
4040

4141
## What's up with the word play?
@@ -49,34 +49,36 @@ and the `ipa` is _api_ reversed. Aaand... `ipa` is also awesome type of beer :be
4949
* **json** Enables **serde_json** serialization of OpenAPI objects which also allows usage of JSON within
5050
OpenAPI values e.g. within `example` value. This is enabled by default.
5151
* **yaml** Enables **serde_yaml** serialization of OpenAPI objects.
52-
* **actix_extras** Enhances [actix-web](https://github.com/actix/actix-web/) integration with being able to
53-
parse `path` and `path and query parameters` from actix web path attribute macros. See
52+
* **actix_extras** Enhances [actix-web](https://github.com/actix/actix-web/) integration with being able to
53+
parse `path` and `path and query parameters` from actix web path attribute macros. See
5454
[docs](https://docs.rs/utoipa/1.1.0/utoipa/attr.path.html#actix_extras-support-for-actix-web) or [examples](./examples) for more details.
5555
* **rocket_extras** Enhances [rocket](https://github.com/SergioBenitez/Rocket) framework integration with being
5656
able to parse `path`, `path and query parameters` from rocket path attribute macros. See [docs](https://docs.rs/utoipa/1.1.0/utoipa/attr.path.html#rocket_extras-support-for-rocket)
5757
or [examples](./examples) for more details.
58-
* **axum_extras** Enhances [axum](https://github.com/tokio-rs/axum) framework integration allowing users to use `IntoParams` without defining the `parameter_in` attribute. See
58+
* **axum_extras** Enhances [axum](https://github.com/tokio-rs/axum) framework integration allowing users to use `IntoParams` without defining the `parameter_in` attribute. See
5959
[docs](https://docs.rs/utoipa/1.1.0/utoipa/attr.path.html#axum_extras-suppport-for-axum) or [examples](./examples) for more details.
6060
* **debug** Add extra traits such as debug traits to openapi definitions and elsewhere.
6161
* **chrono** Add support for [chrono](https://crates.io/crates/chrono) `DateTime`, `Date` and `Duration`
62-
types. By default these types are parsed to `string` types without additional format. If you want to have
63-
formats added to the types use *chrono_with_format* feature. This is useful because OpenAPI 3.1 spec
64-
does not have date-time formats. To override default `string` representation
62+
types. By default these types are parsed to `string` types without additional format. If you want to have
63+
formats added to the types use *chrono_with_format* feature. This is useful because OpenAPI 3.1 spec
64+
does not have date-time formats. To override default `string` representation
6565
users have to use `value_type` attribute to override the type. See [docs](https://docs.rs/utoipa/1.1.0/utoipa/derive.Component.html) for more details.
66-
* **chrono_with_format** Add support to [chrono](https://crates.io/crates/chrono) types described above
66+
* **chrono_with_format** Add support to [chrono](https://crates.io/crates/chrono) types described above
6767
with additional `format` information type. `date-time` for `DateTime` and `date` for `Date` according
68-
[RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14) as `ISO-8601`. To override default `string` representation
68+
[RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14) as `ISO-8601`. To override default `string` representation
6969
users have to use `value_type` attribute to override the type. See [docs](https://docs.rs/utoipa/1.1.0/utoipa/derive.Component.html) for more details.
7070
* **time** Add support for [time](https://crates.io/crates/time) `OffsetDateTime`, `PrimitiveDateTime`, `Date`, and `Duration` types.
7171
By default these types are parsed as `string`. `OffsetDateTime` and `PrimitiveDateTime` will use `date-time` format. `Date` will use
7272
`date` format and `Duration` will not have any format. To override default `string` representation users have to use `value_type` attribute
7373
to override the type. See [docs](https://docs.rs/utoipa/1.1.0/utoipa/derive.Component.html) for more details.
74-
* **decimal** Add support for [rust_decimal](https://crates.io/crates/rust_decimal) `Decimal` type. **By default**
75-
it is interpreted as `String`. If you wish to change the format you need to override the type.
74+
* **decimal** Add support for [rust_decimal](https://crates.io/crates/rust_decimal) `Decimal` type. **By default**
75+
it is interpreted as `String`. If you wish to change the format you need to override the type.
7676
See the `value_type` in [component derive docs](https://docs.rs/utoipa/1.1.0/utoipa/derive.Component.html).
7777
* **uuid** Add support for [uuid](https://github.com/uuid-rs/uuid). `Uuid` type will be presented as `String` with
7878
format `uuid` in OpenAPI spec.
7979
* **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`.
80+
* **openapi_extensions** Adds traits and functions that provide extra convenience functions.
81+
See the [`request_body` docs](https://docs.rs/utoipa/latest/utoipa/openapi/request_body) for an example.
8082

8183
Utoipa implicitly has partial support for `serde` attributes. See [docs](https://docs.rs/utoipa/1.1.0/utoipa/derive.Component.html#partial-serde-attributes-support) for more details.
8284

@@ -121,7 +123,7 @@ Create a handler that would handle your business logic and add `path` proc attri
121123
mod pet_api {
122124
/// Get pet by id
123125
///
124-
/// Get pet from database by pet id
126+
/// Get pet from database by pet id
125127
#[utoipa::path(
126128
get,
127129
path = "/pets/{id}",
@@ -249,5 +251,5 @@ This would produce api doc something similar to:
249251

250252
Licensed under either of [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) license at your option.
251253

252-
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate
253-
by you, shall be dual licensed, without any additional terms or conditions.
254+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate
255+
by you, shall be dual licensed, without any additional terms or conditions.

src/lib.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
//! * **uuid** Add support for [uuid](https://github.com/uuid-rs/uuid). `Uuid` type will be presented as `String` with
7474
//! format `uuid` in OpenAPI spec.
7575
//! * **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`.
76+
//! * **openapi_extensions** Adds convenience functions for documenting common scenarios, such as JSON request bodies and responses.
77+
//! See the [`request_body`](https://docs.rs/utoipa/latest/utoipa/openapi/request_body/index.html) and
78+
//! [`response`](https://docs.rs/utoipa/latest/utoipa/openapi/response/index.html) docs for examples.
7679
//!
7780
//! Utoipa implicitly has partial support for `serde` attributes. See [component derive][serde] for more details.
7881
//!
@@ -114,7 +117,7 @@
114117
//! mod pet_api {
115118
//! # use utoipa::OpenApi;
116119
//! # use utoipa::Component;
117-
//! #
120+
//! #
118121
//! # #[derive(Component)]
119122
//! # struct Pet {
120123
//! # id: u64,
@@ -123,7 +126,7 @@
123126
//! # }
124127
//! /// Get pet by id
125128
//! ///
126-
//! /// Get pet from database by pet id
129+
//! /// Get pet from database by pet id
127130
//! #[utoipa::path(
128131
//! get,
129132
//! path = "/pets/{id}",
@@ -149,7 +152,7 @@
149152
//! ```rust
150153
//! # mod pet_api {
151154
//! # use utoipa::Component;
152-
//! #
155+
//! #
153156
//! # #[derive(Component)]
154157
//! # struct Pet {
155158
//! # id: u64,
@@ -159,7 +162,7 @@
159162
//! #
160163
//! # /// Get pet by id
161164
//! # ///
162-
//! # /// Get pet from database by pet id
165+
//! # /// Get pet from database by pet id
163166
//! # #[utoipa::path(
164167
//! # get,
165168
//! # path = "/pets/{id}",
@@ -345,7 +348,7 @@ pub trait Component {
345348
/// #
346349
/// /// Get pet by id
347350
/// ///
348-
/// /// Get pet from database by pet database id
351+
/// /// Get pet from database by pet database id
349352
/// #[utoipa::path(
350353
/// get,
351354
/// path = "/pets/{id}",

src/openapi/request_body.rs

+135-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ builder! {
3232
}
3333

3434
impl RequestBody {
35-
/// Constrcut a new [`RequestBody`].
35+
/// Construct a new [`RequestBody`].
3636
pub fn new() -> Self {
3737
Default::default()
3838
}
@@ -57,9 +57,72 @@ impl RequestBodyBuilder {
5757
}
5858
}
5959

60+
/// Trait with convenience functions for documenting request bodies.
61+
///
62+
/// This trait requires a feature-flag to enable:
63+
/// ```text
64+
/// [dependencies]
65+
/// utoipa = { version = "1", features = ["openapi_extensions"] }
66+
/// ```
67+
///
68+
/// Once enabled, with a single method call we can add [`Content`] to our RequestBodyBuilder
69+
/// that references a [`crate::Component`] schema using content-tpe "application/json":
70+
///
71+
/// ```rust
72+
/// use utoipa::openapi::request_body::{RequestBodyBuilder, RequestBodyExt};
73+
///
74+
/// let request = RequestBodyBuilder::new().json_component_ref("EmailPayload").build();
75+
/// ```
76+
///
77+
/// If serialized to JSON, the above will result in a requestBody schema like this:
78+
///
79+
/// ```json
80+
/// {
81+
/// "content": {
82+
/// "application/json": {
83+
/// "schema": {
84+
/// "$ref": "#/components/schemas/EmailPayload"
85+
/// }
86+
/// }
87+
/// }
88+
/// }
89+
/// ```
90+
///
91+
#[cfg(feature = "openapi_extensions")]
92+
pub trait RequestBodyExt {
93+
/// Add [`Content`] to [`RequestBody`] referring to a schema
94+
/// with Content-Type `application/json`.
95+
fn json_component_ref(self, ref_name: &str) -> Self;
96+
}
97+
98+
#[cfg(feature = "openapi_extensions")]
99+
impl RequestBodyExt for RequestBody {
100+
fn json_component_ref(mut self, ref_name: &str) -> RequestBody {
101+
self.content.insert(
102+
"application/json".to_string(),
103+
crate::openapi::Content::new(crate::openapi::Ref::from_component_name(ref_name)),
104+
);
105+
self
106+
}
107+
}
108+
109+
110+
#[cfg(feature = "openapi_extensions")]
111+
impl RequestBodyExt for RequestBodyBuilder {
112+
fn json_component_ref(self, ref_name: &str) -> RequestBodyBuilder {
113+
self.content(
114+
"application/json",
115+
crate::openapi::Content::new(crate::openapi::Ref::from_component_name(ref_name)),
116+
)
117+
}
118+
}
119+
60120
#[cfg(test)]
61121
mod tests {
62-
use super::RequestBody;
122+
use assert_json_diff::assert_json_eq;
123+
use serde_json::json;
124+
125+
use super::{Content, RequestBody, RequestBodyBuilder, Required};
63126

64127
#[test]
65128
fn request_body_new() {
@@ -69,4 +132,74 @@ mod tests {
69132
assert_eq!(request_body.description, None);
70133
assert!(request_body.required.is_none());
71134
}
135+
136+
#[test]
137+
fn request_body_builder() -> Result<(), serde_json::Error> {
138+
let request_body = RequestBodyBuilder::new()
139+
.description(Some("A sample requestBody"))
140+
.required(Some(Required::True))
141+
.content(
142+
"application/json",
143+
Content::new(crate::openapi::Ref::from_component_name("EmailPayload")),
144+
)
145+
.build();
146+
let serialized = serde_json::to_string_pretty(&request_body)?;
147+
println!("serialized json:\n {}", serialized);
148+
assert_json_eq!(
149+
request_body,
150+
json!({
151+
"description": "A sample requestBody",
152+
"content": {
153+
"application/json": {
154+
"schema": {
155+
"$ref": "#/components/schemas/EmailPayload"
156+
}
157+
}
158+
},
159+
"required": true
160+
})
161+
);
162+
Ok(())
163+
}
164+
#[cfg(feature = "openapi_extensions")]
165+
use super::RequestBodyExt;
166+
167+
#[cfg(feature = "openapi_extensions")]
168+
#[test]
169+
fn request_body_ext() {
170+
let request_body = RequestBodyBuilder::new()
171+
.build()
172+
// build a RequestBody first to test the method
173+
.json_component_ref("EmailPayload");
174+
assert_json_eq!(
175+
request_body,
176+
json!({
177+
"content": {
178+
"application/json": {
179+
"schema": {
180+
"$ref": "#/components/schemas/EmailPayload"
181+
}
182+
}
183+
}
184+
})
185+
);
186+
}
187+
188+
#[cfg(feature = "openapi_extensions")]
189+
#[test]
190+
fn request_body_builder_ext() {
191+
let request_body = RequestBodyBuilder::new().json_component_ref("EmailPayload").build();
192+
assert_json_eq!(
193+
request_body,
194+
json!({
195+
"content": {
196+
"application/json": {
197+
"schema": {
198+
"$ref": "#/components/schemas/EmailPayload"
199+
}
200+
}
201+
}
202+
})
203+
);
204+
}
72205
}

0 commit comments

Comments
 (0)