diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md index 317c51b8..52ddc8a2 100644 --- a/docs/1.1-attributes.md +++ b/docs/1.1-attributes.md @@ -49,6 +49,7 @@ TABLE OF CONTENTS - [`deprecated`](#deprecated) - [`crate`](#crate) - [`extend`](#extend) + - [`transform`](#transform) - [Doc Comments (`doc`)](#doc) @@ -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; ``` +

+ +`#[schemars(transform = some::transform)]` + +

+ +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; +``` +

Doc Comments (`#[doc = "..."]`) diff --git a/docs/Gemfile b/docs/Gemfile index d3cf985c..0888f7a2 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -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? - diff --git a/schemars/tests/expected/transform_enum_external.json b/schemars/tests/expected/transform_enum_external.json new file mode 100644 index 00000000..af746e3c --- /dev/null +++ b/schemars/tests/expected/transform_enum_external.json @@ -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 +} \ No newline at end of file diff --git a/schemars/tests/expected/transform_struct.json b/schemars/tests/expected/transform_struct.json new file mode 100644 index 00000000..6723414c --- /dev/null +++ b/schemars/tests/expected/transform_struct.json @@ -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 +} \ No newline at end of file diff --git a/schemars/tests/extend.rs b/schemars/tests/extend.rs index 08f42fa9..2b44f5e0 100644 --- a/schemars/tests/extend.rs +++ b/schemars/tests/extend.rs @@ -17,7 +17,7 @@ struct Struct { } #[test] -fn doc_comments_struct() -> TestResult { +fn extend_struct() -> TestResult { test_default_generated_schema::("extend_struct") } @@ -36,7 +36,7 @@ enum External { } #[test] -fn doc_comments_enum_external() -> TestResult { +fn extend_enum_external() -> TestResult { test_default_generated_schema::("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::("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::("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::("extend_enum_adjacent") } diff --git a/schemars/tests/transform.rs b/schemars/tests/transform.rs new file mode 100644 index 00000000..13ac30b1 --- /dev/null +++ b/schemars/tests/transform.rs @@ -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::("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::("transform_enum_external") +} diff --git a/schemars/tests/ui/transform_str.rs b/schemars/tests/ui/transform_str.rs new file mode 100644 index 00000000..6570accf --- /dev/null +++ b/schemars/tests/ui/transform_str.rs @@ -0,0 +1,7 @@ +use schemars::JsonSchema; + +#[derive(JsonSchema)] +#[schemars(transform = "x")] +pub struct Struct; + +fn main() {} diff --git a/schemars/tests/ui/transform_str.stderr b/schemars/tests/ui/transform_str.stderr new file mode 100644 index 00000000..6ee36983 --- /dev/null +++ b/schemars/tests/ui/transform_str.stderr @@ -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")] + | ^^^ diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 108cedc4..6b6e6766 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -27,6 +27,7 @@ pub struct Attrs { pub crate_name: Option, pub is_renamed: bool, pub extensions: Vec<(String, TokenStream)>, + pub transforms: Vec, } #[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::(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::::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()) } } diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs index 6a3808c3..c6b5dfe7 100644 --- a/schemars_derive/src/metadata.rs +++ b/schemars_derive/src/metadata.rs @@ -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, 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 {