Merge pull request #78 from GREsau/validate
Read #[validate(...)] attributes
This commit is contained in:
commit
cec8751c82
31 changed files with 1447 additions and 191 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -38,6 +38,8 @@ jobs:
|
||||||
run: cargo check --verbose --no-default-features
|
run: cargo check --verbose --no-default-features
|
||||||
continue-on-error: ${{ matrix.allow_failure }}
|
continue-on-error: ${{ matrix.allow_failure }}
|
||||||
working-directory: ./schemars
|
working-directory: ./schemars
|
||||||
|
- if: matrix.rust == '1.37.0'
|
||||||
|
run: cargo update -p indexmap --precise 1.6.2
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose ${{ matrix.test_features }} --no-fail-fast
|
run: cargo test --verbose ${{ matrix.test_features }} --no-fail-fast
|
||||||
continue-on-error: ${{ matrix.allow_failure }}
|
continue-on-error: ${{ matrix.allow_failure }}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
pub struct MyStruct {
|
pub struct MyStruct {
|
||||||
#[serde(rename = "thisIsOverridden")]
|
#[serde(rename = "thisIsOverridden")]
|
||||||
#[schemars(rename = "myNumber")]
|
#[schemars(rename = "myNumber", range(min = 1, max = 10))]
|
||||||
pub my_int: i32,
|
pub my_int: i32,
|
||||||
pub my_bool: bool,
|
pub my_bool: bool,
|
||||||
#[schemars(default)]
|
#[schemars(default)]
|
||||||
|
@ -15,8 +15,11 @@ pub struct MyStruct {
|
||||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||||
#[schemars(untagged)]
|
#[schemars(untagged)]
|
||||||
pub enum MyEnum {
|
pub enum MyEnum {
|
||||||
StringNewType(String),
|
StringNewType(#[schemars(phone)] String),
|
||||||
StructVariant { floats: Vec<f32> },
|
StructVariant {
|
||||||
|
#[schemars(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
},
|
},
|
||||||
"myNumber": {
|
"myNumber": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
@ -31,7 +33,8 @@
|
||||||
"MyEnum": {
|
"MyEnum": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -44,7 +47,9 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "float"
|
"format": "float"
|
||||||
}
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
docs/_includes/examples/validate.rs
Normal file
24
docs/_includes/examples/validate.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use schemars::{schema_for, JsonSchema};
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct MyStruct {
|
||||||
|
#[validate(range(min = 1, max = 10))]
|
||||||
|
pub my_int: i32,
|
||||||
|
pub my_bool: bool,
|
||||||
|
#[validate(required)]
|
||||||
|
pub my_nullable_enum: Option<MyEnum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub enum MyEnum {
|
||||||
|
StringNewType(#[validate(phone)] String),
|
||||||
|
StructVariant {
|
||||||
|
#[validate(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(MyStruct);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
64
docs/_includes/examples/validate.schema.json
Normal file
64
docs/_includes/examples/validate.schema.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "MyStruct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"my_bool",
|
||||||
|
"my_int",
|
||||||
|
"my_nullable_enum"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"my_bool": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"my_int": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"my_nullable_enum": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StringNewType"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StringNewType": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StructVariant"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StructVariant": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"floats"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"floats": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
pub struct MyStruct {
|
pub struct MyStruct {
|
||||||
#[serde(rename = "thisIsOverridden")]
|
#[serde(rename = "thisIsOverridden")]
|
||||||
#[schemars(rename = "myNumber")]
|
#[schemars(rename = "myNumber", range(min = 1, max = 10))]
|
||||||
pub my_int: i32,
|
pub my_int: i32,
|
||||||
pub my_bool: bool,
|
pub my_bool: bool,
|
||||||
#[schemars(default)]
|
#[schemars(default)]
|
||||||
|
@ -15,8 +15,11 @@ pub struct MyStruct {
|
||||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||||
#[schemars(untagged)]
|
#[schemars(untagged)]
|
||||||
pub enum MyEnum {
|
pub enum MyEnum {
|
||||||
StringNewType(String),
|
StringNewType(#[schemars(phone)] String),
|
||||||
StructVariant { floats: Vec<f32> },
|
StructVariant {
|
||||||
|
#[schemars(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
},
|
},
|
||||||
"myNumber": {
|
"myNumber": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
@ -31,7 +33,8 @@
|
||||||
"MyEnum": {
|
"MyEnum": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -44,7 +47,9 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "float"
|
"format": "float"
|
||||||
}
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
schemars/examples/validate.rs
Normal file
24
schemars/examples/validate.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use schemars::{schema_for, JsonSchema};
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct MyStruct {
|
||||||
|
#[validate(range(min = 1, max = 10))]
|
||||||
|
pub my_int: i32,
|
||||||
|
pub my_bool: bool,
|
||||||
|
#[validate(required)]
|
||||||
|
pub my_nullable_enum: Option<MyEnum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub enum MyEnum {
|
||||||
|
StringNewType(#[validate(phone)] String),
|
||||||
|
StructVariant {
|
||||||
|
#[validate(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(MyStruct);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
64
schemars/examples/validate.schema.json
Normal file
64
schemars/examples/validate.schema.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "MyStruct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"my_bool",
|
||||||
|
"my_int",
|
||||||
|
"my_nullable_enum"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"my_bool": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"my_int": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"my_nullable_enum": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StringNewType"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StringNewType": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StructVariant"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StructVariant": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"floats"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"floats": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,13 @@ use crate::schema::{Metadata, Schema, SchemaObject};
|
||||||
use crate::JsonSchema;
|
use crate::JsonSchema;
|
||||||
|
|
||||||
// Helper for generating schemas for flattened `Option` fields.
|
// Helper for generating schemas for flattened `Option` fields.
|
||||||
pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(gen: &mut SchemaGenerator) -> Schema {
|
pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(
|
||||||
|
gen: &mut SchemaGenerator,
|
||||||
|
required: bool,
|
||||||
|
) -> Schema {
|
||||||
let mut schema = T::_schemars_private_non_optional_json_schema(gen);
|
let mut schema = T::_schemars_private_non_optional_json_schema(gen);
|
||||||
if T::_schemars_private_is_option() {
|
|
||||||
|
if T::_schemars_private_is_option() && !required {
|
||||||
if let Schema::Object(SchemaObject {
|
if let Schema::Object(SchemaObject {
|
||||||
object: Some(ref mut object_validation),
|
object: Some(ref mut object_validation),
|
||||||
..
|
..
|
||||||
|
@ -15,35 +19,16 @@ pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(gen: &mut SchemaGenerator
|
||||||
object_validation.required.clear();
|
object_validation.required.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
schema
|
schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for generating schemas for `Option` fields.
|
pub fn apply_metadata(schema: Schema, metadata: Metadata) -> Schema {
|
||||||
pub fn add_schema_as_property<T: ?Sized + JsonSchema>(
|
if metadata == Metadata::default() {
|
||||||
gen: &mut SchemaGenerator,
|
schema
|
||||||
parent: &mut SchemaObject,
|
} else {
|
||||||
name: String,
|
|
||||||
metadata: Option<Metadata>,
|
|
||||||
required: bool,
|
|
||||||
) {
|
|
||||||
let mut schema = gen.subschema_for::<T>();
|
|
||||||
schema = apply_metadata(schema, metadata);
|
|
||||||
|
|
||||||
let object = parent.object();
|
|
||||||
if required && !T::_schemars_private_is_option() {
|
|
||||||
object.required.insert(name.clone());
|
|
||||||
}
|
|
||||||
object.properties.insert(name, schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_metadata(schema: Schema, metadata: Option<Metadata>) -> Schema {
|
|
||||||
match metadata {
|
|
||||||
None => schema,
|
|
||||||
Some(ref metadata) if *metadata == Metadata::default() => schema,
|
|
||||||
Some(metadata) => {
|
|
||||||
let mut schema_obj = schema.into_object();
|
let mut schema_obj = schema.into_object();
|
||||||
schema_obj.metadata = Some(Box::new(metadata)).merge(schema_obj.metadata);
|
schema_obj.metadata = Some(Box::new(metadata)).merge(schema_obj.metadata);
|
||||||
Schema::Object(schema_obj)
|
Schema::Object(schema_obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::JsonSchema;
|
||||||
use crate::{Map, Set};
|
use crate::{Map, Set};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
/// A JSON Schema.
|
/// A JSON Schema.
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
@ -191,7 +192,13 @@ where
|
||||||
macro_rules! get_or_insert_default_fn {
|
macro_rules! get_or_insert_default_fn {
|
||||||
($name:ident, $ret:ty) => {
|
($name:ident, $ret:ty) => {
|
||||||
get_or_insert_default_fn!(
|
get_or_insert_default_fn!(
|
||||||
concat!("Returns a mutable reference to this schema's [`", stringify!($ret), "`](#structfield.", stringify!($name), "), creating it if it was `None`."),
|
concat!(
|
||||||
|
"Returns a mutable reference to this schema's [`",
|
||||||
|
stringify!($ret),
|
||||||
|
"`](#structfield.",
|
||||||
|
stringify!($name),
|
||||||
|
"), creating it if it was `None`."
|
||||||
|
),
|
||||||
$name,
|
$name,
|
||||||
$ret
|
$ret
|
||||||
);
|
);
|
||||||
|
@ -224,6 +231,17 @@ impl SchemaObject {
|
||||||
self.reference.is_some()
|
self.reference.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `self` accepts values of the given type, according to the [`instance_type`] field.
|
||||||
|
///
|
||||||
|
/// This is a basic check that always returns `true` if no `instance_type` is specified on the schema,
|
||||||
|
/// and does not check any subschemas. Because of this, both `{}` and `{"not": {}}` accept any type according
|
||||||
|
/// to this method.
|
||||||
|
pub fn has_type(&self, ty: InstanceType) -> bool {
|
||||||
|
self.instance_type
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |x| x.contains(&ty))
|
||||||
|
}
|
||||||
|
|
||||||
get_or_insert_default_fn!(metadata, Metadata);
|
get_or_insert_default_fn!(metadata, Metadata);
|
||||||
get_or_insert_default_fn!(subschemas, SubschemaValidation);
|
get_or_insert_default_fn!(subschemas, SubschemaValidation);
|
||||||
get_or_insert_default_fn!(number, NumberValidation);
|
get_or_insert_default_fn!(number, NumberValidation);
|
||||||
|
@ -506,3 +524,28 @@ impl<T> From<Vec<T>> for SingleOrVec<T> {
|
||||||
SingleOrVec::Vec(vec)
|
SingleOrVec::Vec(vec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: PartialEq> SingleOrVec<T> {
|
||||||
|
/// Returns `true` if `self` is either a `Single` equal to `x`, or a `Vec` containing `x`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use schemars::schema::SingleOrVec;
|
||||||
|
///
|
||||||
|
/// let s = SingleOrVec::from(10);
|
||||||
|
/// assert!(s.contains(&10));
|
||||||
|
/// assert!(!s.contains(&20));
|
||||||
|
///
|
||||||
|
/// let v = SingleOrVec::from(vec![10, 20]);
|
||||||
|
/// assert!(v.contains(&10));
|
||||||
|
/// assert!(v.contains(&20));
|
||||||
|
/// assert!(!v.contains(&30));
|
||||||
|
/// ```
|
||||||
|
pub fn contains(&self, x: &T) -> bool {
|
||||||
|
match self {
|
||||||
|
SingleOrVec::Single(s) => s.deref() == x,
|
||||||
|
SingleOrVec::Vec(v) => v.contains(x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,16 @@
|
||||||
],
|
],
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"InnerStruct": {
|
"InnerStruct": {
|
||||||
"type": "object"
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
104
schemars/tests/expected/validate.json
Normal file
104
schemars/tests/expected/validate.json
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Struct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"contains_str1",
|
||||||
|
"contains_str2",
|
||||||
|
"email_address",
|
||||||
|
"homepage",
|
||||||
|
"map_contains",
|
||||||
|
"min_max",
|
||||||
|
"min_max2",
|
||||||
|
"non_empty_str",
|
||||||
|
"non_empty_str2",
|
||||||
|
"pair",
|
||||||
|
"regex_str1",
|
||||||
|
"regex_str2",
|
||||||
|
"regex_str3",
|
||||||
|
"required_option",
|
||||||
|
"tel",
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"min_max": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 100.0,
|
||||||
|
"minimum": 0.01
|
||||||
|
},
|
||||||
|
"min_max2": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 1000.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"regex_str1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
},
|
||||||
|
"regex_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
},
|
||||||
|
"regex_str3": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+$"
|
||||||
|
},
|
||||||
|
"contains_str1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
},
|
||||||
|
"contains_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
},
|
||||||
|
"email_address": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email"
|
||||||
|
},
|
||||||
|
"tel": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"non_empty_str": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"non_empty_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1000,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"pair": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"map_contains": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"map_key"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required_option": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
schemars/tests/expected/validate_newtype.json
Normal file
8
schemars/tests/expected/validate_newtype.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "NewType",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint8",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 0.0
|
||||||
|
}
|
104
schemars/tests/expected/validate_schemars_attrs.json
Normal file
104
schemars/tests/expected/validate_schemars_attrs.json
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Struct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"contains_str1",
|
||||||
|
"contains_str2",
|
||||||
|
"email_address",
|
||||||
|
"homepage",
|
||||||
|
"map_contains",
|
||||||
|
"min_max",
|
||||||
|
"min_max2",
|
||||||
|
"non_empty_str",
|
||||||
|
"non_empty_str2",
|
||||||
|
"pair",
|
||||||
|
"regex_str1",
|
||||||
|
"regex_str2",
|
||||||
|
"regex_str3",
|
||||||
|
"required_option",
|
||||||
|
"tel",
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"min_max": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 100.0,
|
||||||
|
"minimum": 0.01
|
||||||
|
},
|
||||||
|
"min_max2": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 1000.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"regex_str1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
},
|
||||||
|
"regex_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
},
|
||||||
|
"regex_str3": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+$"
|
||||||
|
},
|
||||||
|
"contains_str1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
},
|
||||||
|
"contains_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
},
|
||||||
|
"email_address": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email"
|
||||||
|
},
|
||||||
|
"tel": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"non_empty_str": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"non_empty_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1000,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"pair": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"map_contains": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"map_key"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required_option": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
schemars/tests/expected/validate_tuple.json
Normal file
18
schemars/tests/expected/validate_tuple.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Tuple",
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "uint8",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
}
|
|
@ -56,7 +56,9 @@ build_enum!(
|
||||||
#[derive(Debug, JsonSchema)]
|
#[derive(Debug, JsonSchema)]
|
||||||
OuterEnum {
|
OuterEnum {
|
||||||
#[derive(Debug, JsonSchema)]
|
#[derive(Debug, JsonSchema)]
|
||||||
InnerStruct {}
|
InnerStruct {
|
||||||
|
x: i32
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,7 +22,7 @@ error: duplicate serde attribute `deny_unknown_fields`
|
||||||
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
||||||
| ^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: unknown schemars container attribute `foo`
|
error: unknown schemars attribute `foo`
|
||||||
--> $DIR/invalid_attrs.rs:8:25
|
--> $DIR/invalid_attrs.rs:8:25
|
||||||
|
|
|
|
||||||
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
||||||
|
|
35
schemars/tests/ui/invalid_validation_attrs.rs
Normal file
35
schemars/tests/ui/invalid_validation_attrs.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct3(
|
||||||
|
#[validate(
|
||||||
|
regex = "foo",
|
||||||
|
contains = "bar",
|
||||||
|
regex(path = "baz"),
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
url
|
||||||
|
)]
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct4(
|
||||||
|
#[schemars(
|
||||||
|
regex = "foo",
|
||||||
|
contains = "bar",
|
||||||
|
regex(path = "baz"),
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
url
|
||||||
|
)]
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn main() {}
|
59
schemars/tests/ui/invalid_validation_attrs.stderr
Normal file
59
schemars/tests/ui/invalid_validation_attrs.stderr
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
error: expected validate regex attribute to be a string: `regex = "..."`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:4:39
|
||||||
|
|
|
||||||
|
4 | pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^
|
||||||
|
|
||||||
|
error: unknown schemars attribute `foo`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:42
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error: expected schemars regex attribute to be a string: `regex = "..."`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:39
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^
|
||||||
|
|
||||||
|
error: schemars attribute cannot contain both `equal` and `min`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:63
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^^^^^
|
||||||
|
|
||||||
|
error: unknown item in schemars length attribute
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:74
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error: schemars attribute cannot contain both `contains` and `regex`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:26:9
|
||||||
|
|
|
||||||
|
26 | contains = "bar",
|
||||||
|
| ^^^^^^^^
|
||||||
|
|
||||||
|
error: duplicate schemars attribute `regex`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:27:9
|
||||||
|
|
|
||||||
|
27 | regex(path = "baz"),
|
||||||
|
| ^^^^^
|
||||||
|
|
||||||
|
error: schemars attribute cannot contain both `phone` and `email`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:29:9
|
||||||
|
|
|
||||||
|
29 | email,
|
||||||
|
| ^^^^^
|
||||||
|
|
||||||
|
error: schemars attribute cannot contain both `phone` and `url`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:30:9
|
||||||
|
|
|
||||||
|
30 | url
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error[E0425]: cannot find value `foo` in this scope
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:12:17
|
||||||
|
|
|
||||||
|
12 | regex = "foo",
|
||||||
|
| ^^^^^ not found in this scope
|
|
@ -4,4 +4,4 @@ error: JsonSchema_repr: missing #[repr(...)] attribute
|
||||||
3 | #[derive(JsonSchema_repr)]
|
3 | #[derive(JsonSchema_repr)]
|
||||||
| ^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^
|
||||||
|
|
|
|
||||||
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
= note: this error originates in the derive macro `JsonSchema_repr` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
|
@ -4,4 +4,4 @@ error: This argument to `schema_for!` is not a type - did you mean to use `schem
|
||||||
4 | let _schema = schema_for!(123);
|
4 | let _schema = schema_for!(123);
|
||||||
| ^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^
|
||||||
|
|
|
|
||||||
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
= note: this error originates in the macro `schema_for` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
120
schemars/tests/validate.rs
Normal file
120
schemars/tests/validate.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
mod util;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
// In real code, this would typically be a Regex, potentially created in a `lazy_static!`.
|
||||||
|
static STARTS_WITH_HELLO: &'static str = r"^[Hh]ello\b";
|
||||||
|
|
||||||
|
const MIN: u32 = 1;
|
||||||
|
const MAX: u32 = 1000;
|
||||||
|
|
||||||
|
#[derive(Debug, JsonSchema)]
|
||||||
|
pub struct Struct {
|
||||||
|
#[validate(range(min = 0.01, max = 100))]
|
||||||
|
min_max: f32,
|
||||||
|
#[validate(range(min = "MIN", max = "MAX"))]
|
||||||
|
min_max2: f32,
|
||||||
|
#[validate(regex = "STARTS_WITH_HELLO")]
|
||||||
|
regex_str1: String,
|
||||||
|
#[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))]
|
||||||
|
regex_str2: String,
|
||||||
|
#[validate(regex(pattern = r"^\d+$"))]
|
||||||
|
regex_str3: String,
|
||||||
|
#[validate(contains = "substring...")]
|
||||||
|
contains_str1: String,
|
||||||
|
#[validate(contains(pattern = "substring...", message = "bar"))]
|
||||||
|
contains_str2: String,
|
||||||
|
#[validate(email)]
|
||||||
|
email_address: String,
|
||||||
|
#[validate(phone)]
|
||||||
|
tel: String,
|
||||||
|
#[validate(url)]
|
||||||
|
homepage: String,
|
||||||
|
#[validate(length(min = 1, max = 100))]
|
||||||
|
non_empty_str: String,
|
||||||
|
#[validate(length(min = "MIN", max = "MAX"))]
|
||||||
|
non_empty_str2: String,
|
||||||
|
#[validate(length(equal = 2))]
|
||||||
|
pair: Vec<i32>,
|
||||||
|
#[validate(contains = "map_key")]
|
||||||
|
map_contains: HashMap<String, ()>,
|
||||||
|
#[validate(required)]
|
||||||
|
required_option: Option<bool>,
|
||||||
|
#[validate(required)]
|
||||||
|
#[serde(flatten)]
|
||||||
|
required_flattened: Option<Inner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, JsonSchema)]
|
||||||
|
pub struct Inner {
|
||||||
|
x: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate() -> TestResult {
|
||||||
|
test_default_generated_schema::<Struct>("validate")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, JsonSchema)]
|
||||||
|
pub struct Struct2 {
|
||||||
|
#[schemars(range(min = 0.01, max = 100))]
|
||||||
|
min_max: f32,
|
||||||
|
#[schemars(range(min = "MIN", max = "MAX"))]
|
||||||
|
min_max2: f32,
|
||||||
|
#[validate(regex = "overridden")]
|
||||||
|
#[schemars(regex = "STARTS_WITH_HELLO")]
|
||||||
|
regex_str1: String,
|
||||||
|
#[schemars(regex(path = "STARTS_WITH_HELLO"))]
|
||||||
|
regex_str2: String,
|
||||||
|
#[schemars(regex(pattern = r"^\d+$"))]
|
||||||
|
regex_str3: String,
|
||||||
|
#[validate(regex = "overridden")]
|
||||||
|
#[schemars(contains = "substring...")]
|
||||||
|
contains_str1: String,
|
||||||
|
#[schemars(contains(pattern = "substring..."))]
|
||||||
|
contains_str2: String,
|
||||||
|
#[schemars(email)]
|
||||||
|
email_address: String,
|
||||||
|
#[schemars(phone)]
|
||||||
|
tel: String,
|
||||||
|
#[schemars(url)]
|
||||||
|
homepage: String,
|
||||||
|
#[schemars(length(min = 1, max = 100))]
|
||||||
|
non_empty_str: String,
|
||||||
|
#[schemars(length(min = "MIN", max = "MAX"))]
|
||||||
|
non_empty_str2: String,
|
||||||
|
#[schemars(length(equal = 2))]
|
||||||
|
pair: Vec<i32>,
|
||||||
|
#[schemars(contains = "map_key")]
|
||||||
|
map_contains: HashMap<String, ()>,
|
||||||
|
#[schemars(required)]
|
||||||
|
required_option: Option<bool>,
|
||||||
|
#[schemars(required)]
|
||||||
|
#[serde(flatten)]
|
||||||
|
required_flattened: Option<Inner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_schemars_attrs() -> TestResult {
|
||||||
|
test_default_generated_schema::<Struct>("validate_schemars_attrs")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, JsonSchema)]
|
||||||
|
pub struct Tuple(
|
||||||
|
#[validate(range(max = 10))] u8,
|
||||||
|
#[validate(required)] Option<bool>,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_tuple() -> TestResult {
|
||||||
|
test_default_generated_schema::<Tuple>("validate_tuple")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, JsonSchema)]
|
||||||
|
pub struct NewType(#[validate(range(max = 10))] u8);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_newtype() -> TestResult {
|
||||||
|
test_default_generated_schema::<NewType>("validate_newtype")
|
||||||
|
}
|
|
@ -73,6 +73,7 @@ impl<'a> FromSerde for Field<'a> {
|
||||||
ty: serde.ty,
|
ty: serde.ty,
|
||||||
original: serde.original,
|
original: serde.original,
|
||||||
attrs: Attrs::new(&serde.original.attrs, errors),
|
attrs: Attrs::new(&serde.original.attrs, errors),
|
||||||
|
validation_attrs: ValidationAttrs::new(&serde.original.attrs, errors),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
mod from_serde;
|
mod from_serde;
|
||||||
|
|
||||||
use crate::attr::Attrs;
|
use crate::attr::{Attrs, ValidationAttrs};
|
||||||
use from_serde::FromSerde;
|
use from_serde::FromSerde;
|
||||||
use serde_derive_internals::ast as serde_ast;
|
use serde_derive_internals::ast as serde_ast;
|
||||||
use serde_derive_internals::{Ctxt, Derive};
|
use serde_derive_internals::{Ctxt, Derive};
|
||||||
|
@ -34,6 +34,7 @@ pub struct Field<'a> {
|
||||||
pub ty: &'a syn::Type,
|
pub ty: &'a syn::Type,
|
||||||
pub original: &'a syn::Field,
|
pub original: &'a syn::Field,
|
||||||
pub attrs: Attrs,
|
pub attrs: Attrs,
|
||||||
|
pub validation_attrs: ValidationAttrs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Container<'a> {
|
impl<'a> Container<'a> {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
mod doc;
|
mod doc;
|
||||||
mod schemars_to_serde;
|
mod schemars_to_serde;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
pub use schemars_to_serde::process_serde_attrs;
|
pub use schemars_to_serde::process_serde_attrs;
|
||||||
|
pub use validation::ValidationAttrs;
|
||||||
|
|
||||||
|
use crate::metadata::SchemaMetadata;
|
||||||
use proc_macro2::{Group, Span, TokenStream, TokenTree};
|
use proc_macro2::{Group, Span, TokenStream, TokenTree};
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use serde_derive_internals::Ctxt;
|
use serde_derive_internals::Ctxt;
|
||||||
|
@ -51,6 +54,27 @@ impl Attrs {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_metadata(&self) -> SchemaMetadata<'_> {
|
||||||
|
#[allow(clippy::ptr_arg)]
|
||||||
|
fn none_if_empty(s: &String) -> Option<&str> {
|
||||||
|
if s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SchemaMetadata {
|
||||||
|
title: self.title.as_ref().and_then(none_if_empty),
|
||||||
|
description: self.description.as_ref().and_then(none_if_empty),
|
||||||
|
deprecated: self.deprecated,
|
||||||
|
examples: &self.examples,
|
||||||
|
read_only: false,
|
||||||
|
write_only: false,
|
||||||
|
default: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn populate(
|
fn populate(
|
||||||
mut self,
|
mut self,
|
||||||
attrs: &[syn::Attribute],
|
attrs: &[syn::Attribute],
|
||||||
|
@ -141,10 +165,7 @@ impl Attrs {
|
||||||
_ if ignore_errors => {}
|
_ if ignore_errors => {}
|
||||||
|
|
||||||
Meta(meta_item) => {
|
Meta(meta_item) => {
|
||||||
let is_known_serde_keyword = schemars_to_serde::SERDE_KEYWORDS
|
if !is_known_serde_or_validation_keyword(meta_item) {
|
||||||
.iter()
|
|
||||||
.any(|k| meta_item.path().is_ident(k));
|
|
||||||
if !is_known_serde_keyword {
|
|
||||||
let path = meta_item
|
let path = meta_item
|
||||||
.path()
|
.path()
|
||||||
.into_token_stream()
|
.into_token_stream()
|
||||||
|
@ -152,16 +173,13 @@ impl Attrs {
|
||||||
.replace(' ', "");
|
.replace(' ', "");
|
||||||
errors.error_spanned_by(
|
errors.error_spanned_by(
|
||||||
meta_item.path(),
|
meta_item.path(),
|
||||||
format!("unknown schemars container attribute `{}`", path),
|
format!("unknown schemars attribute `{}`", path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Lit(lit) => {
|
Lit(lit) => {
|
||||||
errors.error_spanned_by(
|
errors.error_spanned_by(lit, "unexpected literal in schemars attribute");
|
||||||
lit,
|
|
||||||
"unexpected literal in schemars container attribute",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,6 +187,16 @@ impl Attrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool {
|
||||||
|
let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS
|
||||||
|
.iter()
|
||||||
|
.chain(validation::VALIDATION_KEYWORDS);
|
||||||
|
meta.path()
|
||||||
|
.get_ident()
|
||||||
|
.map(|i| known_keywords.any(|k| i == k))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_meta_items(
|
fn get_meta_items(
|
||||||
attr: &syn::Attribute,
|
attr: &syn::Attribute,
|
||||||
attr_type: &'static str,
|
attr_type: &'static str,
|
||||||
|
|
498
schemars_derive/src/attr/validation.rs
Normal file
498
schemars_derive/src/attr/validation.rs
Normal file
|
@ -0,0 +1,498 @@
|
||||||
|
use super::{get_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str};
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use serde_derive_internals::Ctxt;
|
||||||
|
use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta, Path};
|
||||||
|
|
||||||
|
pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
|
||||||
|
"range", "regex", "contains", "email", "phone", "url", "length", "required",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
enum Format {
|
||||||
|
Email,
|
||||||
|
Uri,
|
||||||
|
Phone,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format {
|
||||||
|
fn attr_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Format::Email => "email",
|
||||||
|
Format::Uri => "url",
|
||||||
|
Format::Phone => "phone",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Format::Email => "email",
|
||||||
|
Format::Uri => "uri",
|
||||||
|
Format::Phone => "phone",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ValidationAttrs {
|
||||||
|
length_min: Option<Expr>,
|
||||||
|
length_max: Option<Expr>,
|
||||||
|
length_equal: Option<Expr>,
|
||||||
|
range_min: Option<Expr>,
|
||||||
|
range_max: Option<Expr>,
|
||||||
|
regex: Option<Expr>,
|
||||||
|
contains: Option<String>,
|
||||||
|
required: bool,
|
||||||
|
format: Option<Format>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationAttrs {
|
||||||
|
pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
|
||||||
|
ValidationAttrs::default()
|
||||||
|
.populate(attrs, "schemars", false, errors)
|
||||||
|
.populate(attrs, "validate", true, errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required(&self) -> bool {
|
||||||
|
self.required
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate(
|
||||||
|
mut self,
|
||||||
|
attrs: &[syn::Attribute],
|
||||||
|
attr_type: &'static str,
|
||||||
|
ignore_errors: bool,
|
||||||
|
errors: &Ctxt,
|
||||||
|
) -> Self {
|
||||||
|
let duplicate_error = |path: &Path| {
|
||||||
|
if !ignore_errors {
|
||||||
|
let msg = format!(
|
||||||
|
"duplicate schemars attribute `{}`",
|
||||||
|
path.get_ident().unwrap()
|
||||||
|
);
|
||||||
|
errors.error_spanned_by(path, msg)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mutual_exclusive_error = |path: &Path, other: &str| {
|
||||||
|
if !ignore_errors {
|
||||||
|
let msg = format!(
|
||||||
|
"schemars attribute cannot contain both `{}` and `{}`",
|
||||||
|
path.get_ident().unwrap(),
|
||||||
|
other,
|
||||||
|
);
|
||||||
|
errors.error_spanned_by(path, msg)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let duplicate_format_error = |existing: Format, new: Format, path: &syn::Path| {
|
||||||
|
if !ignore_errors {
|
||||||
|
let msg = if existing == new {
|
||||||
|
format!("duplicate schemars attribute `{}`", existing.attr_str())
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"schemars attribute cannot contain both `{}` and `{}`",
|
||||||
|
existing.attr_str(),
|
||||||
|
new.attr_str(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
errors.error_spanned_by(path, msg)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for meta_item in attrs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|attr| get_meta_items(attr, attr_type, errors))
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
match &meta_item {
|
||||||
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => {
|
||||||
|
for nested in meta_list.nested.iter() {
|
||||||
|
match nested {
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
|
||||||
|
if self.length_min.is_some() {
|
||||||
|
duplicate_error(&nv.path)
|
||||||
|
} else if self.length_equal.is_some() {
|
||||||
|
mutual_exclusive_error(&nv.path, "equal")
|
||||||
|
} else {
|
||||||
|
self.length_min = str_or_num_to_expr(&errors, "min", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
|
||||||
|
if self.length_max.is_some() {
|
||||||
|
duplicate_error(&nv.path)
|
||||||
|
} else if self.length_equal.is_some() {
|
||||||
|
mutual_exclusive_error(&nv.path, "equal")
|
||||||
|
} else {
|
||||||
|
self.length_max = str_or_num_to_expr(&errors, "max", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("equal") => {
|
||||||
|
if self.length_equal.is_some() {
|
||||||
|
duplicate_error(&nv.path)
|
||||||
|
} else if self.length_min.is_some() {
|
||||||
|
mutual_exclusive_error(&nv.path, "min")
|
||||||
|
} else if self.length_max.is_some() {
|
||||||
|
mutual_exclusive_error(&nv.path, "max")
|
||||||
|
} else {
|
||||||
|
self.length_equal =
|
||||||
|
str_or_num_to_expr(&errors, "equal", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars length attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("range") => {
|
||||||
|
for nested in meta_list.nested.iter() {
|
||||||
|
match nested {
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
|
||||||
|
if self.range_min.is_some() {
|
||||||
|
duplicate_error(&nv.path)
|
||||||
|
} else {
|
||||||
|
self.range_min = str_or_num_to_expr(&errors, "min", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
|
||||||
|
if self.range_max.is_some() {
|
||||||
|
duplicate_error(&nv.path)
|
||||||
|
} else {
|
||||||
|
self.range_max = str_or_num_to_expr(&errors, "max", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars range attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::Path(m))
|
||||||
|
if m.is_ident("required") || m.is_ident("required_nested") =>
|
||||||
|
{
|
||||||
|
self.required = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Email.attr_str()) => {
|
||||||
|
match self.format {
|
||||||
|
Some(f) => duplicate_format_error(f, Format::Email, p),
|
||||||
|
None => self.format = Some(Format::Email),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Uri.attr_str()) => {
|
||||||
|
match self.format {
|
||||||
|
Some(f) => duplicate_format_error(f, Format::Uri, p),
|
||||||
|
None => self.format = Some(Format::Uri),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Phone.attr_str()) => {
|
||||||
|
match self.format {
|
||||||
|
Some(f) => duplicate_format_error(f, Format::Phone, p),
|
||||||
|
None => self.format = Some(Format::Phone),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("regex") => {
|
||||||
|
match (&self.regex, &self.contains) {
|
||||||
|
(Some(_), _) => duplicate_error(&nv.path),
|
||||||
|
(None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"),
|
||||||
|
(None, None) => {
|
||||||
|
self.regex =
|
||||||
|
parse_lit_into_expr_path(errors, attr_type, "regex", &nv.lit).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("regex") => {
|
||||||
|
match (&self.regex, &self.contains) {
|
||||||
|
(Some(_), _) => duplicate_error(&meta_list.path),
|
||||||
|
(None, Some(_)) => mutual_exclusive_error(&meta_list.path, "contains"),
|
||||||
|
(None, None) => {
|
||||||
|
for x in meta_list.nested.iter() {
|
||||||
|
match x {
|
||||||
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||||
|
path,
|
||||||
|
lit,
|
||||||
|
..
|
||||||
|
})) if path.is_ident("path") => {
|
||||||
|
self.regex =
|
||||||
|
parse_lit_into_expr_path(errors, attr_type, "path", lit)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||||
|
path,
|
||||||
|
lit,
|
||||||
|
..
|
||||||
|
})) if path.is_ident("pattern") => {
|
||||||
|
self.regex = get_lit_str(errors, attr_type, "pattern", lit)
|
||||||
|
.ok()
|
||||||
|
.map(|litstr| {
|
||||||
|
Expr::Lit(syn::ExprLit {
|
||||||
|
attrs: Vec::new(),
|
||||||
|
lit: Lit::Str(litstr.clone()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars regex attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
|
||||||
|
if path.is_ident("contains") =>
|
||||||
|
{
|
||||||
|
match (&self.contains, &self.regex) {
|
||||||
|
(Some(_), _) => duplicate_error(&path),
|
||||||
|
(None, Some(_)) => mutual_exclusive_error(&path, "regex"),
|
||||||
|
(None, None) => {
|
||||||
|
self.contains = get_lit_str(errors, attr_type, "contains", lit)
|
||||||
|
.map(|litstr| litstr.value())
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => {
|
||||||
|
match (&self.contains, &self.regex) {
|
||||||
|
(Some(_), _) => duplicate_error(&meta_list.path),
|
||||||
|
(None, Some(_)) => mutual_exclusive_error(&meta_list.path, "regex"),
|
||||||
|
(None, None) => {
|
||||||
|
for x in meta_list.nested.iter() {
|
||||||
|
match x {
|
||||||
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||||
|
path,
|
||||||
|
lit,
|
||||||
|
..
|
||||||
|
})) if path.is_ident("pattern") => {
|
||||||
|
self.contains =
|
||||||
|
get_lit_str(errors, attr_type, "contains", lit)
|
||||||
|
.ok()
|
||||||
|
.map(|litstr| litstr.value())
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!(
|
||||||
|
"unknown item in schemars contains attribute"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
|
||||||
|
let mut array_validation = Vec::new();
|
||||||
|
let mut number_validation = Vec::new();
|
||||||
|
let mut object_validation = Vec::new();
|
||||||
|
let mut string_validation = Vec::new();
|
||||||
|
|
||||||
|
if let Some(length_min) = self
|
||||||
|
.length_min
|
||||||
|
.as_ref()
|
||||||
|
.or_else(|| self.length_equal.as_ref())
|
||||||
|
{
|
||||||
|
string_validation.push(quote! {
|
||||||
|
validation.min_length = Some(#length_min as u32);
|
||||||
|
});
|
||||||
|
array_validation.push(quote! {
|
||||||
|
validation.min_items = Some(#length_min as u32);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(length_max) = self
|
||||||
|
.length_max
|
||||||
|
.as_ref()
|
||||||
|
.or_else(|| self.length_equal.as_ref())
|
||||||
|
{
|
||||||
|
string_validation.push(quote! {
|
||||||
|
validation.max_length = Some(#length_max as u32);
|
||||||
|
});
|
||||||
|
array_validation.push(quote! {
|
||||||
|
validation.max_items = Some(#length_max as u32);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(range_min) = &self.range_min {
|
||||||
|
number_validation.push(quote! {
|
||||||
|
validation.minimum = Some(#range_min as f64);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(range_max) = &self.range_max {
|
||||||
|
number_validation.push(quote! {
|
||||||
|
validation.maximum = Some(#range_max as f64);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(regex) = &self.regex {
|
||||||
|
string_validation.push(quote! {
|
||||||
|
validation.pattern = Some(#regex.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(contains) = &self.contains {
|
||||||
|
object_validation.push(quote! {
|
||||||
|
validation.required.insert(#contains.to_string());
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.regex.is_none() {
|
||||||
|
let pattern = crate::regex_syntax::escape(contains);
|
||||||
|
string_validation.push(quote! {
|
||||||
|
validation.pattern = Some(#pattern.to_string());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let format = self.format.as_ref().map(|f| {
|
||||||
|
let f = f.schema_str();
|
||||||
|
quote! {
|
||||||
|
schema_object.format = Some(#f.to_string());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let array_validation = wrap_array_validation(array_validation);
|
||||||
|
let number_validation = wrap_number_validation(number_validation);
|
||||||
|
let object_validation = wrap_object_validation(object_validation);
|
||||||
|
let string_validation = wrap_string_validation(string_validation);
|
||||||
|
|
||||||
|
if array_validation.is_some()
|
||||||
|
|| number_validation.is_some()
|
||||||
|
|| object_validation.is_some()
|
||||||
|
|| string_validation.is_some()
|
||||||
|
|| format.is_some()
|
||||||
|
{
|
||||||
|
*schema_expr = quote! {
|
||||||
|
{
|
||||||
|
let mut schema = #schema_expr;
|
||||||
|
if let schemars::schema::Schema::Object(schema_object) = &mut schema
|
||||||
|
{
|
||||||
|
#array_validation
|
||||||
|
#number_validation
|
||||||
|
#object_validation
|
||||||
|
#string_validation
|
||||||
|
#format
|
||||||
|
}
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_lit_into_expr_path(
|
||||||
|
cx: &Ctxt,
|
||||||
|
attr_type: &'static str,
|
||||||
|
meta_item_name: &'static str,
|
||||||
|
lit: &syn::Lit,
|
||||||
|
) -> Result<Expr, ()> {
|
||||||
|
parse_lit_into_path(cx, attr_type, meta_item_name, lit).map(|path| {
|
||||||
|
Expr::Path(ExprPath {
|
||||||
|
attrs: Vec::new(),
|
||||||
|
qself: None,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_array_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
||||||
|
if v.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(quote! {
|
||||||
|
if schema_object.has_type(schemars::schema::InstanceType::Array) {
|
||||||
|
let validation = schema_object.array();
|
||||||
|
#(#v)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_number_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
||||||
|
if v.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(quote! {
|
||||||
|
if schema_object.has_type(schemars::schema::InstanceType::Integer)
|
||||||
|
|| schema_object.has_type(schemars::schema::InstanceType::Number) {
|
||||||
|
let validation = schema_object.number();
|
||||||
|
#(#v)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_object_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
||||||
|
if v.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(quote! {
|
||||||
|
if schema_object.has_type(schemars::schema::InstanceType::Object) {
|
||||||
|
let validation = schema_object.object();
|
||||||
|
#(#v)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_string_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
||||||
|
if v.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(quote! {
|
||||||
|
if schema_object.has_type(schemars::schema::InstanceType::String) {
|
||||||
|
let validation = schema_object.string();
|
||||||
|
#(#v)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option<Expr> {
|
||||||
|
match lit {
|
||||||
|
Lit::Str(s) => parse_lit_str::<ExprPath>(s).ok().map(Expr::Path),
|
||||||
|
Lit::Int(_) | Lit::Float(_) => Some(Expr::Lit(ExprLit {
|
||||||
|
attrs: Vec::new(),
|
||||||
|
lit: lit.clone(),
|
||||||
|
})),
|
||||||
|
_ => {
|
||||||
|
cx.error_spanned_by(
|
||||||
|
lit,
|
||||||
|
format!(
|
||||||
|
"expected `{}` to be a string or number literal",
|
||||||
|
meta_item_name
|
||||||
|
),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,13 +9,14 @@ extern crate proc_macro;
|
||||||
mod ast;
|
mod ast;
|
||||||
mod attr;
|
mod attr;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
|
mod regex_syntax;
|
||||||
mod schema_exprs;
|
mod schema_exprs;
|
||||||
|
|
||||||
use ast::*;
|
use ast::*;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
|
#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate))]
|
||||||
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||||
derive_json_schema(input, false)
|
derive_json_schema(input, false)
|
||||||
|
@ -50,7 +51,7 @@ fn derive_json_schema(
|
||||||
let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();
|
||||||
|
|
||||||
if let Some(transparent_field) = cont.transparent_field() {
|
if let Some(transparent_field) = cont.transparent_field() {
|
||||||
let (ty, type_def) = schema_exprs::type_for_field_schema(transparent_field, 0);
|
let (ty, type_def) = schema_exprs::type_for_field_schema(transparent_field);
|
||||||
return Ok(quote! {
|
return Ok(quote! {
|
||||||
const _: () = {
|
const _: () = {
|
||||||
#crate_alias
|
#crate_alias
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::attr;
|
use proc_macro2::TokenStream;
|
||||||
use attr::Attrs;
|
|
||||||
use proc_macro2::{Ident, Span, TokenStream};
|
|
||||||
use quote::{ToTokens, TokenStreamExt};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SchemaMetadata<'a> {
|
pub struct SchemaMetadata<'a> {
|
||||||
|
@ -14,42 +11,15 @@ pub struct SchemaMetadata<'a> {
|
||||||
pub default: Option<TokenStream>,
|
pub default: Option<TokenStream>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for SchemaMetadata<'_> {
|
impl<'a> SchemaMetadata<'a> {
|
||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
|
||||||
let setters = self.make_setters();
|
let setters = self.make_setters();
|
||||||
if setters.is_empty() {
|
if !setters.is_empty() {
|
||||||
tokens.append(Ident::new("None", Span::call_site()))
|
*schema_expr = quote! {
|
||||||
} else {
|
schemars::_private::apply_metadata(#schema_expr, schemars::schema::Metadata {
|
||||||
tokens.extend(quote! {
|
|
||||||
Some({
|
|
||||||
schemars::schema::Metadata {
|
|
||||||
#(#setters)*
|
#(#setters)*
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> SchemaMetadata<'a> {
|
|
||||||
pub fn from_attrs(attrs: &'a Attrs) -> Self {
|
|
||||||
SchemaMetadata {
|
|
||||||
title: attrs.title.as_ref().and_then(none_if_empty),
|
|
||||||
description: attrs.description.as_ref().and_then(none_if_empty),
|
|
||||||
deprecated: attrs.deprecated,
|
|
||||||
examples: &attrs.examples,
|
|
||||||
read_only: false,
|
|
||||||
write_only: false,
|
|
||||||
default: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_to_schema(&self, schema_expr: TokenStream) -> TokenStream {
|
|
||||||
quote! {
|
|
||||||
{
|
|
||||||
let schema = #schema_expr;
|
|
||||||
schemars::_private::apply_metadata(schema, #self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,12 +75,3 @@ impl<'a> SchemaMetadata<'a> {
|
||||||
setters
|
setters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::ptr_arg)]
|
|
||||||
fn none_if_empty(s: &String) -> Option<&str> {
|
|
||||||
if s.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
26
schemars_derive/src/regex_syntax.rs
Normal file
26
schemars_derive/src/regex_syntax.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function
|
||||||
|
// https://github.com/rust-lang/regex/blob/ff283badce21dcebd581909d38b81f2c8c9bfb54/regex-syntax/src/lib.rs
|
||||||
|
|
||||||
|
pub fn escape(text: &str) -> String {
|
||||||
|
let mut quoted = String::new();
|
||||||
|
escape_into(text, &mut quoted);
|
||||||
|
quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_into(text: &str, buf: &mut String) {
|
||||||
|
buf.reserve(text.len());
|
||||||
|
for c in text.chars() {
|
||||||
|
if is_meta_character(c) {
|
||||||
|
buf.push('\\');
|
||||||
|
}
|
||||||
|
buf.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_meta_character(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
'\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$'
|
||||||
|
| '#' | '&' | '-' | '~' => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault,
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
pub fn expr_for_container(cont: &Container) -> TokenStream {
|
pub fn expr_for_container(cont: &Container) -> TokenStream {
|
||||||
let schema_expr = match &cont.data {
|
let mut schema_expr = match &cont.data {
|
||||||
Data::Struct(Style::Unit, _) => expr_for_unit_struct(),
|
Data::Struct(Style::Unit, _) => expr_for_unit_struct(),
|
||||||
Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(&fields[0]),
|
Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(&fields[0]),
|
||||||
Data::Struct(Style::Tuple, fields) => expr_for_tuple_struct(fields),
|
Data::Struct(Style::Tuple, fields) => expr_for_tuple_struct(fields),
|
||||||
|
@ -17,8 +17,8 @@ pub fn expr_for_container(cont: &Container) -> TokenStream {
|
||||||
Data::Enum(variants) => expr_for_enum(variants, &cont.serde_attrs),
|
Data::Enum(variants) => expr_for_enum(variants, &cont.serde_attrs),
|
||||||
};
|
};
|
||||||
|
|
||||||
let doc_metadata = SchemaMetadata::from_attrs(&cont.attrs);
|
cont.attrs.as_metadata().apply_to_schema(&mut schema_expr);
|
||||||
doc_metadata.apply_to_schema(schema_expr)
|
schema_expr
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
|
pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
|
||||||
|
@ -47,49 +47,52 @@ pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
|
||||||
let enum_ident = &cont.ident;
|
let enum_ident = &cont.ident;
|
||||||
let variant_idents = variants.iter().map(|v| &v.ident);
|
let variant_idents = variants.iter().map(|v| &v.ident);
|
||||||
|
|
||||||
let schema_expr = schema_object(quote! {
|
let mut schema_expr = schema_object(quote! {
|
||||||
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
|
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
|
||||||
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
|
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
|
||||||
});
|
});
|
||||||
|
|
||||||
let doc_metadata = SchemaMetadata::from_attrs(&cont.attrs);
|
cont.attrs.as_metadata().apply_to_schema(&mut schema_expr);
|
||||||
Ok(doc_metadata.apply_to_schema(schema_expr))
|
Ok(schema_expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
|
fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
|
||||||
let (ty, type_def) = type_for_field_schema(field, 0);
|
let (ty, type_def) = type_for_field_schema(field);
|
||||||
let span = field.original.span();
|
let span = field.original.span();
|
||||||
let gen = quote!(gen);
|
let gen = quote!(gen);
|
||||||
|
|
||||||
if allow_ref {
|
let mut schema_expr = if field.validation_attrs.required() {
|
||||||
quote_spanned! {span=>
|
quote_spanned! {span=>
|
||||||
{
|
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen)
|
||||||
#type_def
|
|
||||||
#gen.subschema_for::<#ty>()
|
|
||||||
}
|
}
|
||||||
|
} else if allow_ref {
|
||||||
|
quote_spanned! {span=>
|
||||||
|
#gen.subschema_for::<#ty>()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quote_spanned! {span=>
|
quote_spanned! {span=>
|
||||||
{
|
|
||||||
#type_def
|
|
||||||
<#ty as schemars::JsonSchema>::json_schema(#gen)
|
<#ty as schemars::JsonSchema>::json_schema(#gen)
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
prepend_type_def(type_def, &mut schema_expr);
|
||||||
|
field.validation_attrs.apply_to_schema(&mut schema_expr);
|
||||||
|
|
||||||
|
schema_expr
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_for_field_schema(field: &Field, local_id: usize) -> (syn::Type, Option<TokenStream>) {
|
pub fn type_for_field_schema(field: &Field) -> (syn::Type, Option<TokenStream>) {
|
||||||
match &field.attrs.with {
|
match &field.attrs.with {
|
||||||
None => (field.ty.to_owned(), None),
|
None => (field.ty.to_owned(), None),
|
||||||
Some(with_attr) => type_for_schema(with_attr, local_id),
|
Some(with_attr) => type_for_schema(with_attr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn type_for_schema(with_attr: &WithAttr, local_id: usize) -> (syn::Type, Option<TokenStream>) {
|
fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option<TokenStream>) {
|
||||||
match with_attr {
|
match with_attr {
|
||||||
WithAttr::Type(ty) => (ty.to_owned(), None),
|
WithAttr::Type(ty) => (ty.to_owned(), None),
|
||||||
WithAttr::Function(fun) => {
|
WithAttr::Function(fun) => {
|
||||||
let ty_name = format_ident!("_SchemarsSchemaWithFunction{}", local_id);
|
let ty_name = syn::Ident::new("_SchemarsSchemaWithFunction", Span::call_site());
|
||||||
let fn_name = fun.segments.last().unwrap().ident.to_string();
|
let fn_name = fun.segments.last().unwrap().ident.to_string();
|
||||||
|
|
||||||
let type_def = quote_spanned! {fun.span()=>
|
let type_def = quote_spanned! {fun.span()=>
|
||||||
|
@ -159,7 +162,7 @@ fn expr_for_external_tagged_enum<'a>(
|
||||||
let name = variant.name();
|
let name = variant.name();
|
||||||
let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
|
let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
|
||||||
|
|
||||||
let schema_expr = schema_object(quote! {
|
let mut schema_expr = schema_object(quote! {
|
||||||
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
||||||
object: Some(Box::new(schemars::schema::ObjectValidation {
|
object: Some(Box::new(schemars::schema::ObjectValidation {
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -176,8 +179,13 @@ fn expr_for_external_tagged_enum<'a>(
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
let doc_metadata = SchemaMetadata::from_attrs(&variant.attrs);
|
|
||||||
doc_metadata.apply_to_schema(schema_expr)
|
variant
|
||||||
|
.attrs
|
||||||
|
.as_metadata()
|
||||||
|
.apply_to_schema(&mut schema_expr);
|
||||||
|
|
||||||
|
schema_expr
|
||||||
}));
|
}));
|
||||||
|
|
||||||
schema_object(quote! {
|
schema_object(quote! {
|
||||||
|
@ -200,7 +208,7 @@ fn expr_for_internal_tagged_enum<'a>(
|
||||||
enum_values: Some(vec![#name.into()]),
|
enum_values: Some(vec![#name.into()]),
|
||||||
});
|
});
|
||||||
|
|
||||||
let tag_schema = schema_object(quote! {
|
let mut tag_schema = schema_object(quote! {
|
||||||
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
||||||
object: Some(Box::new(schemars::schema::ObjectValidation {
|
object: Some(Box::new(schemars::schema::ObjectValidation {
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -216,15 +224,16 @@ fn expr_for_internal_tagged_enum<'a>(
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
let doc_metadata = SchemaMetadata::from_attrs(&variant.attrs);
|
|
||||||
let tag_schema = doc_metadata.apply_to_schema(tag_schema);
|
|
||||||
|
|
||||||
match expr_for_untagged_enum_variant_for_flatten(&variant, deny_unknown_fields) {
|
variant.attrs.as_metadata().apply_to_schema(&mut tag_schema);
|
||||||
Some(variant_schema) => quote! {
|
|
||||||
#tag_schema.flatten(#variant_schema)
|
if let Some(variant_schema) =
|
||||||
},
|
expr_for_untagged_enum_variant_for_flatten(&variant, deny_unknown_fields)
|
||||||
None => tag_schema,
|
{
|
||||||
|
tag_schema.extend(quote!(.flatten(#variant_schema)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tag_schema
|
||||||
});
|
});
|
||||||
|
|
||||||
schema_object(quote! {
|
schema_object(quote! {
|
||||||
|
@ -240,9 +249,14 @@ fn expr_for_untagged_enum<'a>(
|
||||||
deny_unknown_fields: bool,
|
deny_unknown_fields: bool,
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
let schemas = variants.map(|variant| {
|
let schemas = variants.map(|variant| {
|
||||||
let schema_expr = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
|
let mut schema_expr = expr_for_untagged_enum_variant(variant, deny_unknown_fields);
|
||||||
let doc_metadata = SchemaMetadata::from_attrs(&variant.attrs);
|
|
||||||
doc_metadata.apply_to_schema(schema_expr)
|
variant
|
||||||
|
.attrs
|
||||||
|
.as_metadata()
|
||||||
|
.apply_to_schema(&mut schema_expr);
|
||||||
|
|
||||||
|
schema_expr
|
||||||
});
|
});
|
||||||
|
|
||||||
schema_object(quote! {
|
schema_object(quote! {
|
||||||
|
@ -289,7 +303,7 @@ fn expr_for_adjacent_tagged_enum<'a>(
|
||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let outer_schema = schema_object(quote! {
|
let mut outer_schema = schema_object(quote! {
|
||||||
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
||||||
object: Some(Box::new(schemars::schema::ObjectValidation {
|
object: Some(Box::new(schemars::schema::ObjectValidation {
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -309,8 +323,12 @@ fn expr_for_adjacent_tagged_enum<'a>(
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
let doc_metadata = SchemaMetadata::from_attrs(&variant.attrs);
|
variant
|
||||||
doc_metadata.apply_to_schema(outer_schema)
|
.attrs
|
||||||
|
.as_metadata()
|
||||||
|
.apply_to_schema(&mut outer_schema);
|
||||||
|
|
||||||
|
outer_schema
|
||||||
});
|
});
|
||||||
|
|
||||||
schema_object(quote! {
|
schema_object(quote! {
|
||||||
|
@ -323,14 +341,14 @@ fn expr_for_adjacent_tagged_enum<'a>(
|
||||||
|
|
||||||
fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream {
|
fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream {
|
||||||
if let Some(with_attr) = &variant.attrs.with {
|
if let Some(with_attr) = &variant.attrs.with {
|
||||||
let (ty, type_def) = type_for_schema(with_attr, 0);
|
let (ty, type_def) = type_for_schema(with_attr);
|
||||||
let gen = quote!(gen);
|
let gen = quote!(gen);
|
||||||
return quote_spanned! {variant.original.span()=>
|
let mut schema_expr = quote_spanned! {variant.original.span()=>
|
||||||
{
|
|
||||||
#type_def
|
|
||||||
#gen.subschema_for::<#ty>()
|
#gen.subschema_for::<#ty>()
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
prepend_type_def(type_def, &mut schema_expr);
|
||||||
|
return schema_expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
match variant.style {
|
match variant.style {
|
||||||
|
@ -346,14 +364,14 @@ fn expr_for_untagged_enum_variant_for_flatten(
|
||||||
deny_unknown_fields: bool,
|
deny_unknown_fields: bool,
|
||||||
) -> Option<TokenStream> {
|
) -> Option<TokenStream> {
|
||||||
if let Some(with_attr) = &variant.attrs.with {
|
if let Some(with_attr) = &variant.attrs.with {
|
||||||
let (ty, type_def) = type_for_schema(with_attr, 0);
|
let (ty, type_def) = type_for_schema(with_attr);
|
||||||
let gen = quote!(gen);
|
let gen = quote!(gen);
|
||||||
return Some(quote_spanned! {variant.original.span()=>
|
let mut schema_expr = quote_spanned! {variant.original.span()=>
|
||||||
{
|
|
||||||
#type_def
|
|
||||||
<#ty as schemars::JsonSchema>::json_schema(#gen)
|
<#ty as schemars::JsonSchema>::json_schema(#gen)
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
prepend_type_def(type_def, &mut schema_expr);
|
||||||
|
return Some(schema_expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(match variant.style {
|
Some(match variant.style {
|
||||||
|
@ -375,17 +393,25 @@ fn expr_for_newtype_struct(field: &Field) -> TokenStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
|
fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
|
||||||
let (types, type_defs): (Vec<_>, Vec<_>) = fields
|
let fields: Vec<_> = fields
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|f| !f.serde_attrs.skip_deserializing())
|
.filter(|f| !f.serde_attrs.skip_deserializing())
|
||||||
.enumerate()
|
.map(|f| expr_for_field(f, true))
|
||||||
.map(|(i, f)| type_for_field_schema(f, i))
|
.collect();
|
||||||
.unzip();
|
let len = fields.len() as u32;
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
{
|
schemars::schema::Schema::Object(
|
||||||
#(#type_defs)*
|
schemars::schema::SchemaObject {
|
||||||
gen.subschema_for::<(#(#types),*)>()
|
instance_type: Some(schemars::schema::InstanceType::Array.into()),
|
||||||
}
|
array: Some(Box::new(schemars::schema::ArrayValidation {
|
||||||
|
items: Some(vec![#(#fields),*].into()),
|
||||||
|
max_items: Some(#len),
|
||||||
|
min_items: Some(#len),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,35 +431,55 @@ fn expr_for_struct(
|
||||||
SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)),
|
SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut type_defs = Vec::new();
|
|
||||||
|
|
||||||
let properties: Vec<_> = property_fields
|
let properties: Vec<_> = property_fields
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|field| {
|
.map(|field| {
|
||||||
let name = field.name();
|
let name = field.name();
|
||||||
let default = field_default_expr(field, set_container_default.is_some());
|
let default = field_default_expr(field, set_container_default.is_some());
|
||||||
|
|
||||||
let required = match default {
|
let (ty, type_def) = type_for_field_schema(field);
|
||||||
Some(_) => quote!(false),
|
|
||||||
None => quote!(true),
|
let maybe_insert_required = match (&default, field.validation_attrs.required()) {
|
||||||
|
(Some(_), _) => TokenStream::new(),
|
||||||
|
(None, false) => {
|
||||||
|
quote! {
|
||||||
|
if !<#ty as schemars::JsonSchema>::_schemars_private_is_option() {
|
||||||
|
object_validation.required.insert(#name.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, true) => quote! {
|
||||||
|
object_validation.required.insert(#name.to_owned());
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let metadata = &SchemaMetadata {
|
let metadata = SchemaMetadata {
|
||||||
read_only: field.serde_attrs.skip_deserializing(),
|
read_only: field.serde_attrs.skip_deserializing(),
|
||||||
write_only: field.serde_attrs.skip_serializing(),
|
write_only: field.serde_attrs.skip_serializing(),
|
||||||
default,
|
default,
|
||||||
..SchemaMetadata::from_attrs(&field.attrs)
|
..field.attrs.as_metadata()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (ty, type_def) = type_for_field_schema(field, type_defs.len());
|
let gen = quote!(gen);
|
||||||
if let Some(type_def) = type_def {
|
let mut schema_expr = if field.validation_attrs.required() {
|
||||||
type_defs.push(type_def);
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = quote!(gen, &mut schema_object, #name.to_owned(), #metadata, #required);
|
|
||||||
|
|
||||||
quote_spanned! {ty.span()=>
|
quote_spanned! {ty.span()=>
|
||||||
schemars::_private::add_schema_as_property::<#ty>(#args);
|
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote_spanned! {ty.span()=>
|
||||||
|
#gen.subschema_for::<#ty>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata.apply_to_schema(&mut schema_expr);
|
||||||
|
field.validation_attrs.apply_to_schema(&mut schema_expr);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
{
|
||||||
|
#type_def
|
||||||
|
object_validation.properties.insert(#name.to_owned(), #schema_expr);
|
||||||
|
#maybe_insert_required
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -441,37 +487,39 @@ fn expr_for_struct(
|
||||||
let flattens: Vec<_> = flattened_fields
|
let flattens: Vec<_> = flattened_fields
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|field| {
|
.map(|field| {
|
||||||
let (ty, type_def) = type_for_field_schema(field, type_defs.len());
|
let (ty, type_def) = type_for_field_schema(field);
|
||||||
if let Some(type_def) = type_def {
|
|
||||||
type_defs.push(type_def);
|
|
||||||
}
|
|
||||||
|
|
||||||
let gen = quote!(gen);
|
let required = field.validation_attrs.required();
|
||||||
quote_spanned! {ty.span()=>
|
|
||||||
.flatten(schemars::_private::json_schema_for_flatten::<#ty>(#gen))
|
let args = quote!(gen, #required);
|
||||||
}
|
let mut schema_expr = quote_spanned! {ty.span()=>
|
||||||
|
schemars::_private::json_schema_for_flatten::<#ty>(#args)
|
||||||
|
};
|
||||||
|
|
||||||
|
prepend_type_def(type_def, &mut schema_expr);
|
||||||
|
schema_expr
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let set_additional_properties = if deny_unknown_fields {
|
let set_additional_properties = if deny_unknown_fields {
|
||||||
quote! {
|
quote! {
|
||||||
schema_object.object().additional_properties = Some(Box::new(false.into()));
|
object_validation.additional_properties = Some(Box::new(false.into()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
TokenStream::new()
|
TokenStream::new()
|
||||||
};
|
};
|
||||||
quote! {
|
quote! {
|
||||||
{
|
{
|
||||||
#(#type_defs)*
|
|
||||||
#set_container_default
|
#set_container_default
|
||||||
let mut schema_object = schemars::schema::SchemaObject {
|
let mut schema_object = schemars::schema::SchemaObject {
|
||||||
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
instance_type: Some(schemars::schema::InstanceType::Object.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
let object_validation = schema_object.object();
|
||||||
#set_additional_properties
|
#set_additional_properties
|
||||||
#(#properties)*
|
#(#properties)*
|
||||||
schemars::schema::Schema::Object(schema_object)
|
schemars::schema::Schema::Object(schema_object)
|
||||||
#(#flattens)*
|
#(.flatten(#flattens))*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -540,3 +588,14 @@ fn schema_object(properties: TokenStream) -> TokenStream {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepend_type_def(type_def: Option<TokenStream>, schema_expr: &mut TokenStream) {
|
||||||
|
if let Some(type_def) = type_def {
|
||||||
|
*schema_expr = quote! {
|
||||||
|
{
|
||||||
|
#type_def
|
||||||
|
#schema_expr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue