Add transform = ...
attribute (#312)
This allows running arbitrary transforms on generated schemas when deriving `JsonSchema`
This commit is contained in:
parent
29067a0331
commit
14b06e71ba
10 changed files with 173 additions and 10 deletions
|
@ -49,6 +49,7 @@ TABLE OF CONTENTS
|
||||||
- [`deprecated`](#deprecated)
|
- [`deprecated`](#deprecated)
|
||||||
- [`crate`](#crate)
|
- [`crate`](#crate)
|
||||||
- [`extend`](#extend)
|
- [`extend`](#extend)
|
||||||
|
- [`transform`](#transform)
|
||||||
- [Doc Comments (`doc`)](#doc)
|
- [Doc Comments (`doc`)](#doc)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -326,10 +327,31 @@ Set on a container, variant or field to add properties (or replace existing prop
|
||||||
The key must be a quoted string, and the value can be any expression that produces a type implementing `serde::Serialize`. The value can also be a JSON literal which can interpolate other values.
|
The key must be a quoted string, and the value can be any expression that produces a type implementing `serde::Serialize`. The value can also be a JSON literal which can interpolate other values.
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
|
#[derive(JsonSchema)]
|
||||||
#[schemars(extend("simple" = "string value", "complex" = {"array": [1, 2, 3]}))]
|
#[schemars(extend("simple" = "string value", "complex" = {"array": [1, 2, 3]}))]
|
||||||
struct Struct;
|
struct Struct;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<h3 id="transform">
|
||||||
|
|
||||||
|
`#[schemars(transform = some::transform)]`
|
||||||
|
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
Set on a container, variant or field to run a `schemars::transform::Transform` against the generated schema. This can be specified multiple times to run multiple transforms.
|
||||||
|
|
||||||
|
The `Transform` trait is implemented on functions with the signature `fn(&mut Schema) -> ()`, allowing you to do this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn my_transform(schema: &mut Schema) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
#[schemars(transform = my_transform)]
|
||||||
|
struct Struct;
|
||||||
|
```
|
||||||
|
|
||||||
<h3 id="doc">
|
<h3 id="doc">
|
||||||
|
|
||||||
Doc Comments (`#[doc = "..."]`)
|
Doc Comments (`#[doc = "..."]`)
|
||||||
|
|
|
@ -24,7 +24,3 @@ install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
|
||||||
gem "tzinfo", "~> 1.2"
|
gem "tzinfo", "~> 1.2"
|
||||||
gem "tzinfo-data"
|
gem "tzinfo-data"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Performance-booster for watching directories on Windows
|
|
||||||
gem "wdm", "~> 0.1.1", :install_if => Gem.win_platform?
|
|
||||||
|
|
||||||
|
|
25
schemars/tests/expected/transform_enum_external.json
Normal file
25
schemars/tests/expected/transform_enum_external.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "External",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"const": "Unit",
|
||||||
|
"propertyCount": 0,
|
||||||
|
"upperType": "STRING"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"NewType": true
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"NewType"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"propertyCount": 1,
|
||||||
|
"upperType": "OBJECT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"propertyCount": 0
|
||||||
|
}
|
20
schemars/tests/expected/transform_struct.json
Normal file
20
schemars/tests/expected/transform_struct.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "Struct",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": true,
|
||||||
|
"int": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"propertyCount": 0,
|
||||||
|
"upperType": "INTEGER"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"value",
|
||||||
|
"int"
|
||||||
|
],
|
||||||
|
"upperType": "OBJECT",
|
||||||
|
"propertyCount": 2
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ struct Struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comments_struct() -> TestResult {
|
fn extend_struct() -> TestResult {
|
||||||
test_default_generated_schema::<Struct>("extend_struct")
|
test_default_generated_schema::<Struct>("extend_struct")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ enum External {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comments_enum_external() -> TestResult {
|
fn extend_enum_external() -> TestResult {
|
||||||
test_default_generated_schema::<External>("extend_enum_external")
|
test_default_generated_schema::<External>("extend_enum_external")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ enum Internal {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comments_enum_internal() -> TestResult {
|
fn extend_enum_internal() -> TestResult {
|
||||||
test_default_generated_schema::<Internal>("extend_enum_internal")
|
test_default_generated_schema::<Internal>("extend_enum_internal")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ enum Untagged {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comments_enum_untagged() -> TestResult {
|
fn extend_enum_untagged() -> TestResult {
|
||||||
test_default_generated_schema::<Untagged>("extend_enum_untagged")
|
test_default_generated_schema::<Untagged>("extend_enum_untagged")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +91,6 @@ enum Adjacent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn doc_comments_enum_adjacent() -> TestResult {
|
fn extend_enum_adjacent() -> TestResult {
|
||||||
test_default_generated_schema::<Adjacent>("extend_enum_adjacent")
|
test_default_generated_schema::<Adjacent>("extend_enum_adjacent")
|
||||||
}
|
}
|
||||||
|
|
51
schemars/tests/transform.rs
Normal file
51
schemars/tests/transform.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
mod util;
|
||||||
|
use schemars::{transform::RecursiveTransform, JsonSchema, Schema};
|
||||||
|
use serde_json::Value;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
fn capitalize_type(schema: &mut Schema) {
|
||||||
|
if let Some(obj) = schema.as_object_mut() {
|
||||||
|
if let Some(Value::String(ty)) = obj.get("type") {
|
||||||
|
obj.insert("upperType".to_owned(), ty.to_uppercase().into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_property_count(schema: &mut Schema) {
|
||||||
|
if let Some(obj) = schema.as_object_mut() {
|
||||||
|
let count = obj
|
||||||
|
.get("properties")
|
||||||
|
.and_then(|p| p.as_object())
|
||||||
|
.map_or(0, |p| p.len());
|
||||||
|
obj.insert("propertyCount".to_owned(), count.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
#[schemars(transform = RecursiveTransform(capitalize_type), transform = insert_property_count)]
|
||||||
|
struct Struct {
|
||||||
|
value: Value,
|
||||||
|
#[schemars(transform = insert_property_count)]
|
||||||
|
int: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transform_struct() -> TestResult {
|
||||||
|
test_default_generated_schema::<Struct>("transform_struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
#[schemars(transform = RecursiveTransform(capitalize_type), transform = insert_property_count)]
|
||||||
|
enum External {
|
||||||
|
#[schemars(transform = insert_property_count)]
|
||||||
|
Unit,
|
||||||
|
#[schemars(transform = insert_property_count)]
|
||||||
|
NewType(Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transform_enum_external() -> TestResult {
|
||||||
|
test_default_generated_schema::<External>("transform_enum_external")
|
||||||
|
}
|
7
schemars/tests/ui/transform_str.rs
Normal file
7
schemars/tests/ui/transform_str.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
#[schemars(transform = "x")]
|
||||||
|
pub struct Struct;
|
||||||
|
|
||||||
|
fn main() {}
|
6
schemars/tests/ui/transform_str.stderr
Normal file
6
schemars/tests/ui/transform_str.stderr
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
error: Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.
|
||||||
|
Did you mean `[schemars(transform = x)]`?
|
||||||
|
--> tests/ui/transform_str.rs:4:24
|
||||||
|
|
|
||||||
|
4 | #[schemars(transform = "x")]
|
||||||
|
| ^^^
|
|
@ -27,6 +27,7 @@ pub struct Attrs {
|
||||||
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)>,
|
pub extensions: Vec<(String, TokenStream)>,
|
||||||
|
pub transforms: Vec<syn::Expr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -70,6 +71,7 @@ impl Attrs {
|
||||||
deprecated: self.deprecated,
|
deprecated: self.deprecated,
|
||||||
examples: &self.examples,
|
examples: &self.examples,
|
||||||
extensions: &self.extensions,
|
extensions: &self.extensions,
|
||||||
|
transforms: &self.transforms,
|
||||||
read_only: false,
|
read_only: false,
|
||||||
write_only: false,
|
write_only: false,
|
||||||
default: None,
|
default: None,
|
||||||
|
@ -164,6 +166,25 @@ impl Attrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Meta::NameValue(m) if m.path.is_ident("transform") && attr_type == "schemars" => {
|
||||||
|
if let syn::Expr::Lit(syn::ExprLit {
|
||||||
|
lit: syn::Lit::Str(lit_str),
|
||||||
|
..
|
||||||
|
}) = &m.value
|
||||||
|
{
|
||||||
|
if parse_lit_str::<syn::Expr>(lit_str).is_ok() {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
&m.value,
|
||||||
|
format!(
|
||||||
|
"Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `[schemars(transform = {})]`?",
|
||||||
|
lit_str.value()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.transforms.push(m.value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
Meta::List(m) if m.path.is_ident("extend") && attr_type == "schemars" => {
|
Meta::List(m) if m.path.is_ident("extend") && attr_type == "schemars" => {
|
||||||
let parser =
|
let parser =
|
||||||
syn::punctuated::Punctuated::<Extension, Token![,]>::parse_terminated;
|
syn::punctuated::Punctuated::<Extension, Token![,]>::parse_terminated;
|
||||||
|
@ -224,7 +245,8 @@ impl Attrs {
|
||||||
crate_name: None,
|
crate_name: None,
|
||||||
is_renamed: _,
|
is_renamed: _,
|
||||||
extensions,
|
extensions,
|
||||||
} if examples.is_empty() && extensions.is_empty())
|
transforms
|
||||||
|
} if examples.is_empty() && extensions.is_empty() && transforms.is_empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SchemaMetadata<'a> {
|
pub struct SchemaMetadata<'a> {
|
||||||
|
@ -10,6 +11,7 @@ pub struct SchemaMetadata<'a> {
|
||||||
pub examples: &'a [syn::Path],
|
pub examples: &'a [syn::Path],
|
||||||
pub default: Option<TokenStream>,
|
pub default: Option<TokenStream>,
|
||||||
pub extensions: &'a [(String, TokenStream)],
|
pub extensions: &'a [(String, TokenStream)],
|
||||||
|
pub transforms: &'a [syn::Expr],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SchemaMetadata<'a> {
|
impl<'a> SchemaMetadata<'a> {
|
||||||
|
@ -23,6 +25,18 @@ impl<'a> SchemaMetadata<'a> {
|
||||||
schema
|
schema
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
if !self.transforms.is_empty() {
|
||||||
|
let apply_transforms = self.transforms.iter().map(|t| {
|
||||||
|
quote_spanned! {t.span()=>
|
||||||
|
schemars::transform::Transform::transform(&mut #t, &mut schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*schema_expr = quote! {{
|
||||||
|
let mut schema = #schema_expr;
|
||||||
|
#(#apply_transforms)*
|
||||||
|
schema
|
||||||
|
}};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_setters(&self) -> Vec<TokenStream> {
|
fn make_setters(&self) -> Vec<TokenStream> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue