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)
|
||||
- [`crate`](#crate)
|
||||
- [`extend`](#extend)
|
||||
- [`transform`](#transform)
|
||||
- [Doc Comments (`doc`)](#doc)
|
||||
|
||||
</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.
|
||||
|
||||
```plaintext
|
||||
#[derive(JsonSchema)]
|
||||
#[schemars(extend("simple" = "string value", "complex" = {"array": [1, 2, 3]}))]
|
||||
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">
|
||||
|
||||
Doc Comments (`#[doc = "..."]`)
|
||||
|
|
|
@ -24,7 +24,3 @@ install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
|
|||
gem "tzinfo", "~> 1.2"
|
||||
gem "tzinfo-data"
|
||||
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]
|
||||
fn doc_comments_struct() -> TestResult {
|
||||
fn extend_struct() -> TestResult {
|
||||
test_default_generated_schema::<Struct>("extend_struct")
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ enum External {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn doc_comments_enum_external() -> TestResult {
|
||||
fn extend_enum_external() -> TestResult {
|
||||
test_default_generated_schema::<External>("extend_enum_external")
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ enum Internal {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn doc_comments_enum_internal() -> TestResult {
|
||||
fn extend_enum_internal() -> TestResult {
|
||||
test_default_generated_schema::<Internal>("extend_enum_internal")
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ enum Untagged {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn doc_comments_enum_untagged() -> TestResult {
|
||||
fn extend_enum_untagged() -> TestResult {
|
||||
test_default_generated_schema::<Untagged>("extend_enum_untagged")
|
||||
}
|
||||
|
||||
|
@ -91,6 +91,6 @@ enum Adjacent {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn doc_comments_enum_adjacent() -> TestResult {
|
||||
fn extend_enum_adjacent() -> TestResult {
|
||||
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 is_renamed: bool,
|
||||
pub extensions: Vec<(String, TokenStream)>,
|
||||
pub transforms: Vec<syn::Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -70,6 +71,7 @@ impl Attrs {
|
|||
deprecated: self.deprecated,
|
||||
examples: &self.examples,
|
||||
extensions: &self.extensions,
|
||||
transforms: &self.transforms,
|
||||
read_only: false,
|
||||
write_only: false,
|
||||
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" => {
|
||||
let parser =
|
||||
syn::punctuated::Punctuated::<Extension, Token![,]>::parse_terminated;
|
||||
|
@ -224,7 +245,8 @@ impl Attrs {
|
|||
crate_name: None,
|
||||
is_renamed: _,
|
||||
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 syn::spanned::Spanned;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchemaMetadata<'a> {
|
||||
|
@ -10,6 +11,7 @@ pub struct SchemaMetadata<'a> {
|
|||
pub examples: &'a [syn::Path],
|
||||
pub default: Option<TokenStream>,
|
||||
pub extensions: &'a [(String, TokenStream)],
|
||||
pub transforms: &'a [syn::Expr],
|
||||
}
|
||||
|
||||
impl<'a> SchemaMetadata<'a> {
|
||||
|
@ -23,6 +25,18 @@ impl<'a> SchemaMetadata<'a> {
|
|||
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> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue