Skip to content

Commit

Permalink
Rewrite attribute handling code (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
GREsau authored Aug 27, 2024
1 parent fb6bd6d commit d07a1be
Show file tree
Hide file tree
Showing 33 changed files with 1,173 additions and 1,077 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

- Allow `regex(path = ...)` value to be a non-string expression (https://github.com/GREsau/schemars/issues/302 / https://github.com/GREsau/schemars/pull/328)

### Changed (_⚠️ possibly-breaking changes ⚠️_)

- Invalid attributes that were previously silently ignored (e.g. setting `schema_with` on structs) will now cause compile errors
- Validation attribute parsing has been altered to match the latest version of the validator crate:
- Remove the `phone` attribute
- Remove the `required_nested` attribute
- `regex` and `contains` attributes must now be specified in list form `#[validate(regex(path = ...))]` rather than name/value form `#[validate(regex = ...)]`

## [1.0.0-alpha.11] - 2024-08-24

### Changed
Expand Down
24 changes: 11 additions & 13 deletions docs/_includes/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ TABLE OF CONTENTS
- [`with`](#with)
- [`bound`](#bound)
1. [Supported Validator Attributes](#supported-validator-attributes)
- [`email` / `phone` / `url`](#email-phone-url)
- [`email` / `url`](#email-url)
- [`length`](#length)
- [`range`](#range)
- [`regex`](#regex)
- [`contains`](#contains)
- [`required` / `required_nested`](#required)
- [`required`](#required)
1. [Other Attributes](#other-attributes)
- [`schema_with`](#schema_with)
- [`title` / `description`](#title-description)
Expand Down Expand Up @@ -177,17 +177,16 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)

<div class="indented">

<h3 id="email-phone-url">
<h3 id="email-url">

`#[validate(email)]` / `#[schemars(email)]`<br />
`#[validate(phone)]` / `#[schemars(phone)]`<br />
`#[validate(url)]` / `#[schemars(url)]`

</h3>

Sets the schema's `format` to `email`/`phone`/`uri`, as appropriate. Only one of these attributes may be present on a single field.
Sets the schema's `format` to `email`/`uri`, as appropriate. Only one of these attributes may be present on a single field.

Validator docs: [email](https://github.com/Keats/validator#email) / [phone](https://github.com/Keats/validator#phone) / [url](https://github.com/Keats/validator#url)
Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url)

<h3 id="length">

Expand All @@ -212,20 +211,20 @@ Validator docs: [range](https://github.com/Keats/validator#range)

<h3 id="regex">

`#[validate(regex = "path::to::regex")]` / `#[schemars(regex = "path::to::regex")]`<br />
`#[schemars(regex(pattern = r"^\d+$"))]`
`#[validate(regex(path = *static_regex)]`<br />
`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`

</h3>

Sets the `pattern` property for string schemas. The `path::to::regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method.
Sets the `pattern` property for string schemas. The `static_regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method.

Providing an inline regex pattern using `regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string.
`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. Using the `path` form is not allowed in a `#[schemars(...)]` attribute.

Validator docs: [regex](https://github.com/Keats/validator#regex)

<h3 id="contains">

`#[validate(contains = "string")]` / `#[schemars(contains = "string")]`
`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`

</h3>

Expand All @@ -236,13 +235,12 @@ Validator docs: [contains](https://github.com/Keats/validator#contains)
<h3 id="required">

`#[validate(required)]` / `#[schemars(required)]`<br />
`#[validate(required_nested)]`

</h3>

When set on an `Option<T>` field, this will create a schemas as though the field were a `T`.

Validator docs: [required](https://github.com/Keats/validator#required) / [required_nested](https://github.com/Keats/validator#required_nested)
Validator docs: [required](https://github.com/Keats/validator#required)

</div>

Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/examples/schemars_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct MyStruct {
#[derive(Deserialize, Serialize, JsonSchema)]
#[schemars(untagged)]
pub enum MyEnum {
StringNewType(#[schemars(phone)] String),
StringNewType(#[schemars(email)] String),
StructVariant {
#[schemars(length(min = 1, max = 100))]
floats: Vec<f32>,
Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/examples/schemars_attrs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"anyOf": [
{
"type": "string",
"format": "phone"
"format": "email"
},
{
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/examples/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct MyStruct {

#[derive(JsonSchema)]
pub enum MyEnum {
StringNewType(#[validate(phone)] String),
StringNewType(#[validate(email)] String),
StructVariant {
#[validate(length(min = 1, max = 100))]
floats: Vec<f32>,
Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/examples/validate.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"properties": {
"StringNewType": {
"type": "string",
"format": "phone"
"format": "email"
}
},
"additionalProperties": false,
Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/examples_v0/schemars_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct MyStruct {
#[derive(Deserialize, Serialize, JsonSchema)]
#[schemars(untagged)]
pub enum MyEnum {
StringNewType(#[schemars(phone)] String),
StringNewType(#[schemars(email)] String),
StructVariant {
#[schemars(length(min = 1, max = 100))]
floats: Vec<f32>,
Expand Down
2 changes: 1 addition & 1 deletion docs/_includes/examples_v0/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct MyStruct {

#[derive(JsonSchema)]
pub enum MyEnum {
StringNewType(#[validate(phone)] String),
StringNewType(#[validate(email)] String),
StructVariant {
#[validate(length(min = 1, max = 100))]
floats: Vec<f32>,
Expand Down
2 changes: 1 addition & 1 deletion docs/_v0/1.1-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
<h3 id="email-phone-url">

`#[validate(email)]` / `#[schemars(email)]`<br />
`#[validate(phone)]` / `#[schemars(phone)]`<br />
`#[validate(email)]` / `#[schemars(email)]`<br />
`#[validate(url)]` / `#[schemars(url)]`

</h3>
Expand Down
2 changes: 1 addition & 1 deletion schemars/examples/schemars_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct MyStruct {
#[derive(Deserialize, Serialize, JsonSchema)]
#[schemars(untagged)]
pub enum MyEnum {
StringNewType(#[schemars(phone)] String),
StringNewType(#[schemars(email)] String),
StructVariant {
#[schemars(length(min = 1, max = 100))]
floats: Vec<f32>,
Expand Down
2 changes: 1 addition & 1 deletion schemars/examples/schemars_attrs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"anyOf": [
{
"type": "string",
"format": "phone"
"format": "email"
},
{
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion schemars/examples/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct MyStruct {

#[derive(JsonSchema)]
pub enum MyEnum {
StringNewType(#[validate(phone)] String),
StringNewType(#[validate(email)] String),
StructVariant {
#[validate(length(min = 1, max = 100))]
floats: Vec<f32>,
Expand Down
2 changes: 1 addition & 1 deletion schemars/examples/validate.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"properties": {
"StringNewType": {
"type": "string",
"format": "phone"
"format": "email"
}
},
"additionalProperties": false,
Expand Down
53 changes: 36 additions & 17 deletions schemars/src/_private/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{JsonSchema, Schema, SchemaGenerator};
use serde::Serialize;
use serde_json::{json, map::Entry, Map, Value};

mod regex_syntax;
mod rustdoc;

pub use rustdoc::get_title_and_description;
Expand Down Expand Up @@ -141,24 +142,35 @@ pub fn insert_object_property<T: ?Sized + JsonSchema>(
required: bool,
sub_schema: Schema,
) {
let obj = schema.ensure_object();
if let Some(properties) = obj
.entry("properties")
.or_insert(Value::Object(Map::new()))
.as_object_mut()
{
properties.insert(key.to_owned(), sub_schema.into());
}

if !has_default && (required || !T::_schemars_private_is_option()) {
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
.as_array_mut()
fn insert_object_property_impl(
schema: &mut Schema,
key: &str,
has_default: bool,
required: bool,
sub_schema: Schema,
) {
let obj = schema.ensure_object();
if let Some(properties) = obj
.entry("properties")
.or_insert(Value::Object(Map::new()))
.as_object_mut()
{
req.push(key.into());
properties.insert(key.to_owned(), sub_schema.into());
}

if !has_default && (required) {
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
.as_array_mut()
{
req.push(key.into());
}
}
}

let required = required || !T::_schemars_private_is_option();
insert_object_property_impl(schema, key, has_default, required, sub_schema);
}

pub fn insert_metadata_property(schema: &mut Schema, key: &str, value: impl Into<Value>) {
Expand Down Expand Up @@ -187,14 +199,21 @@ pub fn insert_validation_property(
}
}

pub fn append_required(schema: &mut Schema, key: &str) {
pub fn must_contain(schema: &mut Schema, contain: String) {
if schema.has_type("string") {
let pattern = regex_syntax::escape(&contain);
schema
.ensure_object()
.insert("pattern".to_owned(), pattern.into());
}

if schema.has_type("object") {
if let Value::Array(array) = schema
.ensure_object()
.entry("required")
.or_insert(Value::Array(Vec::new()))
{
let value = Value::from(key);
let value = Value::from(contain);
if !array.contains(&value) {
array.push(value);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![allow(clippy::all)]
use crate::_alloc_prelude::*;
// Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function
// https://github.com/rust-lang/regex/blob/431c4e4867e1eb33eb39b23ed47c9934b2672f8f/regex-syntax/src/lib.rs
//
Expand Down
3 changes: 2 additions & 1 deletion schemars/tests/expected/skip_tuple_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"prefixItems": [
{
"type": "number",
"format": "float"
"format": "float",
"writeOnly": true
},
{
"type": "null"
Expand Down
10 changes: 0 additions & 10 deletions schemars/tests/expected/validate.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
"type": "string",
"pattern": "^[Hh]ello\\b"
},
"regex_str3": {
"type": "string",
"pattern": "^\\d+$"
},
"contains_str1": {
"type": "string",
"pattern": "substring\\.\\.\\."
Expand All @@ -39,10 +35,6 @@
"type": "string",
"format": "email"
},
"tel": {
"type": "string",
"format": "phone"
},
"homepage": {
"type": "string",
"format": "uri"
Expand Down Expand Up @@ -88,11 +80,9 @@
"min_max2",
"regex_str1",
"regex_str2",
"regex_str3",
"contains_str1",
"contains_str2",
"email_address",
"tel",
"homepage",
"non_empty_str",
"non_empty_str2",
Expand Down
10 changes: 0 additions & 10 deletions schemars/tests/expected/validate_schemars_attrs.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
"pattern": "^[Hh]ello\\b"
},
"regex_str2": {
"type": "string",
"pattern": "^[Hh]ello\\b"
},
"regex_str3": {
"type": "string",
"pattern": "^\\d+$"
},
Expand All @@ -39,10 +35,6 @@
"type": "string",
"format": "email"
},
"tel": {
"type": "string",
"format": "phone"
},
"homepage": {
"type": "string",
"format": "uri"
Expand Down Expand Up @@ -88,11 +80,9 @@
"min_max2",
"regex_str1",
"regex_str2",
"regex_str3",
"contains_str1",
"contains_str2",
"email_address",
"tel",
"homepage",
"non_empty_str",
"non_empty_str2",
Expand Down
4 changes: 2 additions & 2 deletions schemars/tests/ui/invalid_validation_attrs.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use schemars::JsonSchema;

// FIXME validation attrs like `email` should be disallowed non structs/enums/variants

#[derive(JsonSchema)]
#[validate(email)]
pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String);
Expand All @@ -15,6 +13,7 @@ pub struct Struct3(
#[validate(
regex = "foo",
contains = "bar",
regex(pattern = "baz"),
regex(path = "baz"),
phone,
email,
Expand All @@ -29,6 +28,7 @@ pub struct Struct4(
regex = "foo",
contains = "bar",
regex(path = "baz"),
regex(pattern = "baz"),
phone,
email,
url
Expand Down
Loading

0 comments on commit d07a1be

Please sign in to comment.