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

View file

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

View file

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

View file

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

View file

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

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#",
"title": "Struct",
"title": "Struct2",
"type": "object",
"required": [
"contains_str1",

View file

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

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
.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>(

View file

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