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.
|
||||
|
||||
<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">
|
||||
|
||||
Doc Comments (`#[doc = "..."]`)
|
||||
|
|
|
@ -10,6 +10,8 @@ pub struct MyStruct {
|
|||
pub my_bool: bool,
|
||||
#[schemars(default)]
|
||||
pub my_nullable_enum: Option<MyEnum>,
|
||||
#[schemars(inner(regex(pattern = "^x$")))]
|
||||
pub my_vec_str: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"myBool",
|
||||
"myNumber"
|
||||
"myNumber",
|
||||
"myVecStr"
|
||||
],
|
||||
"properties": {
|
||||
"myBool": {
|
||||
|
@ -26,6 +27,13 @@
|
|||
"format": "int32",
|
||||
"maximum": 10.0,
|
||||
"minimum": 1.0
|
||||
},
|
||||
"myVecStr": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^x$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
|
@ -10,6 +10,8 @@ pub struct MyStruct {
|
|||
pub my_bool: bool,
|
||||
#[schemars(default)]
|
||||
pub my_nullable_enum: Option<MyEnum>,
|
||||
#[schemars(inner(regex(pattern = "^x$")))]
|
||||
pub my_vec_str: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"myBool",
|
||||
"myNumber"
|
||||
"myNumber",
|
||||
"myVecStr"
|
||||
],
|
||||
"properties": {
|
||||
"myBool": {
|
||||
|
@ -26,6 +27,13 @@
|
|||
"format": "int32",
|
||||
"maximum": 10.0,
|
||||
"minimum": 1.0
|
||||
},
|
||||
"myVecStr": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^x$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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#",
|
||||
"title": "Struct",
|
||||
"title": "Struct2",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contains_str1",
|
||||
|
|
|
@ -101,7 +101,7 @@ pub struct Struct2 {
|
|||
|
||||
#[test]
|
||||
fn validate_schemars_attrs() -> TestResult {
|
||||
test_default_generated_schema::<Struct>("validate_schemars_attrs")
|
||||
test_default_generated_schema::<Struct2>("validate_schemars_attrs")
|
||||
}
|
||||
|
||||
#[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
|
||||
.iter()
|
||||
.flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors))
|
||||
.flatten()
|
||||
{
|
||||
for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) {
|
||||
match &meta_item {
|
||||
Meta(NameValue(m)) if m.path.is_ident("with") => {
|
||||
if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) {
|
||||
|
@ -167,6 +163,12 @@ impl Attrs {
|
|||
|
||||
_ 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) => {
|
||||
if !is_known_serde_or_validation_keyword(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(
|
||||
attr: &syn::Attribute,
|
||||
attrs: &[syn::Attribute],
|
||||
attr_type: &'static str,
|
||||
errors: &Ctxt,
|
||||
ignore_errors: bool,
|
||||
) -> Result<Vec<syn::NestedMeta>, ()> {
|
||||
if !attr.path.is_ident(attr_type) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
match attr.parse_meta() {
|
||||
Ok(List(meta)) => Ok(meta.nested.into_iter().collect()),
|
||||
Ok(other) => {
|
||||
if !ignore_errors {
|
||||
) -> Vec<syn::NestedMeta> {
|
||||
attrs.iter().fold(vec![], |mut acc, attr| {
|
||||
if !attr.path.is_ident(attr_type) {
|
||||
return acc;
|
||||
}
|
||||
match attr.parse_meta() {
|
||||
Ok(List(meta)) => acc.extend(meta.nested),
|
||||
Ok(other) if !ignore_errors => {
|
||||
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>(
|
||||
|
|
|
@ -43,13 +43,17 @@ pub struct ValidationAttrs {
|
|||
contains: Option<String>,
|
||||
required: bool,
|
||||
format: Option<Format>,
|
||||
inner: Option<Box<ValidationAttrs>>,
|
||||
}
|
||||
|
||||
impl ValidationAttrs {
|
||||
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()
|
||||
.populate(attrs, "schemars", false, errors)
|
||||
.populate(attrs, "validate", true, errors)
|
||||
.populate(schemars_items, "schemars", false, errors)
|
||||
.populate(validate_items, "validate", true, errors)
|
||||
}
|
||||
|
||||
pub fn required(&self) -> bool {
|
||||
|
@ -58,7 +62,7 @@ impl ValidationAttrs {
|
|||
|
||||
fn populate(
|
||||
mut self,
|
||||
attrs: &[syn::Attribute],
|
||||
meta_items: Vec<syn::NestedMeta>,
|
||||
attr_type: &'static str,
|
||||
ignore_errors: bool,
|
||||
errors: &Ctxt,
|
||||
|
@ -97,11 +101,7 @@ impl ValidationAttrs {
|
|||
}
|
||||
};
|
||||
|
||||
for meta_item in attrs
|
||||
.iter()
|
||||
.flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors))
|
||||
.flatten()
|
||||
{
|
||||
for meta_item in meta_items {
|
||||
match &meta_item {
|
||||
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => {
|
||||
for nested in meta_list.nested.iter() {
|
||||
|
@ -247,7 +247,8 @@ impl ValidationAttrs {
|
|||
if !ignore_errors {
|
||||
errors.error_spanned_by(
|
||||
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 {
|
||||
errors.error_spanned_by(
|
||||
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) {
|
||||
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 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(self.length_equal.as_ref())
|
||||
{
|
||||
if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) {
|
||||
string_validation.push(quote! {
|
||||
validation.min_length = Some(#length_min as u32);
|
||||
});
|
||||
|
@ -327,11 +352,7 @@ impl ValidationAttrs {
|
|||
});
|
||||
}
|
||||
|
||||
if let Some(length_max) = self
|
||||
.length_max
|
||||
.as_ref()
|
||||
.or(self.length_equal.as_ref())
|
||||
{
|
||||
if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) {
|
||||
string_validation.push(quote! {
|
||||
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 number_validation = wrap_number_validation(number_validation);
|
||||
let object_validation = wrap_object_validation(object_validation);
|
||||
|
@ -388,21 +424,20 @@ impl ValidationAttrs {
|
|||
|| object_validation.is_some()
|
||||
|| string_validation.is_some()
|
||||
|| format.is_some()
|
||||
|| inner_validation.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
|
||||
Some(quote! {
|
||||
if let schemars::schema::Schema::Object(schema_object) = &mut schema {
|
||||
#array_validation
|
||||
#number_validation
|
||||
#object_validation
|
||||
#string_validation
|
||||
#format
|
||||
#inner_validation
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue