Add #[schemars(inner(...)] attribute to specify schema for array items (#234)
This commit is contained in:
parent
30e513ac14
commit
a5e51b22b3
11 changed files with 231 additions and 60 deletions
|
@ -276,6 +276,20 @@ Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rus
|
||||||
|
|
||||||
Set the path to the schemars crate instance the generated code should depend on. This is mostly useful for other crates that depend on schemars in their macros.
|
Set the path to the schemars crate instance the generated code should depend on. This is mostly useful for other crates that depend on schemars in their macros.
|
||||||
|
|
||||||
|
<h3 id="inner">
|
||||||
|
|
||||||
|
`#[schemars(inner(...))]`
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
Sets properties specified by [validator attributes](#supported-validator-attributes) on items of an array schema. For example:
|
||||||
|
|
||||||
|
```rs
|
||||||
|
struct Struct {
|
||||||
|
#[schemars(inner(url, regex(pattern = "^https://")))]
|
||||||
|
urls: Vec<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
<h3 id="doc">
|
<h3 id="doc">
|
||||||
|
|
||||||
Doc Comments (`#[doc = "..."]`)
|
Doc Comments (`#[doc = "..."]`)
|
||||||
|
|
|
@ -10,6 +10,8 @@ pub struct MyStruct {
|
||||||
pub my_bool: bool,
|
pub my_bool: bool,
|
||||||
#[schemars(default)]
|
#[schemars(default)]
|
||||||
pub my_nullable_enum: Option<MyEnum>,
|
pub my_nullable_enum: Option<MyEnum>,
|
||||||
|
#[schemars(inner(regex(pattern = "^x$")))]
|
||||||
|
pub my_vec_str: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"myBool",
|
"myBool",
|
||||||
"myNumber"
|
"myNumber",
|
||||||
|
"myVecStr"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"myBool": {
|
"myBool": {
|
||||||
|
@ -26,6 +27,13 @@
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"maximum": 10.0,
|
"maximum": 10.0,
|
||||||
"minimum": 1.0
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"myVecStr": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^x$"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|
|
@ -10,6 +10,8 @@ pub struct MyStruct {
|
||||||
pub my_bool: bool,
|
pub my_bool: bool,
|
||||||
#[schemars(default)]
|
#[schemars(default)]
|
||||||
pub my_nullable_enum: Option<MyEnum>,
|
pub my_nullable_enum: Option<MyEnum>,
|
||||||
|
#[schemars(inner(regex(pattern = "^x$")))]
|
||||||
|
pub my_vec_str: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"myBool",
|
"myBool",
|
||||||
"myNumber"
|
"myNumber",
|
||||||
|
"myVecStr"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"myBool": {
|
"myBool": {
|
||||||
|
@ -26,6 +27,13 @@
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"maximum": 10.0,
|
"maximum": 10.0,
|
||||||
"minimum": 1.0
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"myVecStr": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^x$"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|
74
schemars/tests/expected/validate_inner.json
Normal file
74
schemars/tests/expected/validate_inner.json
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Struct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"array_str_length",
|
||||||
|
"slice_str_contains",
|
||||||
|
"vec_i32_range",
|
||||||
|
"vec_str_length",
|
||||||
|
"vec_str_length2",
|
||||||
|
"vec_str_regex",
|
||||||
|
"vec_str_url"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"array_str_length": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 5
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"slice_str_contains": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vec_i32_range": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": -10.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vec_str_length": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vec_str_length2": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"maxItems": 3,
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"vec_str_regex": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vec_str_url": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "Struct",
|
"title": "Struct2",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"contains_str1",
|
"contains_str1",
|
||||||
|
|
|
@ -101,7 +101,7 @@ pub struct Struct2 {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_schemars_attrs() -> TestResult {
|
fn validate_schemars_attrs() -> TestResult {
|
||||||
test_default_generated_schema::<Struct>("validate_schemars_attrs")
|
test_default_generated_schema::<Struct2>("validate_schemars_attrs")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(JsonSchema)]
|
#[derive(JsonSchema)]
|
||||||
|
|
31
schemars/tests/validate_inner.rs
Normal file
31
schemars/tests/validate_inner.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
// In real code, this would typically be a Regex, potentially created in a `lazy_static!`.
|
||||||
|
static STARTS_WITH_HELLO: &str = r"^[Hh]ello\b";
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct<'a> {
|
||||||
|
#[schemars(inner(length(min = 5, max = 100)))]
|
||||||
|
array_str_length: [&'a str; 2],
|
||||||
|
#[schemars(inner(contains(pattern = "substring...")))]
|
||||||
|
slice_str_contains: &'a[&'a str],
|
||||||
|
#[schemars(inner(regex = "STARTS_WITH_HELLO"))]
|
||||||
|
vec_str_regex: Vec<String>,
|
||||||
|
#[schemars(inner(length(min = 1, max = 100)))]
|
||||||
|
vec_str_length: Vec<&'a str>,
|
||||||
|
#[schemars(length(min = 1, max = 3), inner(length(min = 1, max = 100)))]
|
||||||
|
vec_str_length2: Vec<String>,
|
||||||
|
#[schemars(inner(url))]
|
||||||
|
vec_str_url: Vec<String>,
|
||||||
|
#[schemars(inner(range(min = -10, max = 10)))]
|
||||||
|
vec_i32_range: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_inner() -> TestResult {
|
||||||
|
test_default_generated_schema::<Struct>("validate_inner")
|
||||||
|
}
|
|
@ -103,11 +103,7 @@ impl Attrs {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for meta_item in attrs
|
for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) {
|
||||||
.iter()
|
|
||||||
.flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors))
|
|
||||||
.flatten()
|
|
||||||
{
|
|
||||||
match &meta_item {
|
match &meta_item {
|
||||||
Meta(NameValue(m)) if m.path.is_ident("with") => {
|
Meta(NameValue(m)) if m.path.is_ident("with") => {
|
||||||
if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) {
|
if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) {
|
||||||
|
@ -167,6 +163,12 @@ impl Attrs {
|
||||||
|
|
||||||
_ if ignore_errors => {}
|
_ if ignore_errors => {}
|
||||||
|
|
||||||
|
Meta(List(m)) if m.path.is_ident("inner") && attr_type == "schemars" => {
|
||||||
|
// This will be processed with the validation attributes.
|
||||||
|
// It's allowed only for the schemars attribute because the
|
||||||
|
// validator crate doesn't support it yet.
|
||||||
|
}
|
||||||
|
|
||||||
Meta(meta_item) => {
|
Meta(meta_item) => {
|
||||||
if !is_known_serde_or_validation_keyword(meta_item) {
|
if !is_known_serde_or_validation_keyword(meta_item) {
|
||||||
let path = meta_item
|
let path = meta_item
|
||||||
|
@ -214,30 +216,25 @@ fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_meta_items(
|
fn get_meta_items(
|
||||||
attr: &syn::Attribute,
|
attrs: &[syn::Attribute],
|
||||||
attr_type: &'static str,
|
attr_type: &'static str,
|
||||||
errors: &Ctxt,
|
errors: &Ctxt,
|
||||||
ignore_errors: bool,
|
ignore_errors: bool,
|
||||||
) -> Result<Vec<syn::NestedMeta>, ()> {
|
) -> Vec<syn::NestedMeta> {
|
||||||
|
attrs.iter().fold(vec![], |mut acc, attr| {
|
||||||
if !attr.path.is_ident(attr_type) {
|
if !attr.path.is_ident(attr_type) {
|
||||||
return Ok(Vec::new());
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
match attr.parse_meta() {
|
match attr.parse_meta() {
|
||||||
Ok(List(meta)) => Ok(meta.nested.into_iter().collect()),
|
Ok(List(meta)) => acc.extend(meta.nested),
|
||||||
Ok(other) => {
|
Ok(other) if !ignore_errors => {
|
||||||
if !ignore_errors {
|
|
||||||
errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type))
|
errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type))
|
||||||
}
|
}
|
||||||
Err(())
|
Err(err) if !ignore_errors => errors.error_spanned_by(attr, err),
|
||||||
}
|
_ => (),
|
||||||
Err(err) => {
|
|
||||||
if !ignore_errors {
|
|
||||||
errors.error_spanned_by(attr, err)
|
|
||||||
}
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
acc
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_lit_str<'a>(
|
fn get_lit_str<'a>(
|
||||||
|
|
|
@ -43,13 +43,17 @@ pub struct ValidationAttrs {
|
||||||
contains: Option<String>,
|
contains: Option<String>,
|
||||||
required: bool,
|
required: bool,
|
||||||
format: Option<Format>,
|
format: Option<Format>,
|
||||||
|
inner: Option<Box<ValidationAttrs>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidationAttrs {
|
impl ValidationAttrs {
|
||||||
pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
|
pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
|
||||||
|
let schemars_items = get_meta_items(attrs, "schemars", errors, false);
|
||||||
|
let validate_items = get_meta_items(attrs, "validate", errors, true);
|
||||||
|
|
||||||
ValidationAttrs::default()
|
ValidationAttrs::default()
|
||||||
.populate(attrs, "schemars", false, errors)
|
.populate(schemars_items, "schemars", false, errors)
|
||||||
.populate(attrs, "validate", true, errors)
|
.populate(validate_items, "validate", true, errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn required(&self) -> bool {
|
pub fn required(&self) -> bool {
|
||||||
|
@ -58,7 +62,7 @@ impl ValidationAttrs {
|
||||||
|
|
||||||
fn populate(
|
fn populate(
|
||||||
mut self,
|
mut self,
|
||||||
attrs: &[syn::Attribute],
|
meta_items: Vec<syn::NestedMeta>,
|
||||||
attr_type: &'static str,
|
attr_type: &'static str,
|
||||||
ignore_errors: bool,
|
ignore_errors: bool,
|
||||||
errors: &Ctxt,
|
errors: &Ctxt,
|
||||||
|
@ -97,11 +101,7 @@ impl ValidationAttrs {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for meta_item in attrs
|
for meta_item in meta_items {
|
||||||
.iter()
|
|
||||||
.flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors))
|
|
||||||
.flatten()
|
|
||||||
{
|
|
||||||
match &meta_item {
|
match &meta_item {
|
||||||
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => {
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => {
|
||||||
for nested in meta_list.nested.iter() {
|
for nested in meta_list.nested.iter() {
|
||||||
|
@ -247,7 +247,8 @@ impl ValidationAttrs {
|
||||||
if !ignore_errors {
|
if !ignore_errors {
|
||||||
errors.error_spanned_by(
|
errors.error_spanned_by(
|
||||||
meta,
|
meta,
|
||||||
"unknown item in schemars regex attribute".to_string(),
|
"unknown item in schemars regex attribute"
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -292,7 +293,8 @@ impl ValidationAttrs {
|
||||||
if !ignore_errors {
|
if !ignore_errors {
|
||||||
errors.error_spanned_by(
|
errors.error_spanned_by(
|
||||||
meta,
|
meta,
|
||||||
"unknown item in schemars contains attribute".to_string(),
|
"unknown item in schemars contains attribute"
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,6 +304,21 @@ impl ValidationAttrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("inner") => {
|
||||||
|
match self.inner {
|
||||||
|
Some(_) => duplicate_error(&meta_list.path),
|
||||||
|
None => {
|
||||||
|
let inner_attrs = ValidationAttrs::default().populate(
|
||||||
|
meta_list.nested.clone().into_iter().collect(),
|
||||||
|
attr_type,
|
||||||
|
ignore_errors,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
self.inner = Some(Box::new(inner_attrs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,16 +326,24 @@ impl ValidationAttrs {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
|
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
|
||||||
|
if let Some(apply_expr) = self.apply_to_schema_expr() {
|
||||||
|
*schema_expr = quote! {
|
||||||
|
{
|
||||||
|
let mut schema = #schema_expr;
|
||||||
|
#apply_expr
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_to_schema_expr(&self) -> Option<TokenStream> {
|
||||||
let mut array_validation = Vec::new();
|
let mut array_validation = Vec::new();
|
||||||
let mut number_validation = Vec::new();
|
let mut number_validation = Vec::new();
|
||||||
let mut object_validation = Vec::new();
|
let mut object_validation = Vec::new();
|
||||||
let mut string_validation = Vec::new();
|
let mut string_validation = Vec::new();
|
||||||
|
|
||||||
if let Some(length_min) = self
|
if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) {
|
||||||
.length_min
|
|
||||||
.as_ref()
|
|
||||||
.or(self.length_equal.as_ref())
|
|
||||||
{
|
|
||||||
string_validation.push(quote! {
|
string_validation.push(quote! {
|
||||||
validation.min_length = Some(#length_min as u32);
|
validation.min_length = Some(#length_min as u32);
|
||||||
});
|
});
|
||||||
|
@ -327,11 +352,7 @@ impl ValidationAttrs {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(length_max) = self
|
if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) {
|
||||||
.length_max
|
|
||||||
.as_ref()
|
|
||||||
.or(self.length_equal.as_ref())
|
|
||||||
{
|
|
||||||
string_validation.push(quote! {
|
string_validation.push(quote! {
|
||||||
validation.max_length = Some(#length_max as u32);
|
validation.max_length = Some(#length_max as u32);
|
||||||
});
|
});
|
||||||
|
@ -378,6 +399,21 @@ impl ValidationAttrs {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let inner_validation = self
|
||||||
|
.inner
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|inner| inner.apply_to_schema_expr())
|
||||||
|
.map(|apply_expr| {
|
||||||
|
quote! {
|
||||||
|
if schema_object.has_type(schemars::schema::InstanceType::Array) {
|
||||||
|
if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items {
|
||||||
|
let mut schema = &mut **inner_schema;
|
||||||
|
#apply_expr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let array_validation = wrap_array_validation(array_validation);
|
let array_validation = wrap_array_validation(array_validation);
|
||||||
let number_validation = wrap_number_validation(number_validation);
|
let number_validation = wrap_number_validation(number_validation);
|
||||||
let object_validation = wrap_object_validation(object_validation);
|
let object_validation = wrap_object_validation(object_validation);
|
||||||
|
@ -388,21 +424,20 @@ impl ValidationAttrs {
|
||||||
|| object_validation.is_some()
|
|| object_validation.is_some()
|
||||||
|| string_validation.is_some()
|
|| string_validation.is_some()
|
||||||
|| format.is_some()
|
|| format.is_some()
|
||||||
|
|| inner_validation.is_some()
|
||||||
{
|
{
|
||||||
*schema_expr = quote! {
|
Some(quote! {
|
||||||
{
|
if let schemars::schema::Schema::Object(schema_object) = &mut schema {
|
||||||
let mut schema = #schema_expr;
|
|
||||||
if let schemars::schema::Schema::Object(schema_object) = &mut schema
|
|
||||||
{
|
|
||||||
#array_validation
|
#array_validation
|
||||||
#number_validation
|
#number_validation
|
||||||
#object_validation
|
#object_validation
|
||||||
#string_validation
|
#string_validation
|
||||||
#format
|
#format
|
||||||
|
#inner_validation
|
||||||
}
|
}
|
||||||
schema
|
})
|
||||||
}
|
} else {
|
||||||
}
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue