Skip to content

Commit

Permalink
Refactor component derive (#12)
Browse files Browse the repository at this point in the history
* Add some missing functionalities like support for deprecated and nested Vecs
* Refactor and get rid of some unnecessities from the code
* Add support for example in struct level
* Add tests for necessary cases
  • Loading branch information
juhaku committed Jan 19, 2022
1 parent 5f64113 commit f3a7537
Show file tree
Hide file tree
Showing 10 changed files with 706 additions and 422 deletions.
4 changes: 3 additions & 1 deletion src/openapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ pub use self::{
licence::Licence,
path::{PathItem, PathItemType, Paths},
response::{Response, Responses},
schema::{Array, Component, ComponentFormat, ComponentType, Object, Property, Ref, Schema},
schema::{
Array, Component, ComponentFormat, ComponentType, Object, Property, Ref, Schema, ToArray,
},
security::Security,
server::Server,
tag::Tag,
Expand Down
59 changes: 52 additions & 7 deletions src/openapi/schema.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::Deprecated;

#[non_exhaustive]
#[derive(Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -57,13 +60,16 @@ pub struct Property {
description: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
default: Option<String>,
default: Option<Value>,

#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
enum_values: Option<Vec<String>>,

#[serde(skip_serializing_if = "Option::is_none")]
example: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
deprecated: Option<Deprecated>,
}

impl Property {
Expand All @@ -86,8 +92,8 @@ impl Property {
self
}

pub fn with_default<S: AsRef<str>>(mut self, default: S) -> Self {
self.default = Some(default.as_ref().to_string());
pub fn with_default(mut self, default: Value) -> Self {
self.default = Some(default);

self
}
Expand All @@ -108,6 +114,12 @@ impl Property {

self
}

pub fn with_deprecated(mut self, deprecated: Deprecated) -> Self {
self.deprecated = Some(deprecated);

self
}
}

impl From<Property> for Component {
Expand Down Expand Up @@ -136,6 +148,12 @@ pub struct Object {

#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
deprecated: Option<Deprecated>,

#[serde(skip_serializing_if = "Option::is_none")]
example: Option<Value>,
}

impl Object {
Expand Down Expand Up @@ -167,6 +185,18 @@ impl Object {

self
}

pub fn with_deprecated(mut self, deprecated: Deprecated) -> Self {
self.deprecated = Some(deprecated);

self
}

pub fn with_example(mut self, example: Value) -> Self {
self.example = Some(example);

self
}
}

impl From<Object> for Component {
Expand Down Expand Up @@ -237,6 +267,8 @@ impl From<Array> for Component {
}
}

impl ToArray for Array {}

pub trait ToArray
where
Component: From<Self>,
Expand Down Expand Up @@ -313,7 +345,7 @@ impl Serialize for ComponentFormat {

#[cfg(test)]
mod tests {
use serde_json::Value;
use serde_json::{json, Value};

use super::*;
use crate::openapi::*;
Expand All @@ -331,7 +363,7 @@ mod tests {
Property::new(ComponentType::Integer)
.with_format(ComponentFormat::Int32)
.with_description("Id of credential")
.with_default("1"),
.with_default(json!(1)),
)
.with_property(
"name",
Expand All @@ -341,7 +373,7 @@ mod tests {
.with_property(
"status",
Property::new(ComponentType::String)
.with_default("Active")
.with_default(json!("Active"))
.with_description("Credential status")
.with_enum_values(&["Active", "NotActive", "Locked", "Expired"]),
)
Expand Down Expand Up @@ -381,7 +413,7 @@ mod tests {
.get("id")
.unwrap_or(&serde_json::value::Value::Null)
.to_string(),
r#"{"default":"1","description":"Id of credential","format":"int32","type":"integer"}"#,
r#"{"default":1,"description":"Id of credential","format":"int32","type":"integer"}"#,
"components.schemas.Credential.properties.id did not match"
);
assert_eq!(
Expand Down Expand Up @@ -417,6 +449,19 @@ mod tests {
Ok(())
}

#[test]
fn derive_object_with_example() {
let expected = r#"{"type":"object","example":{"age":20,"name":"bob the cat"}}"#;
let json_value = Object::new().with_example(json!({"age": 20, "name": "bob the cat"}));

let value_string = serde_json::to_string(&json_value).unwrap();
assert_eq!(
value_string, expected,
"value string != expected string, {} != {}",
value_string, expected
);
}

fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
path.split('.').into_iter().fold(value, |acc, fragment| {
acc.get(fragment).unwrap_or(&serde_json::value::Value::Null)
Expand Down
108 changes: 97 additions & 11 deletions tests/component_derive_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::{collections::HashMap, vec};

use serde_json::Value;
use utoipa::{Component, OpenApi};
Expand Down Expand Up @@ -77,7 +77,7 @@ fn derive_enum_with_defaults_success() {
#[test]
fn derive_enum_with_with_custom_default_fn_success() {
let mode = api_doc! {
#[component(default = "crate::mode_custom_default_fn")]
#[component(default = mode_custom_default_fn)]
enum Mode {
Mode1,
Mode2
Expand Down Expand Up @@ -119,11 +119,12 @@ fn derive_struct_with_defaults_success() {
fn derive_struct_with_custom_properties_success() {
let book = api_doc! {
struct Book {
#[component(default = String::default)]
name: String,
#[component(
default = "testhash"
default = "testhash",
example = "base64 text",
format = "ComponentFormat::Byte"
format = ComponentFormat::Byte,
)]
hash: String,
}
Expand All @@ -132,6 +133,7 @@ fn derive_struct_with_custom_properties_success() {
assert_value! {book=>
"type" = r#""object""#, "Book type"
"properties.name.type" = r#""string""#, "Book name type"
"properties.name.default" = r#""""#, "Book name default"
"properties.hash.type" = r#""string""#, "Book hash type"
"properties.hash.format" = r#""byte""#, "Book hash format"
"properties.hash.example" = r#""base64 text""#, "Book hash example"
Expand Down Expand Up @@ -306,18 +308,102 @@ fn derive_struct_unnamed_field_vec_type_success() {
}

#[test]
#[ignore = "not supported yet!!!"]
fn derive_struct_nested_vec_success() {
let vecs = api_doc! {
struct VecTest {
vecs: Vec<Vec<String>>
}
};

println!("{:#?}", vecs);
// assert_value! {point=>
// "type" = r#""array""#, "Wrapper type"
// "items.type" = r#""integer""#, "Wrapper items type"
// "items.format" = r#""int32""#, "Wrapper items format"
// }
assert_value! {vecs=>
"properties.vecs.type" = r#""array""#, "Vecs property type"
"properties.vecs.items.type" = r#""array""#, "Vecs property items type"
"properties.vecs.items.items.type" = r#""string""#, "Vecs property items item type"
"type" = r#""object""#, "Property type"
"required.[0]" = r#""vecs""#, "Required properties"
}
common::assert_json_array_len(vecs.get("required").unwrap(), 1);
}

#[test]
fn derive_struct_with_example() {
let pet = api_doc! {
#[component(example = json!({"name": "bob the cat", "age": 8}))]
struct Pet {
name: String,
age: i32
}
};

assert_value! {pet=>
"example.name" = r#""bob the cat""#, "Pet example name"
"example.age" = r#"8"#, "Pet example age"
}
}

#[test]
fn derive_struct_with_deprecated() {
#[allow(deprecated)]
let pet = api_doc! {
#[deprecated]
struct Pet {
name: String,
#[deprecated]
age: i32
}
};

assert_value! {pet=>
"deprecated" = r#"true"#, "Pet deprecated"
"properties.name.type" = r#""string""#, "Pet properties name type"
"properties.name.deprecated" = r#"null"#, "Pet properties name deprecated"
"properties.age.type" = r#""integer""#, "Pet properties age type"
"properties.age.deprecated" = r#"true"#, "Pet properties age deprecated"
"example" = r#"null"#, "Pet example"
}
}

#[test]
fn derive_unnamed_struct_deprecated_success() {
#[allow(deprecated)]
let pet_age = api_doc! {
#[deprecated]
#[component(example = 8)]
struct PetAge(u64);
};

assert_value! {pet_age=>
"deprecated" = r#"true"#, "PetAge deprecated"
"example" = r#""8""#, "PetAge example"
}
}

#[test]
fn derive_unnamed_struct_example_json_array_success() {
let pet_age = api_doc! {
#[component(example = "0", default = u64::default)]
struct PetAge(u64, u64);
};

assert_value! {pet_age=>
"items.example" = r#""0""#, "PetAge example"
"items.default" = r#"0"#, "PetAge default"
}
}

#[test]
fn derive_enum_with_deprecated() {
#[allow(deprecated)]
let mode = api_doc! {
#[deprecated]
enum Mode {
Mode1, Mode2
}
};

assert_value! {mode=>
"enum" = r#"["Mode1","Mode2"]"#, "Mode enum variants"
"type" = r#""string""#, "Mode type"
"deprecated" = r#"true"#, "Mode deprecated"
};
}
1 change: 1 addition & 0 deletions tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde_json::json;
use utoipa::{Component, OpenApi};

#[derive(Deserialize, Serialize, Component)]
#[component(example = json!({"name": "bob the cat", "id": 1}))]
struct Pet {
id: u64,
name: String,
Expand Down
Loading

0 comments on commit f3a7537

Please sign in to comment.