diff --git a/.gitignore b/.gitignore index 088ba6ba..40cb88fa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +# IDE +.idea/ diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index ad972dd9..bd60db10 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -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" @@ -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 diff --git a/schemars/src/json_schema_impls/enumflags2.rs b/schemars/src/json_schema_impls/enumflags2.rs new file mode 100644 index 00000000..ba1c275a --- /dev/null +++ b/schemars/src/json_schema_impls/enumflags2.rs @@ -0,0 +1,26 @@ +use crate::gen::SchemaGenerator; +use crate::schema::*; +use crate::JsonSchema; +use enumflags2::{BitFlags, _internal::RawBitFlags}; + +impl JsonSchema for BitFlags 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::(); + 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, + } + } +} diff --git a/schemars/src/json_schema_impls/mod.rs b/schemars/src/json_schema_impls/mod.rs index 490a544e..69f98888 100644 --- a/schemars/src/json_schema_impls/mod.rs +++ b/schemars/src/json_schema_impls/mod.rs @@ -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; diff --git a/schemars/tests/enum_repr.rs b/schemars/tests/enum_repr.rs index d3183477..68ffc5f3 100644 --- a/schemars/tests/enum_repr.rs +++ b/schemars/tests/enum_repr.rs @@ -17,6 +17,22 @@ fn enum_repr() -> TestResult { test_default_generated_schema::("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::("enum-repr-with-x-enum-names") +} + #[derive(JsonSchema_repr)] #[repr(i64)] #[serde(rename = "Renamed")] diff --git a/schemars/tests/enumflags2.rs b/schemars/tests/enumflags2.rs new file mode 100644 index 00000000..6695f51c --- /dev/null +++ b/schemars/tests/enumflags2.rs @@ -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::("enum-state") +} + +#[test] +fn enum_bitflags_state() -> TestResult { + test_default_generated_schema::>("enum-bitflags-state") +} diff --git a/schemars/tests/expected/enum-bitflags-state.json b/schemars/tests/expected/enum-bitflags-state.json new file mode 100644 index 00000000..29b0dac9 --- /dev/null +++ b/schemars/tests/expected/enum-bitflags-state.json @@ -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" + ] + } + } +} diff --git a/schemars/tests/expected/enum-repr-with-x-enum-names.json b/schemars/tests/expected/enum-repr-with-x-enum-names.json new file mode 100644 index 00000000..41fdcef1 --- /dev/null +++ b/schemars/tests/expected/enum-repr-with-x-enum-names.json @@ -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" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum-state.json b/schemars/tests/expected/enum-state.json new file mode 100644 index 00000000..4fddae95 --- /dev/null +++ b/schemars/tests/expected/enum-state.json @@ -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" + ] +} \ No newline at end of file diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs index 453c247f..245464f5 100644 --- a/schemars_derive/src/ast/from_serde.rs +++ b/schemars_derive/src/ast/from_serde.rs @@ -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(), }) } } diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 7b9052e4..d512a7da 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -12,6 +12,7 @@ pub struct Container<'a> { pub generics: syn::Generics, pub original: &'a syn::DeriveInput, pub attrs: Attrs, + pub extensions: Vec, } pub enum Data<'a> { @@ -39,10 +40,26 @@ pub struct Field<'a> { impl<'a> Container<'a> { pub fn from_ast(item: &'a syn::DeriveInput) -> Result, Vec> { + let mut extensions: Vec = 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")) diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index f790694a..97a3eb94 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -27,7 +27,7 @@ pub struct Attrs { pub examples: Vec, pub repr: Option, pub crate_name: Option, - pub is_renamed: bool, + pub is_renamed: bool } #[derive(Debug)] diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index 7878a2c1..403f194b 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -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 @@ -75,7 +77,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { .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)) @@ -98,6 +100,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { if !schemars_meta_names.contains(&i) && SERDE_KEYWORDS.contains(&i.as_ref()) && i != "bound" + && i != "extensions" { serde_meta.push(meta); } diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 1f55084b..a25c6aa9 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -49,10 +49,35 @@ pub fn expr_for_repr(cont: &Container) -> Result { 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 = 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)