Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support enumflags2::BitFlags and optional x-enumNames extension #221

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# IDE
.idea/
5 changes: 5 additions & 0 deletions schemars/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ rust_decimal = { version = "1", default-features = false, optional = true }
bigdecimal = { version = "0.3", default-features = false, optional = true }
enumset = { version = "1.0", optional = true }
smol_str = { version = "0.1.17", optional = true }
enumflags2 = { version = "0.7.7", optional = true }

[dev-dependencies]
pretty_assertions = "1.2.1"
Expand Down Expand Up @@ -107,5 +108,9 @@ required-features = ["enumset"]
name = "smol_str"
required-features = ["smol_str"]

[[test]]
name = "enumflags2"
required-features = ["enumflags2"]

[package.metadata.docs.rs]
all-features = true
26 changes: 26 additions & 0 deletions schemars/src/json_schema_impls/enumflags2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use enumflags2::{BitFlags, _internal::RawBitFlags};

impl<T> JsonSchema for BitFlags<T> where T: JsonSchema + RawBitFlags {
fn is_referenceable() -> bool {
true
}

fn schema_name() -> String {
format!("BitFlags_{}", T::schema_name())
}

fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let target = gen.subschema_for::<T>();
let repr = u64::json_schema(gen);
match (repr, target) {
(Schema::Object(mut o), Schema::Object(target)) => {
o.metadata = target.metadata;
Schema::Object(o).into()
},
(repr, _) => repr,
}
}
}
2 changes: 2 additions & 0 deletions schemars/src/json_schema_impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ mod core;
mod decimal;
#[cfg(feature = "either")]
mod either;
#[cfg(feature = "enumflags2")]
mod enumflags2;
#[cfg(feature = "enumset")]
mod enumset;
mod ffi;
Expand Down
16 changes: 16 additions & 0 deletions schemars/tests/enum_repr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ fn enum_repr() -> TestResult {
test_default_generated_schema::<Enum>("enum-repr")
}

#[derive(JsonSchema_repr)]
#[repr(u8)]
#[schemars(extension = "x-enumNames")]
pub enum EnumWithXEnumNames {
Zero,
One,
Five = 5,
Six,
Three = 3,
}

#[test]
fn enum_repr_with_x_enum_names() -> TestResult {
test_default_generated_schema::<EnumWithXEnumNames>("enum-repr-with-x-enum-names")
}

#[derive(JsonSchema_repr)]
#[repr(i64)]
#[serde(rename = "Renamed")]
Expand Down
26 changes: 26 additions & 0 deletions schemars/tests/enumflags2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
mod util;

use schemars::JsonSchema_repr;
use enumflags2::{bitflags, BitFlags};
use util::*;

#[derive(Copy, Clone, JsonSchema_repr)]
#[repr(u8)]
#[schemars(extension = "x-enumNames")]
#[bitflags]
pub enum EnumState {
A,
B,
C,
D
}

#[test]
fn enum_state() -> TestResult {
test_default_generated_schema::<EnumState>("enum-state")
}

#[test]
fn enum_bitflags_state() -> TestResult {
test_default_generated_schema::<BitFlags<EnumState>>("enum-bitflags-state")
}
24 changes: 24 additions & 0 deletions schemars/tests/expected/enum-bitflags-state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "BitFlags_EnumState",
"type": "integer",
"format": "uint64",
"minimum": 0.0,
"definitions": {
"EnumState": {
"type": "integer",
"enum": [
1,
2,
4,
8
],
"x-enumNames": [
"A",
"B",
"C",
"D"
]
}
}
}
19 changes: 19 additions & 0 deletions schemars/tests/expected/enum-repr-with-x-enum-names.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EnumWithXEnumNames",
"type": "integer",
"enum": [
0,
1,
5,
6,
3
],
"x-enumNames": [
"Zero",
"One",
"Five",
"Six",
"Three"
]
}
17 changes: 17 additions & 0 deletions schemars/tests/expected/enum-state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "EnumState",
"type": "integer",
"enum": [
1,
2,
4,
8
],
"x-enumNames": [
"A",
"B",
"C",
"D"
]
}
1 change: 1 addition & 0 deletions schemars_derive/src/ast/from_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ impl<'a> FromSerde for Container<'a> {
original: serde.original,
// FIXME this allows with/schema_with attribute on containers
attrs: Attrs::new(&serde.original.attrs, errors),
extensions: Vec::new(),
})
}
}
Expand Down
19 changes: 18 additions & 1 deletion schemars_derive/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct Container<'a> {
pub generics: syn::Generics,
pub original: &'a syn::DeriveInput,
pub attrs: Attrs,
pub extensions: Vec<String>,
}

pub enum Data<'a> {
Expand Down Expand Up @@ -39,10 +40,26 @@ pub struct Field<'a> {

impl<'a> Container<'a> {
pub fn from_ast(item: &'a syn::DeriveInput) -> Result<Container<'a>, Vec<syn::Error>> {
let mut extensions: Vec<String> = Vec::new();
item.attrs.iter().for_each(|a| {
if a.path.is_ident("schemars") {
if let Ok(syn::Meta::List(lst)) = a.parse_meta() {
for it in lst.nested {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue { path, lit: syn::Lit::Str(s), .. })) = it {
if path.is_ident("extension") {
extensions.push(s.value());
}
}
}
}
}
});

let ctxt = Ctxt::new();
let result = serde_ast::Container::from_ast(&ctxt, item, Derive::Deserialize)
.ok_or(())
.and_then(|serde| Self::from_serde(&ctxt, serde));
.and_then(|serde| Self::from_serde(&ctxt, serde))
.map(|c| Container { extensions, ..c });

ctxt.check()
.map(|_| result.expect("from_ast set no errors on Ctxt, so should have returned Ok"))
Expand Down
2 changes: 1 addition & 1 deletion schemars_derive/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct Attrs {
pub examples: Vec<syn::Path>,
pub repr: Option<syn::Type>,
pub crate_name: Option<syn::Path>,
pub is_renamed: bool,
pub is_renamed: bool
}

#[derive(Debug)]
Expand Down
5 changes: 4 additions & 1 deletion schemars_derive/src/attr/schemars_to_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[
// JsonSchema on remote types, but we parse that ourselves rather than using serde_derive_internals.
"serialize_with",
"with",
// Special case - `extension` is removed from serde attrs, so is only respected when present in schemars attr.
"extension"
];

// If a struct/variant/field has any #[schemars] attributes, then create copies of them
Expand Down Expand Up @@ -75,7 +77,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
.flatten()
.filter_map(|meta| {
let keyword = get_meta_ident(ctxt, &meta).ok()?;
if keyword.ends_with("with") || !SERDE_KEYWORDS.contains(&keyword.as_ref()) {
if keyword.ends_with("with") || keyword.ends_with("extension") || !SERDE_KEYWORDS.contains(&keyword.as_ref()) {
None
} else {
Some((meta, keyword))
Expand All @@ -98,6 +100,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
if !schemars_meta_names.contains(&i)
&& SERDE_KEYWORDS.contains(&i.as_ref())
&& i != "bound"
&& i != "extensions"
{
serde_meta.push(meta);
}
Expand Down
33 changes: 29 additions & 4 deletions schemars_derive/src/schema_exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,35 @@ pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
let enum_ident = &cont.ident;
let variant_idents = variants.iter().map(|v| &v.ident);

let mut schema_expr = schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
});
let extensions: Vec<TokenStream> = cont.extensions.iter().filter_map(|e| {
if e == "x-enumNames" {
let variant_names = variants.iter().map(|v| {
let ident = &v.ident;
quote! { stringify!(#ident) }
});
Some(quote! {
(
"x-enumNames".to_string(),
serde_json::json!([ #(#variant_names),* ])
)
})
} else {
None
}
}).collect();

let mut schema_expr = if extensions.is_empty() {
schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
})
} else {
schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
extensions: [ #(#extensions),* ].iter().map(|i| i.clone()).collect(),
})
};

cont.attrs.as_metadata().apply_to_schema(&mut schema_expr);
Ok(schema_expr)
Expand Down