From e5168819a41469465171e12c8d48f53dc9160821 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Wed, 20 Nov 2024 22:18:31 +0000 Subject: [PATCH] Allow `example` attribute value to be any expression (#354) --- CHANGELOG.md | 5 ++-- docs/_includes/attributes.md | 8 +++--- schemars/tests/integration/examples.rs | 12 +++------ .../integration/examples.rs~examples.de.json | 3 ++- .../integration/examples.rs~examples.ser.json | 3 ++- schemars/tests/ui/example_fn.rs | 9 +++++++ schemars/tests/ui/example_fn.stderr | 7 ++++++ schemars/tests/ui/transform_str.stderr | 2 +- schemars_derive/src/attr/mod.rs | 25 ++++++++++++++++--- 9 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 schemars/tests/ui/example_fn.rs create mode 100644 schemars/tests/ui/example_fn.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index a84c477..7b0adea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,10 @@ - the `enumset1`/`enumset` optional dependency has been removed, as its `JsonSchema` impl did not actually match the default serialization format of `EnumSet` (https://github.com/GREsau/schemars/pull/339) -### Changed +### Changed (_⚠️ breaking changes ⚠️_) -- ⚠️ MSRV is now 1.70 ⚠️ +- MSRV is now 1.70 +- [The `example` attribute](https://graham.cool/schemars/deriving/attributes/#example) value is now an arbitrary expression, rather than a string literal identifying a function to call. To avoid silent behaviour changes, the expression must not be a string literal where the value can be parsed as a function path - e.g. `#[schemars(example = "foo")]` is now a compile error, but `#[schemars(example = foo())]` is allowed (as is `#[schemars(example = &"foo")]` if you want the the literal string value `"foo"` to be the example). ### Fixed diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md index 37f6833..1497b0f 100644 --- a/docs/_includes/attributes.md +++ b/docs/_includes/attributes.md @@ -298,13 +298,15 @@ Set on a container, variant or field to set the generated schema's `title` and/o

-`#[schemars(example = "some::function")]` +`#[schemars(example = value)]`

-Set on a container, variant or field to include the result of the given function in the generated schema's `examples`. The function should take no parameters and can return any type that implements serde's `Serialize` trait - it does not need to return the same type as the attached struct/field. This attribute can be repeated to specify multiple examples. +Set on a container, variant or field to include the given value in the generated schema's `examples`. The value can be any type that implements serde's `Serialize` trait - it does not need to be the same type as the attached struct/field. This attribute can be repeated to specify multiple examples. -To use the result of arbitrary expressions as examples, you can instead use the [`extend`](#extend) attribute, e.g. `[schemars(extend("examples" = ["example string"]))]`. +In previous versions of schemars, the value had to be a string literal identifying a defined function that would be called to return the actual example value (similar to the [`default`](#default) attribute). To avoid the new attribute behaviour from silently breaking old consumers, string literals consisting of a single word (e.g. `#[schemars(example = "my_fn")]`) or a path (e.g. `#[schemars(example = "my_mod::my_fn")]`) are currently disallowed. This restriction may be relaxed in a future version of schemars, but for now if you want to include such a string as the literal example value, this can be done by borrowing the value, e.g. `#[schemars(example = &"my_fn")]`. If you instead want to call a function to get the example value (mirrorring the old behaviour), you must use an explicit function call expression, e.g. `#[schemars(example = my_fn())]`. + +Alternatively, to directly set multiple examples without repeating `example = ...` attribute, you can instead use the [`extend`](#extend) attribute, e.g. `#[schemars(extend("examples" = [1, 2, 3]))]`.

diff --git a/schemars/tests/integration/examples.rs b/schemars/tests/integration/examples.rs index 43b7419..427a666 100644 --- a/schemars/tests/integration/examples.rs +++ b/schemars/tests/integration/examples.rs @@ -1,21 +1,15 @@ use crate::prelude::*; #[derive(Default, JsonSchema, Serialize)] -#[schemars(example = "Struct::default", example = "null")] +#[schemars(example = Struct::default(), example = ())] struct Struct { - #[schemars(example = "eight", example = "null")] + #[schemars(example = 4 + 4, example = ())] foo: i32, bar: bool, - #[schemars(example = "null")] + #[schemars(example = (), example = &"foo")] baz: Option<&'static str>, } -fn eight() -> i32 { - 8 -} - -fn null() {} - #[test] fn examples() { test!(Struct).assert_snapshot(); diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json index b891aa0..da04166 100644 --- a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json +++ b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.de.json @@ -20,7 +20,8 @@ "null" ], "examples": [ - null + null, + "foo" ] } }, diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json index 97465db..7de3378 100644 --- a/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json +++ b/schemars/tests/integration/snapshots/schemars/tests/integration/examples.rs~examples.ser.json @@ -20,7 +20,8 @@ "null" ], "examples": [ - null + null, + "foo" ] } }, diff --git a/schemars/tests/ui/example_fn.rs b/schemars/tests/ui/example_fn.rs new file mode 100644 index 0000000..c4921c5 --- /dev/null +++ b/schemars/tests/ui/example_fn.rs @@ -0,0 +1,9 @@ +use schemars::JsonSchema; + +#[derive(JsonSchema)] +#[schemars(example = "my_fn")] +pub struct Struct; + +fn my_fn() {} + +fn main() {} diff --git a/schemars/tests/ui/example_fn.stderr b/schemars/tests/ui/example_fn.stderr new file mode 100644 index 0000000..92a34f3 --- /dev/null +++ b/schemars/tests/ui/example_fn.stderr @@ -0,0 +1,7 @@ +error: `example` value must be an expression, and string literals that may be interpreted as function paths are currently disallowed to avoid migration errors (this restriction may be relaxed in a future version of schemars). + If you want to use the result of a function, use `#[schemars(example = my_fn())]`. + Or to use the string literal value, use `#[schemars(example = &"my_fn")]`. + --> tests/ui/example_fn.rs:4:22 + | +4 | #[schemars(example = "my_fn")] + | ^^^^^^^ diff --git a/schemars/tests/ui/transform_str.stderr b/schemars/tests/ui/transform_str.stderr index 6ee3698..50cf7b9 100644 --- a/schemars/tests/ui/transform_str.stderr +++ b/schemars/tests/ui/transform_str.stderr @@ -1,5 +1,5 @@ error: Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`. - Did you mean `[schemars(transform = x)]`? + 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 14fd6eb..37350d3 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -21,7 +21,7 @@ pub struct CommonAttrs { pub deprecated: bool, pub title: Option, pub description: Option, - pub examples: Vec, + pub examples: Vec, pub extensions: Vec<(String, TokenStream)>, pub transforms: Vec, } @@ -84,7 +84,24 @@ impl CommonAttrs { }, "example" => { - self.examples.extend(parse_name_value_lit_str(meta, cx)); + if let Ok(expr) = parse_name_value_expr(meta, cx) { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = &expr + { + if lit_str.parse::().is_ok() { + let lit_str_value = lit_str.value(); + cx.error_spanned_by(&expr, format_args!( + "`example` value must be an expression, and string literals that may be interpreted as function paths are currently disallowed to avoid migration errors \ + (this restriction may be relaxed in a future version of schemars).\n\ + If you want to use the result of a function, use `#[schemars(example = {lit_str_value}())]`.\n\ + Or to use the string literal value, use `#[schemars(example = &\"{lit_str_value}\")]`.")); + } + } + + self.examples.push(expr); + } } "extend" => { @@ -114,7 +131,7 @@ impl CommonAttrs { cx.error_spanned_by( &expr, format_args!( - "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `[schemars(transform = {})]`?", + "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `#[schemars(transform = {})]`?", lit_str.value() ), ) @@ -178,7 +195,7 @@ impl CommonAttrs { if !self.examples.is_empty() { let examples = self.examples.iter().map(|eg| { quote! { - schemars::_private::serde_json::value::to_value(#eg()) + schemars::_private::serde_json::value::to_value(#eg) } }); mutators.push(quote! {