Add #[schemars(inner(...)] attribute to specify schema for array items (#234)

This commit is contained in:
Jakub Jirutka 2023-09-09 14:35:53 +02:00 committed by GitHub
parent 30e513ac14
commit a5e51b22b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 231 additions and 60 deletions

View file

@ -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 = "..."]`)

View file

@ -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)]

View file

@ -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,

View file

@ -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)]

View file

@ -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,

View 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"
}
}
}
}

View file

@ -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",

View file

@ -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)]

View 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")
}

View file

@ -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> {
if !attr.path.is_ident(attr_type) { attrs.iter().fold(vec![], |mut acc, attr| {
return Ok(Vec::new()); if !attr.path.is_ident(attr_type) {
} 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) => { acc
if !ignore_errors { })
errors.error_spanned_by(attr, err)
}
Err(())
}
}
} }
fn get_lit_str<'a>( fn get_lit_str<'a>(

View file

@ -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; #array_validation
if let schemars::schema::Schema::Object(schema_object) = &mut schema #number_validation
{ #object_validation
#array_validation #string_validation
#number_validation #format
#object_validation #inner_validation
#string_validation
#format
}
schema
} }
} })
} else {
None
} }
} }
} }