Skip to content

Commit

Permalink
Adds support for serde(flatten) (#229)
Browse files Browse the repository at this point in the history
* Adds support for serde(flatten)

This change adds support for #[serde(flatten)] item attribute, which allows to reuse structs for building flat JSON queries and responses like in example below:
```
    #[derive(Deserialize, Serialize, Apiv2Schema)]
    struct Paging {
        /// Starting image number
        offset: i32,
        /// Total images found
        total: i32,
        /// Page size
        size: i32,
    };

    #[derive(Serialize, Apiv2Schema)]
    struct Image {
        data: String,
        id: Uuid,
        time: chrono_dev::DateTime<chrono_dev::Utc>,
    }

    /// Images response with paging information embedded
    #[derive(Serialize, Apiv2Schema)]
    struct Images {
        data: Vec<Image>,
        #[serde(flatten)]
        paging: Paging,
    }
```

Covered with relevant test.

* Fix tests

* Apply code review suggestions
  • Loading branch information
dunnock authored Oct 4, 2020
1 parent b4db533 commit bdf51fe
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 4 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ required-features = ["v2", "codegen"]
[[test]]
name = "test_errors"
required-features = ["v2", "codegen"]

[[test]]
name = "test_app"
required-features = ["cli", "actix", "uuid", "chrono"]
46 changes: 42 additions & 4 deletions macros/src/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,15 +850,20 @@ fn handle_field_struct(
let docs = extract_documentation(&field.attrs);
let docs = docs.trim();

let mut gen = quote!(
{
let mut gen = if !SerdeFlatten::exists(&field.attrs) {
quote!({
let mut s = #ty_ref::raw_schema();
if !#docs.is_empty() {
s.description = Some(#docs.to_string());
}
schema.properties.insert(#field_name.into(), s.into());
}
);
})
} else {
quote!({
let s = #ty_ref::raw_schema();
schema.properties.extend(s.properties);
})
};

if is_required {
gen.extend(quote! {
Expand Down Expand Up @@ -1057,3 +1062,36 @@ impl SerdeProps {
props
}
}

/// Supported flattening of embedded struct (https://serde.rs/variant-attrs.html).
struct SerdeFlatten;

impl SerdeFlatten {
/// Traverses the field attributes and returns true if there is `#[serde(flatten)]`.
fn exists(field_attrs: &[Attribute]) -> bool {
for meta in field_attrs.iter().filter_map(|a| a.parse_meta().ok()) {
let inner_meta = match meta {
Meta::List(ref l)
if l.path
.segments
.last()
.map(|p| p.ident == "serde")
.unwrap_or(false) =>
{
&l.nested
}
_ => continue,
};

for meta in inner_meta {
if let NestedMeta::Meta(Meta::Path(syn::Path { segments, .. })) = meta {
if segments.iter().any(|p| p.ident == "flatten") {
return true;
}
}
}
}

false
}
}
163 changes: 163 additions & 0 deletions tests/test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,169 @@ fn test_map_in_out() {
);
}

#[test]
fn test_serde_flatten() {
#[derive(Deserialize, Serialize, Apiv2Schema)]
struct PagedQuery {
/// First image number to return
offset: Option<i32>,
/// Return number of images
size: Option<i32>,
};

#[derive(Deserialize, Serialize, Apiv2Schema)]
struct Paging {
/// Starting image number
offset: i32,
/// Total images found
total: i32,
/// Page size
size: i32,
};

#[derive(Serialize, Apiv2Schema)]
struct Image {
data: String,
id: Uuid,
time: chrono_dev::DateTime<chrono_dev::Utc>,
}

/// Images response with paging information embedded
#[derive(Serialize, Apiv2Schema)]
struct Images {
data: Vec<Image>,
#[serde(flatten)]
paging: Paging,
}

/// Query images from library by name
#[derive(Deserialize, Apiv2Schema)]
struct ImagesQuery {
#[serde(flatten)]
paging: PagedQuery,
name: Option<String>,
}

#[api_v2_operation]
async fn some_images(_filter: web::Query<ImagesQuery>) -> Result<web::Json<Images>, ()> {
#[allow(unreachable_code)]
if _filter.paging.offset.is_some() && _filter.name.is_some() {
unimplemented!()
}
unimplemented!()
}

run_and_check_app(
|| {
App::new()
.wrap_api()
.with_json_spec_at("/api/spec")
.service(web::resource("/images").route(web::get().to(some_images)))
.build()
},
|addr| {
let resp = CLIENT
.get(&format!("http://{}/api/spec", addr))
.send()
.expect("request failed?");

check_json(
resp,
json!({
"definitions": {
"Images": {
"properties": {
"data": {
"items": {
"properties": {
"data": {
"type": "string"
},
"id": {
"format": "uuid",
"type": "string"
},
"time": {
"format": "date-time",
"type": "string"
}
},
"required": [
"data",
"id",
"time"
]
},
"type": "array"
},
"offset": {
"description": "Starting image number",
"format": "int32",
"type": "integer"
},
"size": {
"description": "Page size",
"format": "int32",
"type": "integer"
},
"total": {
"description": "Total images found",
"format": "int32",
"type": "integer"
}
},
"required": [
"data",
"paging"
]
}
},
"info": {
"title": "",
"version": ""
},
"paths": {
"/images": {
"get": {
"parameters": [
{
"in": "query",
"name": "name",
"type": "string"
},
{
"description": "First image number to return",
"format": "int32",
"in": "query",
"name": "offset",
"type": "integer"
},
{
"description": "Return number of images",
"format": "int32",
"in": "query",
"name": "size",
"type": "integer"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Images"
}
}
}
}
}
},
"swagger": "2.0"
}),
);
},
);
}

#[test]
fn test_list_in_out() {
#[derive(Serialize, Deserialize, Apiv2Schema)]
Expand Down

0 comments on commit bdf51fe

Please sign in to comment.