Skip to content

Commit

Permalink
Feature openapi_extensions with helper functions for JSON responses…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
erewok authored Aug 4, 2022
1 parent 45e3eb4 commit a7907ee
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 28 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
test:
strategy:
matrix:
testset:
testset:
- utoipa
- utoipa-gen
- utoipa-swagger-ui
Expand All @@ -38,7 +38,7 @@ jobs:
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Resolve changed paths
id: changes
run: |
Expand All @@ -61,7 +61,7 @@ jobs:
- name: Run tests
run: |
if [[ "${{ matrix.testset }}" == "utoipa" ]] && [[ ${{ steps.changes.outputs.root_changed }} == true ]]; then
cargo test --features uuid
cargo test --features uuid,openapi_extensions
cargo test --test path_response_derive_test_no_serde_json --no-default-features
cargo test --test component_derive_no_serde_json --no-default-features
cargo test --test path_derive_actix --test path_parameter_derive_actix --features actix_extras
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ yaml = ["serde_yaml"]
uuid = ["utoipa-gen/uuid"]
time = ["utoipa-gen/time"]
smallvec = ["utoipa-gen/smallvec"]
openapi_extensions = []

[dependencies]
serde = { version = "1.0", features = ["derive"] }
Expand Down
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ Rust if auto generation is not your flavor or does not fit your purpose.
Long term goal of the library is to be the place to go when OpenAPI documentation is needed in Rust
codebase.

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

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

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

* **[actix-web](https://github.com/actix/actix-web)**
* **[actix-web](https://github.com/actix/actix-web)**
* **[axum](https://github.com/tokio-rs/axum)**
* **[warp](https://github.com/seanmonstar/warp)**
* **[tide](https://github.com/http-rs/tide)**
* **[rocket](https://github.com/SergioBenitez/Rocket)**

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

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

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.

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

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

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate
by you, shall be dual licensed, without any additional terms or conditions.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate
by you, shall be dual licensed, without any additional terms or conditions.
13 changes: 8 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
//! * **uuid** Add support for [uuid](https://github.com/uuid-rs/uuid). `Uuid` type will be presented as `String` with
//! format `uuid` in OpenAPI spec.
//! * **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`.
//! * **openapi_extensions** Adds convenience functions for documenting common scenarios, such as JSON request bodies and responses.
//! See the [`request_body`](https://docs.rs/utoipa/latest/utoipa/openapi/request_body/index.html) and
//! [`response`](https://docs.rs/utoipa/latest/utoipa/openapi/response/index.html) docs for examples.
//!
//! Utoipa implicitly has partial support for `serde` attributes. See [component derive][serde] for more details.
//!
Expand Down Expand Up @@ -114,7 +117,7 @@
//! mod pet_api {
//! # use utoipa::OpenApi;
//! # use utoipa::Component;
//! #
//! #
//! # #[derive(Component)]
//! # struct Pet {
//! # id: u64,
Expand All @@ -123,7 +126,7 @@
//! # }
//! /// Get pet by id
//! ///
//! /// Get pet from database by pet id
//! /// Get pet from database by pet id
//! #[utoipa::path(
//! get,
//! path = "/pets/{id}",
Expand All @@ -149,7 +152,7 @@
//! ```rust
//! # mod pet_api {
//! # use utoipa::Component;
//! #
//! #
//! # #[derive(Component)]
//! # struct Pet {
//! # id: u64,
Expand All @@ -159,7 +162,7 @@
//! #
//! # /// Get pet by id
//! # ///
//! # /// Get pet from database by pet id
//! # /// Get pet from database by pet id
//! # #[utoipa::path(
//! # get,
//! # path = "/pets/{id}",
Expand Down Expand Up @@ -345,7 +348,7 @@ pub trait Component {
/// #
/// /// Get pet by id
/// ///
/// /// Get pet from database by pet database id
/// /// Get pet from database by pet database id
/// #[utoipa::path(
/// get,
/// path = "/pets/{id}",
Expand Down
137 changes: 135 additions & 2 deletions src/openapi/request_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ builder! {
}

impl RequestBody {
/// Constrcut a new [`RequestBody`].
/// Construct a new [`RequestBody`].
pub fn new() -> Self {
Default::default()
}
Expand All @@ -57,9 +57,72 @@ impl RequestBodyBuilder {
}
}

/// Trait with convenience functions for documenting request bodies.
///
/// This trait requires a feature-flag to enable:
/// ```text
/// [dependencies]
/// utoipa = { version = "1", features = ["openapi_extensions"] }
/// ```
///
/// Once enabled, with a single method call we can add [`Content`] to our RequestBodyBuilder
/// that references a [`crate::Component`] schema using content-tpe "application/json":
///
/// ```rust
/// use utoipa::openapi::request_body::{RequestBodyBuilder, RequestBodyExt};
///
/// let request = RequestBodyBuilder::new().json_component_ref("EmailPayload").build();
/// ```
///
/// If serialized to JSON, the above will result in a requestBody schema like this:
///
/// ```json
/// {
/// "content": {
/// "application/json": {
/// "schema": {
/// "$ref": "#/components/schemas/EmailPayload"
/// }
/// }
/// }
/// }
/// ```
///
#[cfg(feature = "openapi_extensions")]
pub trait RequestBodyExt {
/// Add [`Content`] to [`RequestBody`] referring to a schema
/// with Content-Type `application/json`.
fn json_component_ref(self, ref_name: &str) -> Self;
}

#[cfg(feature = "openapi_extensions")]
impl RequestBodyExt for RequestBody {
fn json_component_ref(mut self, ref_name: &str) -> RequestBody {
self.content.insert(
"application/json".to_string(),
crate::openapi::Content::new(crate::openapi::Ref::from_component_name(ref_name)),
);
self
}
}


#[cfg(feature = "openapi_extensions")]
impl RequestBodyExt for RequestBodyBuilder {
fn json_component_ref(self, ref_name: &str) -> RequestBodyBuilder {
self.content(
"application/json",
crate::openapi::Content::new(crate::openapi::Ref::from_component_name(ref_name)),
)
}
}

#[cfg(test)]
mod tests {
use super::RequestBody;
use assert_json_diff::assert_json_eq;
use serde_json::json;

use super::{Content, RequestBody, RequestBodyBuilder, Required};

#[test]
fn request_body_new() {
Expand All @@ -69,4 +132,74 @@ mod tests {
assert_eq!(request_body.description, None);
assert!(request_body.required.is_none());
}

#[test]
fn request_body_builder() -> Result<(), serde_json::Error> {
let request_body = RequestBodyBuilder::new()
.description(Some("A sample requestBody"))
.required(Some(Required::True))
.content(
"application/json",
Content::new(crate::openapi::Ref::from_component_name("EmailPayload")),
)
.build();
let serialized = serde_json::to_string_pretty(&request_body)?;
println!("serialized json:\n {}", serialized);
assert_json_eq!(
request_body,
json!({
"description": "A sample requestBody",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EmailPayload"
}
}
},
"required": true
})
);
Ok(())
}
#[cfg(feature = "openapi_extensions")]
use super::RequestBodyExt;

#[cfg(feature = "openapi_extensions")]
#[test]
fn request_body_ext() {
let request_body = RequestBodyBuilder::new()
.build()
// build a RequestBody first to test the method
.json_component_ref("EmailPayload");
assert_json_eq!(
request_body,
json!({
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EmailPayload"
}
}
}
})
);
}

#[cfg(feature = "openapi_extensions")]
#[test]
fn request_body_builder_ext() {
let request_body = RequestBodyBuilder::new().json_component_ref("EmailPayload").build();
assert_json_eq!(
request_body,
json!({
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EmailPayload"
}
}
}
})
);
}
}
Loading

0 comments on commit a7907ee

Please sign in to comment.