Add #[schemars(extend("key" = value))] attribute (#297)

This commit is contained in:
Graham Esau 2024-06-05 21:09:52 +01:00 committed by GitHub
parent 97b70aa82c
commit 840315b2dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 527 additions and 26 deletions

View file

@ -13,11 +13,11 @@
"default": false "default": false
}, },
"my_optional_string": { "my_optional_string": {
"default": null,
"type": [ "type": [
"string", "string",
"null" "null"
] ],
"default": null
}, },
"my_struct2": { "my_struct2": {
"$ref": "#/$defs/MyStruct2", "$ref": "#/$defs/MyStruct2",

View file

@ -0,0 +1,101 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Adjacent",
"oneOf": [
{
"type": "object",
"properties": {
"t": {
"type": "string",
"enum": [
"Unit"
]
}
},
"required": [
"t"
],
"foo": "bar"
},
{
"type": "object",
"properties": {
"t": {
"type": "string",
"enum": [
"NewType"
]
},
"c": true
},
"required": [
"t",
"c"
],
"foo": "bar"
},
{
"type": "object",
"properties": {
"t": {
"type": "string",
"enum": [
"Tuple"
]
},
"c": {
"type": "array",
"prefixItems": [
{
"type": "integer",
"format": "int32"
},
{
"type": "boolean"
}
],
"minItems": 2,
"maxItems": 2
}
},
"required": [
"t",
"c"
],
"foo": "bar"
},
{
"type": "object",
"properties": {
"t": {
"type": "string",
"enum": [
"Struct"
]
},
"c": {
"type": "object",
"properties": {
"i": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "boolean"
}
},
"required": [
"i",
"b"
]
}
},
"required": [
"t",
"c"
],
"foo": "bar"
}
],
"foo": "bar"
}

View file

@ -0,0 +1,73 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "External",
"oneOf": [
{
"type": "string",
"const": "Unit",
"foo": "bar"
},
{
"type": "object",
"properties": {
"NewType": true
},
"required": [
"NewType"
],
"additionalProperties": false,
"foo": "bar"
},
{
"type": "object",
"properties": {
"Tuple": {
"type": "array",
"prefixItems": [
{
"type": "integer",
"format": "int32"
},
{
"type": "boolean"
}
],
"minItems": 2,
"maxItems": 2
}
},
"required": [
"Tuple"
],
"additionalProperties": false,
"foo": "bar"
},
{
"type": "object",
"properties": {
"Struct": {
"type": "object",
"properties": {
"i": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "boolean"
}
},
"required": [
"i",
"b"
]
}
},
"required": [
"Struct"
],
"additionalProperties": false,
"foo": "bar"
}
],
"foo": "bar"
}

View file

@ -0,0 +1,55 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Internal",
"oneOf": [
{
"type": "object",
"properties": {
"typeProperty": {
"type": "string",
"const": "Unit"
}
},
"required": [
"typeProperty"
],
"foo": "bar"
},
{
"type": "object",
"properties": {
"typeProperty": {
"type": "string",
"const": "NewType"
}
},
"required": [
"typeProperty"
],
"foo": "bar"
},
{
"type": "object",
"properties": {
"i": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "boolean"
},
"typeProperty": {
"type": "string",
"const": "Struct"
}
},
"required": [
"typeProperty",
"i",
"b"
],
"foo": "bar"
}
],
"foo": "bar"
}

View file

@ -0,0 +1,46 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Untagged",
"anyOf": [
{
"type": "null",
"foo": "bar"
},
{
"foo": "bar"
},
{
"type": "array",
"prefixItems": [
{
"type": "integer",
"format": "int32"
},
{
"type": "boolean"
}
],
"minItems": 2,
"maxItems": 2,
"foo": "bar"
},
{
"type": "object",
"properties": {
"i": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "boolean"
}
},
"required": [
"i",
"b"
],
"foo": "bar"
}
],
"foo": "bar"
}

View file

@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Struct",
"type": "object",
"properties": {
"value": {
"foo": "bar"
},
"int": {
"type": "overridden",
"format": "int32"
}
},
"required": [
"value",
"int"
],
"msg": "hello world",
"obj": {
"array": [
null,
null
]
},
"3": 3.0,
"pi": 3.14
}

View file

@ -35,6 +35,8 @@
}, },
"my_tuple": { "my_tuple": {
"type": "array", "type": "array",
"minItems": 2,
"maxItems": 2,
"items": [ "items": [
{ {
"type": "string", "type": "string",
@ -44,9 +46,7 @@
{ {
"type": "integer" "type": "integer"
} }
], ]
"maxItems": 2,
"minItems": 2
} }
} }
} }

View file

@ -35,6 +35,8 @@
}, },
"my_tuple": { "my_tuple": {
"type": "array", "type": "array",
"minItems": 2,
"maxItems": 2,
"items": [ "items": [
{ {
"type": "string", "type": "string",
@ -44,9 +46,7 @@
{ {
"type": "integer" "type": "integer"
} }
], ]
"maxItems": 2,
"minItems": 2
} }
} }
} }

View file

@ -37,6 +37,8 @@
}, },
"my_tuple": { "my_tuple": {
"type": "array", "type": "array",
"minItems": 2,
"maxItems": 2,
"items": [ "items": [
{ {
"type": "string", "type": "string",
@ -46,9 +48,7 @@
{ {
"type": "integer" "type": "integer"
} }
], ]
"maxItems": 2,
"minItems": 2
} }
} }
} }

View file

@ -30,6 +30,8 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "array", "type": "array",
"maxItems": 2,
"minItems": 2,
"items": [ "items": [
{ {
"type": "integer", "type": "integer",
@ -40,9 +42,7 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
} }
], ]
"minItems": 2,
"maxItems": 2
} }
} }
}, },

View file

@ -25,6 +25,8 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "array", "type": "array",
"maxItems": 2,
"minItems": 2,
"items": [ "items": [
{ {
"type": "integer", "type": "integer",
@ -35,9 +37,7 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
} }
], ]
"minItems": 2,
"maxItems": 2
} }
} }
}, },

View file

@ -30,6 +30,8 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "array", "type": "array",
"maxItems": 2,
"minItems": 2,
"items": [ "items": [
{ {
"type": "integer", "type": "integer",
@ -40,9 +42,7 @@
"type": "integer", "type": "integer",
"format": "int64" "format": "int64"
} }
], ]
"minItems": 2,
"maxItems": 2
} }
} }
}, },

96
schemars/tests/extend.rs Normal file
View file

@ -0,0 +1,96 @@
mod util;
use schemars::JsonSchema;
use serde_json::Value;
use util::*;
const THREE: f64 = 3.0;
#[allow(dead_code)]
#[derive(JsonSchema)]
#[schemars(extend("msg" = concat!("hello ", "world"), "obj" = {"array": [null, ()]}))]
#[schemars(extend("3" = THREE), extend("pi" = THREE + 0.14))]
struct Struct {
#[schemars(extend("foo" = "bar"))]
value: Value,
#[schemars(extend("type" = "overridden"))]
int: i32,
}
#[test]
fn doc_comments_struct() -> TestResult {
test_default_generated_schema::<Struct>("extend_struct")
}
#[allow(dead_code)]
#[derive(JsonSchema)]
#[schemars(extend("foo" = "bar"))]
enum External {
#[schemars(extend("foo" = "bar"))]
Unit,
#[schemars(extend("foo" = "bar"))]
NewType(Value),
#[schemars(extend("foo" = "bar"))]
Tuple(i32, bool),
#[schemars(extend("foo" = "bar"))]
Struct { i: i32, b: bool },
}
#[test]
fn doc_comments_enum_external() -> TestResult {
test_default_generated_schema::<External>("extend_enum_external")
}
#[allow(dead_code)]
#[derive(JsonSchema)]
#[schemars(tag = "typeProperty", extend("foo" = "bar"))]
enum Internal {
#[schemars(extend("foo" = "bar"))]
Unit,
#[schemars(extend("foo" = "bar"))]
NewType(Value),
#[schemars(extend("foo" = "bar"))]
Struct { i: i32, b: bool },
}
#[test]
fn doc_comments_enum_internal() -> TestResult {
test_default_generated_schema::<Internal>("extend_enum_internal")
}
#[allow(dead_code)]
#[derive(JsonSchema)]
#[schemars(untagged, extend("foo" = "bar"))]
enum Untagged {
#[schemars(extend("foo" = "bar"))]
Unit,
#[schemars(extend("foo" = "bar"))]
NewType(Value),
#[schemars(extend("foo" = "bar"))]
Tuple(i32, bool),
#[schemars(extend("foo" = "bar"))]
Struct { i: i32, b: bool },
}
#[test]
fn doc_comments_enum_untagged() -> TestResult {
test_default_generated_schema::<Untagged>("extend_enum_untagged")
}
#[allow(dead_code)]
#[derive(JsonSchema)]
#[schemars(tag = "t", content = "c", extend("foo" = "bar"))]
enum Adjacent {
#[schemars(extend("foo" = "bar"))]
Unit,
#[schemars(extend("foo" = "bar"))]
NewType(Value),
#[schemars(extend("foo" = "bar"))]
Tuple(i32, bool),
#[schemars(extend("foo" = "bar"))]
Struct { i: i32, b: bool },
}
#[test]
fn doc_comments_enum_adjacent() -> TestResult {
test_default_generated_schema::<Adjacent>("extend_enum_adjacent")
}

View file

@ -0,0 +1,11 @@
use schemars::JsonSchema;
#[derive(JsonSchema)]
#[schemars(extend(x))]
#[schemars(extend("x"))]
#[schemars(extend("x" = ))]
#[schemars(extend("y" = "ok!", "y" = "duplicated!"), extend("y" = "duplicated!"))]
#[schemars(extend("y" = "duplicated!"))]
pub struct Struct;
fn main() {}

View file

@ -0,0 +1,35 @@
error: expected string literal
--> tests/ui/invalid_extend.rs:4:19
|
4 | #[schemars(extend(x))]
| ^
error: expected `=`
--> tests/ui/invalid_extend.rs:5:22
|
5 | #[schemars(extend("x"))]
| ^
error: Expected extension value
--> tests/ui/invalid_extend.rs:6:25
|
6 | #[schemars(extend("x" = ))]
| ^
error: Duplicate extension key 'y'
--> tests/ui/invalid_extend.rs:7:32
|
7 | #[schemars(extend("y" = "ok!", "y" = "duplicated!"), extend("y" = "duplicated!"))]
| ^^^
error: Duplicate extension key 'y'
--> tests/ui/invalid_extend.rs:7:61
|
7 | #[schemars(extend("y" = "ok!", "y" = "duplicated!"), extend("y" = "duplicated!"))]
| ^^^
error: Duplicate extension key 'y'
--> tests/ui/invalid_extend.rs:8:19
|
8 | #[schemars(extend("y" = "duplicated!"))]
| ^^^

View file

@ -10,7 +10,7 @@ use proc_macro2::{Group, Span, TokenStream, TokenTree};
use quote::ToTokens; use quote::ToTokens;
use serde_derive_internals::Ctxt; use serde_derive_internals::Ctxt;
use syn::parse::{self, Parse}; use syn::parse::{self, Parse};
use syn::{Meta, MetaNameValue}; use syn::{LitStr, Meta, MetaNameValue};
// FIXME using the same struct for containers+variants+fields means that // FIXME using the same struct for containers+variants+fields means that
// with/schema_with are accepted (but ignored) on containers, and // with/schema_with are accepted (but ignored) on containers, and
@ -26,6 +26,7 @@ pub struct Attrs {
pub repr: Option<syn::Type>, pub repr: Option<syn::Type>,
pub crate_name: Option<syn::Path>, pub crate_name: Option<syn::Path>,
pub is_renamed: bool, pub is_renamed: bool,
pub extensions: Vec<(String, TokenStream)>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -68,6 +69,7 @@ impl Attrs {
description: self.description.as_ref().and_then(none_if_empty), description: self.description.as_ref().and_then(none_if_empty),
deprecated: self.deprecated, deprecated: self.deprecated,
examples: &self.examples, examples: &self.examples,
extensions: &self.extensions,
read_only: false, read_only: false,
write_only: false, write_only: false,
default: None, default: None,
@ -162,6 +164,29 @@ impl Attrs {
} }
} }
Meta::List(m) if m.path.is_ident("extend") && attr_type == "schemars" => {
let parser =
syn::punctuated::Punctuated::<Extension, Token![,]>::parse_terminated;
match m.parse_args_with(parser) {
Ok(extensions) => {
for extension in extensions {
let key = extension.key.value();
// This is O(n^2) but should be fine with the typically small number of extensions.
// If this does become a problem, it can be changed to use IndexMap, or a separate Map with cloned keys.
if self.extensions.iter().any(|e| e.0 == key) {
errors.error_spanned_by(
extension.key,
format!("Duplicate extension key '{}'", key),
);
} else {
self.extensions.push((key, extension.value));
}
}
}
Err(err) => errors.syn_error(err),
}
}
_ if ignore_errors => {} _ if ignore_errors => {}
Meta::List(m) if m.path.is_ident("inner") && attr_type == "schemars" => { Meta::List(m) if m.path.is_ident("inner") && attr_type == "schemars" => {
@ -198,7 +223,8 @@ impl Attrs {
repr: None, repr: None,
crate_name: None, crate_name: None,
is_renamed: _, is_renamed: _,
} if examples.is_empty()) extensions,
} if examples.is_empty() && extensions.is_empty())
} }
} }
@ -322,3 +348,27 @@ fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree {
token.set_span(span); token.set_span(span);
token token
} }
#[derive(Debug)]
struct Extension {
key: LitStr,
value: TokenStream,
}
impl Parse for Extension {
fn parse(input: parse::ParseStream) -> syn::Result<Self> {
let key = input.parse::<LitStr>()?;
input.parse::<Token![=]>()?;
let mut value = TokenStream::new();
while !input.is_empty() && !input.peek(Token![,]) {
value.extend([input.parse::<TokenTree>()?]);
}
if value.is_empty() {
return Err(syn::Error::new(input.span(), "Expected extension value"));
}
Ok(Extension { key, value })
}
}

View file

@ -9,6 +9,7 @@ pub struct SchemaMetadata<'a> {
pub write_only: bool, pub write_only: bool,
pub examples: &'a [syn::Path], pub examples: &'a [syn::Path],
pub default: Option<TokenStream>, pub default: Option<TokenStream>,
pub extensions: &'a [(String, TokenStream)],
} }
impl<'a> SchemaMetadata<'a> { impl<'a> SchemaMetadata<'a> {
@ -74,6 +75,12 @@ impl<'a> SchemaMetadata<'a> {
}); });
} }
for (k, v) in self.extensions {
setters.push(quote! {
obj.insert(#k.to_owned(), schemars::_serde_json::json!(#v));
});
}
setters setters
} }
} }

View file

@ -232,13 +232,13 @@ fn expr_for_internal_tagged_enum<'a>(
let name = variant.name(); let name = variant.name();
let mut schema_expr = expr_for_internal_tagged_enum_variant(variant, deny_unknown_fields); let mut schema_expr = expr_for_internal_tagged_enum_variant(variant, deny_unknown_fields);
variant.attrs.as_metadata().apply_to_schema(&mut schema_expr); schema_expr = quote!({
quote!({
let mut schema = #schema_expr; let mut schema = #schema_expr;
schemars::_private::apply_internal_enum_variant_tag(&mut schema, #tag_name, #name, #deny_unknown_fields); schemars::_private::apply_internal_enum_variant_tag(&mut schema, #tag_name, #name, #deny_unknown_fields);
schema schema
}) });
variant.attrs.as_metadata().apply_to_schema(&mut schema_expr);
schema_expr
}) })
.collect(); .collect();