Skip to content

Commit

Permalink
Add feature flags for chrono (#74)
Browse files Browse the repository at this point in the history
Add support for chrono types like DateTime, Date and Duration to be recognized as primitive types.
Previously these types caused issue as they are external types and parser identifed them as components
even the are not components.

* Add feature flags to support chrono type parsing with `chrono` and `chrono_with_format`.
  • Loading branch information
juhaku authored Apr 9, 2022
1 parent 38d71a5 commit 5d624ca
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
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
cargo test --test component_derive_test --features chrono_types
cargo test --test component_derive_test --features chrono_types_with_format
elif [[ "${{ matrix.testset }}" == "utoipa-gen" ]] && [[ ${{ steps.changes.outputs.gen_changed }} == true ]]; then
cargo test -p utoipa-gen --features actix_extras
elif [[ "${{ matrix.testset }}" == "utoipa-swagger-ui" ]] && [[ ${{ steps.changes.outputs.swagger_changed }} == true ]]; then
Expand Down
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "utoipa"
description = "Compile time generated OpenAPI documentation for Rust"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand All @@ -24,15 +24,18 @@ default = ["json"]
debug = ["utoipa-gen/debug"]
actix_extras = ["utoipa-gen/actix_extras"]
json = ["serde_json", "utoipa-gen/json"]
chrono_types = ["utoipa-gen/chrono_types"]
chrono_types_with_format = ["utoipa-gen/chrono_types_with_format"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true }
utoipa-gen = { version = "0.1.1", path = "./utoipa-gen" }
utoipa-gen = { version = "0.1.2", path = "./utoipa-gen" }

[dev-dependencies]
actix-web = { version = "4" }
paste = "1"
chrono = { version = "0.4", features = ["serde"] }

[workspace]
members = [
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ and the `ipa` is _api_ reversed. Aaand... `ipa` is also awesome type of beer :be
* **actix_extras** Enhances actix-web intgration with being able to parse some documentation
from actix web macro attributes and types. See the [path attribute macro](https://docs.rs/utoipa/0.1.1/utoipa/attr.path.html) for more details.
* **debug** Add extra traits such as debug traits to openapi definitions and elsewhere.
* **chrono_types** Add support for _**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_types_with_format** feature. This is useful because OpenAPI 3.1 spec does not have date-time formats.
* **chrono_types_with_format** Add support to _**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`.

## Install

Expand Down
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
//! * **actix_extras** Enhances actix-web intgration with being able to parse some documentation
//! from actix web macro attributes and types. See [`utoipa::path(...)`][path] for more details.
//! * **debug** Add extra traits such as debug traits to openapi definitions and elsewhere.
//! * **chrono_types** Add support for _**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.
//! * **chrono_types_with_format** Add support to _**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`.
//!
//! # Install
//!
Expand Down
55 changes: 55 additions & 0 deletions tests/component_derive_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![cfg(feature = "serde_json")]
use std::{borrow::Cow, cell::RefCell, collections::HashMap, vec};

use chrono::{Date, DateTime, Duration, Utc};
use serde_json::Value;
use utoipa::{Component, OpenApi};

Expand Down Expand Up @@ -601,3 +602,57 @@ fn derive_struct_xml() {
"properties.photos_urls.items.xml.wrapped" = r###"null"###, "User photos_urls links xml items wrapped"
}
}

#[cfg(feature = "chrono_types_with_format")]
#[test]
fn derive_component_with_chrono_types_with_chrono_with_format_feature() {
let post = api_doc! {
struct Post {
id: i32,
value: String,
datetime: DateTime<Utc>,
date: Date<Utc>,
duration: Duration,
}
};

assert_value! {post=>
"properties.datetime.type" = r#""string""#, "Post datetime type"
"properties.datetime.format" = r#""date-time""#, "Post datetime format"
"properties.date.type" = r#""string""#, "Post date type"
"properties.date.format" = r#""date""#, "Post date format"
"properties.duration.type" = r#""string""#, "Post duration type"
"properties.duration.format" = r#"null"#, "Post duration format"
"properties.id.type" = r#""integer""#, "Post id type"
"properties.id.format" = r#""int32""#, "Post id format"
"properties.value.type" = r#""string""#, "Post value type"
"properties.value.format" = r#"null"#, "Post value format"
}
}

#[cfg(feature = "chrono_types")]
#[test]
fn derive_component_with_chrono_types_with_chrono_feature() {
let post = api_doc! {
struct Post {
id: i32,
value: String,
datetime: DateTime<Utc>,
date: Date<Utc>,
duration: Duration,
}
};

assert_value! {post=>
"properties.datetime.type" = r#""string""#, "Post datetime type"
"properties.datetime.format" = r#"null"#, "Post datetime format"
"properties.date.type" = r#""string""#, "Post date type"
"properties.date.format" = r#"null"#, "Post date format"
"properties.duration.type" = r#""string""#, "Post duration type"
"properties.duration.format" = r#"null"#, "Post duration format"
"properties.id.type" = r#""integer""#, "Post id type"
"properties.id.format" = r#""int32""#, "Post id format"
"properties.value.type" = r#""string""#, "Post value type"
"properties.value.format" = r#"null"#, "Post value format"
}
}
4 changes: 3 additions & 1 deletion utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "utoipa-gen"
description = "Code generation implementation for utoipa"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand Down Expand Up @@ -30,4 +30,6 @@ actix-web = { version = "4" }
[features]
debug = ["syn/extra-traits"]
actix_extras = ["regex", "lazy_static"]
chrono_types = []
chrono_types_with_format = []
json = []
107 changes: 78 additions & 29 deletions utoipa-gen/src/component_type.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,67 @@
use std::fmt::Display;

use proc_macro2::Ident;
use quote::{quote, ToTokens};

/// Tokenizes OpenAPI data type correctly according to the Rust type
pub(crate) struct ComponentType<'a>(pub &'a Ident);
pub(crate) struct ComponentType<'a, T: Display>(pub &'a T);

impl<'a> ComponentType<'a> {
impl<'a, T> ComponentType<'a, T>
where
T: Display,
{
/// Check whether type is known to be primitive in wich case returns true.
pub(crate) fn is_primitive(&self) -> bool {
let name = &*self.0.to_string();

matches!(
name,
"String"
| "str"
| "char"
| "bool"
| "usize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "isize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "f32"
| "f64"
)
let primitive = is_primitive(name);

#[cfg(any(feature = "chrono_types", feature = "chrono_types_with_format"))]
let mut primitive = primitive;

#[cfg(any(feature = "chrono_types", feature = "chrono_types_with_format"))]
if !primitive {
primitive = is_primitive_chrono(name);
}

primitive
}
}

impl<'a> ToTokens for ComponentType<'a> {
#[inline]
fn is_primitive(name: &str) -> bool {
matches!(
name,
"String"
| "str"
| "char"
| "bool"
| "usize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "isize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "f32"
| "f64"
)
}

#[inline]
#[cfg(any(feature = "chrono_types", feature = "chrono_types_with_format"))]
fn is_primitive_chrono(name: &str) -> bool {
matches!(name, "DateTime" | "Date" | "Duration")
}

impl<'a, T> ToTokens for ComponentType<'a, T>
where
T: Display,
{
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let name = &*self.0.to_string();

Expand All @@ -47,6 +73,10 @@ impl<'a> ToTokens for ComponentType<'a> {
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64"
| "u128" | "usize" => tokens.extend(quote! {utoipa::openapi::ComponentType::Integer}),
"f32" | "f64" => tokens.extend(quote! {utoipa::openapi::ComponentType::Number}),
#[cfg(any(feature = "chrono_types", feature = "chrono_types_with_format"))]
"DateTime" | "Date" | "Duration" => {
tokens.extend(quote! { utoipa::openapi::ComponentType::String })
}
_ => tokens.extend(quote! {utoipa::openapi::ComponentType::Object}),
}
}
Expand All @@ -61,13 +91,28 @@ impl<T: Display> ComponentFormat<T> {
pub(crate) fn is_known_format(&self) -> bool {
let name = &*self.0.to_string();

matches!(
name,
"i8" | "i16" | "i32" | "u8" | "u16" | "u32" | "i64" | "u64" | "f32" | "f64"
)
let known_format = is_known_format(name);

#[cfg(feature = "chrono_types_with_format")]
let mut known_format = known_format;

#[cfg(feature = "chrono_types_with_format")]
if !known_format {
known_format = matches!(name, "DateTime" | "Date");
}

known_format
}
}

#[inline]
fn is_known_format(name: &str) -> bool {
matches!(
name,
"i8" | "i16" | "i32" | "u8" | "u16" | "u32" | "i64" | "u64" | "f32" | "f64"
)
}

impl<T: Display> ToTokens for ComponentFormat<T> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let name = &*self.0.to_string();
Expand All @@ -78,6 +123,10 @@ impl<T: Display> ToTokens for ComponentFormat<T> {
}
"i64" | "u64" => tokens.extend(quote! {utoipa::openapi::ComponentFormat::Int64}),
"f32" | "f64" => tokens.extend(quote! {utoipa::openapi::ComponentFormat::Float}),
#[cfg(feature = "chrono_types_with_format")]
"DateTime" => tokens.extend(quote! { utoipa::openapi::ComponentFormat::DateTime}),
#[cfg(feature = "chrono_types_with_format")]
"Date" => tokens.extend(quote! { utoipa::openapi::ComponentFormat::Date}),
_ => (),
}
}
Expand Down
5 changes: 3 additions & 2 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::Display;
use std::{io::Error, str::FromStr};

use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
Expand Down Expand Up @@ -492,10 +493,10 @@ impl ToTokens for Operation<'_> {
}

trait ContentTypeResolver {
fn resolve_content_type<'a>(
fn resolve_content_type<'a, T: Display>(
&self,
content_type: Option<&'a String>,
component_type: &ComponentType<'a>,
component_type: &ComponentType<'a, T>,
) -> &'a str {
if let Some(content_type) = content_type {
content_type
Expand Down
19 changes: 13 additions & 6 deletions utoipa-gen/src/path/property.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
use proc_macro2::Ident;
use std::fmt::Display;

use quote::{quote, ToTokens};

use crate::component_type::{ComponentFormat, ComponentType};

/// Tokenizable object property. It is used as a object property for components or as property
/// of request or response body or response header.
pub(crate) struct Property<'a> {
pub(crate) struct Property<'a, T: Display> {
pub(crate) is_array: bool,
pub(crate) component_type: ComponentType<'a>,
pub(crate) component_type: ComponentType<'a, T>,
}

impl<'a> Property<'a> {
pub fn new(is_array: bool, ident: &'a Ident) -> Self {
impl<'a, T> Property<'a, T>
where
T: Display,
{
pub fn new(is_array: bool, ident: &'a T) -> Self {
Self {
is_array,
component_type: ComponentType(ident),
}
}
}

impl ToTokens for Property<'_> {
impl<T> ToTokens for Property<'_, T>
where
T: Display,
{
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
if self.component_type.is_primitive() {
let component_type = &self.component_type;
Expand Down

0 comments on commit 5d624ca

Please sign in to comment.