Add transform = ... attribute (#312)

This allows running arbitrary transforms on generated schemas when deriving `JsonSchema`
This commit is contained in:
Graham Esau 2024-08-10 09:56:52 +01:00 committed by GitHub
parent 29067a0331
commit 14b06e71ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 173 additions and 10 deletions

View file

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

View file

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

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

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

View file

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

View 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")
}

View file

@ -0,0 +1,7 @@
use schemars::JsonSchema;
#[derive(JsonSchema)]
#[schemars(transform = "x")]
pub struct Struct;
fn main() {}

View 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")]
| ^^^

View file

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

View file

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