Skip to content

Commit

Permalink
feat: add preserve_order feature to keep struct fields order in schema
Browse files Browse the repository at this point in the history
Fixes #538
  • Loading branch information
Sufflope committed Oct 6, 2024
1 parent 04a0be0 commit b390150
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 14 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ actix-base = ["v2", "paperclip-macros/actix"]
swagger-ui = ["paperclip-actix/swagger-ui"]
rapidoc = ["paperclip-actix/rapidoc"]
path-in-definition = ["paperclip-macros/path-in-definition"]
preserve_order = ["paperclip-core/preserve_order"]

# OpenAPI support (v2 and codegen)
cli = ["env_logger", "structopt", "git2", "v2", "codegen"]
Expand Down
2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ actix-session = { version = "0", optional = true }
actix-identity = { version = "0", optional = true }
actix-files = { version = "0", optional = true }
chrono = { version = "0.4", optional = true }
indexmap = { version = "2", optional = true }
jiff = { version = "0.1.5", optional = true }
heck = { version = "0.4", optional = true }
once_cell = "1.4"
Expand Down Expand Up @@ -57,6 +58,7 @@ nightly = ["paperclip-macros/nightly"]
v2 = ["paperclip-macros/v2"]
v3 = ["v2", "openapiv3"]
codegen = ["v2", "heck", "log"]
preserve_order = ["dep:indexmap", "indexmap/serde", "serde_json/preserve_order"]
uuid = ["uuid0"]
uuid0 = ["uuid0_dep"]
uuid1 = ["uuid1_dep"]
5 changes: 5 additions & 0 deletions core/src/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ use self::resolver::Resolver;
#[cfg(feature = "codegen")]
use crate::error::ValidationError;

#[cfg(feature = "preserve_order")]
pub type PropertiesMap<K, V> = indexmap::IndexMap<K, V>;
#[cfg(not(feature = "preserve_order"))]
pub type PropertiesMap<K, V> = std::collections::BTreeMap<K, V>;

#[cfg(feature = "codegen")]
impl<S: Schema + Default> ResolvableApi<S> {
/// Consumes this API schema, resolves the references and returns
Expand Down
13 changes: 8 additions & 5 deletions core/src/v2/schema.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Traits used for code and spec generation.
use super::models::{
DataType, DataTypeFormat, DefaultOperationRaw, DefaultSchemaRaw, Either, Resolvable,
SecurityScheme,
use super::{
models::{
DataType, DataTypeFormat, DefaultOperationRaw, DefaultSchemaRaw, Either, Resolvable,
SecurityScheme,
},
PropertiesMap,
};

use std::collections::{BTreeMap, BTreeSet};
Expand Down Expand Up @@ -39,10 +42,10 @@ pub trait Schema: Sized {
fn additional_properties_mut(&mut self) -> Option<&mut Either<bool, Resolvable<Self>>>;

/// Map of names and schema for properties, if it's an object (`properties` field)
fn properties(&self) -> Option<&BTreeMap<String, Resolvable<Self>>>;
fn properties(&self) -> Option<&PropertiesMap<String, Resolvable<Self>>>;

/// Mutable access to `properties` field.
fn properties_mut(&mut self) -> Option<&mut BTreeMap<String, Resolvable<Self>>>;
fn properties_mut(&mut self) -> Option<&mut PropertiesMap<String, Resolvable<Self>>>;

/// Returns the required properties (if any) for this object.
fn required_properties(&self) -> Option<&BTreeSet<String>>;
Expand Down
4 changes: 2 additions & 2 deletions core/src/v3/schema.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::v2::models::Either;
use crate::v2::{models::Either, PropertiesMap};

use super::{invalid_referenceor, v2};
use std::ops::Deref;
Expand Down Expand Up @@ -65,7 +65,7 @@ fn v2_data_type_to_v3(
format: &Option<v2::DataTypeFormat>,
enum_: &[serde_json::Value],
items: &Option<Box<v2::DefaultSchemaRaw>>,
properties: &std::collections::BTreeMap<String, Box<v2::DefaultSchemaRaw>>,
properties: &PropertiesMap<String, Box<v2::DefaultSchemaRaw>>,
extra_properties: &Option<Either<bool, Box<v2::DefaultSchemaRaw>>>,
required: &std::collections::BTreeSet<String>,
) -> openapiv3::SchemaKind {
Expand Down
8 changes: 4 additions & 4 deletions macros/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ pub fn emit_v2_schema_struct(input: TokenStream) -> TokenStream {
}

#[inline]
fn properties(&self) -> Option<&std::collections::BTreeMap<String, paperclip::v2::models::Resolvable<Self>>> {
fn properties(&self) -> Option<&paperclip::v2::PropertiesMap<String, paperclip::v2::models::Resolvable<Self>>> {
if self.properties.is_empty() {
None
} else {
Expand All @@ -157,7 +157,7 @@ pub fn emit_v2_schema_struct(input: TokenStream) -> TokenStream {
}

#[inline]
fn properties_mut(&mut self) -> Option<&mut std::collections::BTreeMap<String, paperclip::v2::models::Resolvable<Self>>> {
fn properties_mut(&mut self) -> Option<&mut paperclip::v2::PropertiesMap<String, paperclip::v2::models::Resolvable<Self>>> {
if self.properties.is_empty() {
None
} else {
Expand Down Expand Up @@ -298,8 +298,8 @@ fn schema_fields(name: &Ident, is_ref: bool) -> proc_macro2::TokenStream {
));

gen.extend(quote!(
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub properties: std::collections::BTreeMap<String,
#[serde(default, skip_serializing_if = "paperclip::v2::PropertiesMap::is_empty")]
pub properties: paperclip::v2::PropertiesMap<String,
));
add_self(&mut gen);
gen.extend(quote!(>,));
Expand Down
4 changes: 2 additions & 2 deletions src/v2/codegen/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ where
let ty = self
.build_def(&schema, ctx.clone().define(false))?
.known_type();
let map = format!("std::collections::BTreeMap<String, {}>", ty);
let map = format!("paperclip::v2::PropertiesMap<String, {}>", ty);
Ok(EmittedUnit::Known(map))
}
_ => Ok(EmittedUnit::None),
Expand Down Expand Up @@ -673,7 +673,7 @@ where
if let Some(Either::Left(true)) = def.additional_properties() {
obj.fields_mut().push(ObjectField {
name: EXTRA_PROPS_FIELD.into(),
ty_path: "std::collections::BTreeMap<String, Any>".into(),
ty_path: "paperclip::v2::PropertiesMap<String, Any>".into(),
description: None,
is_required: false,
needs_any: true,
Expand Down
2 changes: 1 addition & 1 deletion src/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub use paperclip_core::{
v2::{
models::{self, DefaultSchema, ResolvableApi},
schema::{self, Schema},
serde_json,
serde_json, PropertiesMap,
},
};

Expand Down
82 changes: 82 additions & 0 deletions tests/test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5202,6 +5202,88 @@ fn test_schema_with_generics() {
);
}

#[test]
fn test_schema_properties_order() {
/// A good boy/girl.
#[derive(Apiv2Schema, Deserialize, Serialize)]
struct Dog {
/// Pick a good one.
name: String,
/// Be sure to chip them!
id: usize,
/// To each their own preferred one.
breed: String,
}

#[post("/echo")]
#[api_v2_operation]
async fn echo(body: web::Json<Dog>) -> Result<web::Json<Dog>, Error> {
Ok(body)
}

run_and_check_app(
|| {
App::new()
.wrap_api()
.service(echo)
.with_raw_json_spec(|app, spec| {
app.route(
"/api/spec",
web::get().to(move || {
#[cfg(feature = "actix4")]
{
let spec = spec.clone();
async move {
paperclip::actix::HttpResponseWrapper(
actix_web::HttpResponse::Ok().json(&spec),
)
}
}

#[cfg(not(feature = "actix4"))]
actix_web::HttpResponse::Ok().json(&spec)
}),
)
})
.build()
},
|addr| {
let resp = CLIENT
.get(&format!("http://{}/api/spec", addr))
.send()
.expect("request failed?");

assert_eq!(resp.status().as_u16(), 200);
let json = resp.json::<serde_json::Value>().expect("json error");

let properties = json
.as_object()
.expect("schema is not an object")
.get("definitions")
.expect("schema does not have definitions")
.as_object()
.expect("definitions is not an object")
.get("Dog")
.expect("no dog definition")
.as_object()
.expect("dog definition is not an object")
.get("properties")
.expect("no dog properties")
.as_object()
.expect("dog properties is not an object")
.keys()
.collect::<Vec<_>>();

#[cfg(feature = "preserve_order")]
let expected = ["name", "id", "breed"];
#[cfg(not(feature = "preserve_order"))]
let expected = ["breed", "id", "name"];

assert_eq!(properties, expected);
},
);
}

#[test]
#[cfg(feature = "path-in-definition")]
fn test_module_path_in_definition_name() {
Expand Down

0 comments on commit b390150

Please sign in to comment.