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
  • Loading branch information
Sufflope committed Oct 6, 2024
1 parent 04a0be0 commit 621133c
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 43 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
6 changes: 3 additions & 3 deletions src/v2/codegen/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ where
self.emit_struct(def, ctx)
}

/// Checks if the given definition is a simple map and returns the corresponding `BTreeMap`.
/// Checks if the given definition is a simple map and returns the corresponding `PropertiesMap`.
fn try_emit_map(
&self,
def: &E::Definition,
Expand All @@ -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
8 changes: 4 additions & 4 deletions src/v2/codegen/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ where
/// Builds the method parameter type using the actual field type.
///
/// For example, if a field is `Vec<T>`, then we replace it (in builder method)
/// with `impl Iterator<Item=Into<T>>`, and if we had `BTreeMap<String, T>`,
/// with `impl Iterator<Item=Into<T>>`, and if we had `PropertiesMap<String, T>`,
/// then we replace it with `impl Iterator<Item = (String, T)>` and
/// we do this... recursively.
// FIXME: Investigate if there's a better way.
Expand All @@ -489,7 +489,7 @@ where
f.write_str("impl Iterator<Item = ")?;
self.write_builder_ty(&ty[i + 1..ty.len() - 1], req, needs_any, f)?;
f.write_str(">")?;
} else if ty[..i].ends_with("std::collections::BTreeMap") {
} else if ty[..i].ends_with("paperclip::v2::PropertiesMap") {
f.write_str("impl Iterator<Item = (String, ")?;
self.write_builder_ty(&ty[i + 9..ty.len() - 1], req, needs_any, f)?;
f.write_str(")>")?;
Expand Down Expand Up @@ -546,10 +546,10 @@ where
f.write_str("value.map(|value| ")?;
Self::write_value_map(&ty[i + 1..ty.len() - 1], f)?;
f.write_str(").collect::<Vec<_>>()")?;
} else if ty[..i].ends_with("std::collections::BTreeMap") {
} else if ty[..i].ends_with("paperclip::v2::PropertiesMap") {
f.write_str("value.map(|(key, value)| (key, ")?;
Self::write_value_map(&ty[i + 9..ty.len() - 1], f)?;
f.write_str(")).collect::<std::collections::BTreeMap<_, _>>()")?;
f.write_str(")).collect::<paperclip::v2::PropertiesMap<_, _>>()")?;
}
} else {
f.write_str("value")?;
Expand Down
6 changes: 3 additions & 3 deletions src/v2/codegen/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ pub struct ObjectField {
/// Required fields of the "deepest" child type in the given definition.
///
/// Now, what do I mean by "deepest"? For example, if we had `Vec<Vec<Vec<T>>>`
/// or `Vec<BTreeMap<String, Vec<BTreeMap<String, T>>>>`, then "deepest" child
/// type is T (as long as it's not a `Vec` or `BTreeMap`).
/// or `Vec<PropertiesMap<String, Vec<PropertiesMap<String, T>>>>`, then "deepest" child
/// type is T (as long as it's not a `Vec` or `PropertiesMap`).
///
/// To understand why we're doing this, see `ApiObjectBuilderImpl::write_builder_ty`
/// and `ApiObjectBuilderImpl::write_value_map` functions.
Expand Down Expand Up @@ -315,7 +315,7 @@ impl ApiObject {
if ty[..i].ends_with("Vec") {
f.write_str(&ty[..=i])?;
Self::write_field_with_any(&ty[i + 1..ty.len() - 1], f)?;
} else if ty[..i].ends_with("std::collections::BTreeMap") {
} else if ty[..i].ends_with("paperclip::v2::PropertiesMap") {
f.write_str(&ty[..i + 9])?;
Self::write_field_with_any(&ty[i + 9..ty.len() - 1], f)?;
} else {
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ expression: data
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct CertificateSigningRequestSpec {
/// Extra information about the requesting user. See user.Info interface for details.
pub extra: Option<std::collections::BTreeMap<String, Vec<String>>>,
pub extra: Option<paperclip::v2::PropertiesMap<String, Vec<String>>>,
/// Group information about the requesting user. See user.Info interface for details.
pub groups: Option<Vec<String>>,
/// Base64-encoded PKCS#10 CSR data
Expand Down Expand Up @@ -49,7 +49,7 @@ impl<Request> CertificateSigningRequestSpecBuilder<Request> {
/// Extra information about the requesting user. See user.Info interface for details.
#[inline]
pub fn extra(mut self, value: impl Iterator<Item = (String, impl Iterator<Item = impl Into<String>>)>) -> Self {
self.body.extra = Some(value.map(|(key, value)| (key, value.map(|value| value.into()).collect::<Vec<_>>().into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.body.extra = Some(value.map(|(key, value)| (key, value.map(|value| value.into()).collect::<Vec<_>>().into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ pub struct ConfigMap {
pub api_version: Option<String>,
/// BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.
#[serde(rename = "binaryData")]
pub binary_data: Option<std::collections::BTreeMap<String, String>>,
pub binary_data: Option<paperclip::v2::PropertiesMap<String, String>>,
/// Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.
pub data: Option<std::collections::BTreeMap<String, String>>,
pub data: Option<paperclip::v2::PropertiesMap<String, String>>,
/// Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds
pub kind: Option<String>,
/// Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata
Expand Down Expand Up @@ -94,14 +94,14 @@ impl ConfigMapBuilder {
/// BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.
#[inline]
pub fn binary_data(mut self, value: impl Iterator<Item = (String, impl Into<String>)>) -> Self {
self.body.binary_data = Some(value.map(|(key, value)| (key, value.into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.body.binary_data = Some(value.map(|(key, value)| (key, value.into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

/// Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.
#[inline]
pub fn data(mut self, value: impl Iterator<Item = (String, impl Into<String>)>) -> Self {
self.body.data = Some(value.map(|(key, value)| (key, value.into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.body.data = Some(value.map(|(key, value)| (key, value.into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

Expand Down Expand Up @@ -176,14 +176,14 @@ impl<Namespace> ConfigMapPostBuilder<Namespace> {
/// BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.
#[inline]
pub fn binary_data(mut self, value: impl Iterator<Item = (String, impl Into<String>)>) -> Self {
self.inner.body.binary_data = Some(value.map(|(key, value)| (key, value.into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.inner.body.binary_data = Some(value.map(|(key, value)| (key, value.into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

/// Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.
#[inline]
pub fn data(mut self, value: impl Iterator<Item = (String, impl Into<String>)>) -> Self {
self.inner.body.data = Some(value.map(|(key, value)| (key, value.into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.inner.body.data = Some(value.map(|(key, value)| (key, value.into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

Expand Down Expand Up @@ -363,14 +363,14 @@ impl<Name, Namespace> ConfigMapPutBuilder1<Name, Namespace> {
/// BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.
#[inline]
pub fn binary_data(mut self, value: impl Iterator<Item = (String, impl Into<String>)>) -> Self {
self.inner.body.binary_data = Some(value.map(|(key, value)| (key, value.into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.inner.body.binary_data = Some(value.map(|(key, value)| (key, value.into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

/// Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.
#[inline]
pub fn data(mut self, value: impl Iterator<Item = (String, impl Into<String>)>) -> Self {
self.inner.body.data = Some(value.map(|(key, value)| (key, value.into())).collect::<std::collections::BTreeMap<_, _>>().into());
self.inner.body.data = Some(value.map(|(key, value)| (key, value.into())).collect::<paperclip::v2::PropertiesMap<_, _>>().into());
self
}

Expand Down
Loading

0 comments on commit 621133c

Please sign in to comment.