Allow example attribute value to be any expression (#354)

This commit is contained in:
Graham Esau 2024-11-20 22:18:31 +00:00 committed by GitHub
parent a479e6cc0e
commit e5168819a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 53 additions and 21 deletions

View file

@ -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) - 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 ### Fixed

View file

@ -298,13 +298,15 @@ Set on a container, variant or field to set the generated schema's `title` and/o
<h3 id="example"> <h3 id="example">
`#[schemars(example = "some::function")]` `#[schemars(example = value)]`
</h3> </h3>
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]))]`.
<h3 id="deprecated"> <h3 id="deprecated">

View file

@ -1,21 +1,15 @@
use crate::prelude::*; use crate::prelude::*;
#[derive(Default, JsonSchema, Serialize)] #[derive(Default, JsonSchema, Serialize)]
#[schemars(example = "Struct::default", example = "null")] #[schemars(example = Struct::default(), example = ())]
struct Struct { struct Struct {
#[schemars(example = "eight", example = "null")] #[schemars(example = 4 + 4, example = ())]
foo: i32, foo: i32,
bar: bool, bar: bool,
#[schemars(example = "null")] #[schemars(example = (), example = &"foo")]
baz: Option<&'static str>, baz: Option<&'static str>,
} }
fn eight() -> i32 {
8
}
fn null() {}
#[test] #[test]
fn examples() { fn examples() {
test!(Struct).assert_snapshot(); test!(Struct).assert_snapshot();

View file

@ -20,7 +20,8 @@
"null" "null"
], ],
"examples": [ "examples": [
null null,
"foo"
] ]
} }
}, },

View file

@ -20,7 +20,8 @@
"null" "null"
], ],
"examples": [ "examples": [
null null,
"foo"
] ]
} }
}, },

View file

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

View file

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

View file

@ -1,5 +1,5 @@
error: Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`. 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 --> tests/ui/transform_str.rs:4:24
| |
4 | #[schemars(transform = "x")] 4 | #[schemars(transform = "x")]

View file

@ -21,7 +21,7 @@ pub struct CommonAttrs {
pub deprecated: bool, pub deprecated: bool,
pub title: Option<Expr>, pub title: Option<Expr>,
pub description: Option<Expr>, pub description: Option<Expr>,
pub examples: Vec<Path>, pub examples: Vec<Expr>,
pub extensions: Vec<(String, TokenStream)>, pub extensions: Vec<(String, TokenStream)>,
pub transforms: Vec<Expr>, pub transforms: Vec<Expr>,
} }
@ -84,7 +84,24 @@ impl CommonAttrs {
}, },
"example" => { "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::<Path>().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" => { "extend" => {
@ -114,7 +131,7 @@ impl CommonAttrs {
cx.error_spanned_by( cx.error_spanned_by(
&expr, &expr,
format_args!( 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() lit_str.value()
), ),
) )
@ -178,7 +195,7 @@ impl CommonAttrs {
if !self.examples.is_empty() { if !self.examples.is_empty() {
let examples = self.examples.iter().map(|eg| { let examples = self.examples.iter().map(|eg| {
quote! { quote! {
schemars::_private::serde_json::value::to_value(#eg()) schemars::_private::serde_json::value::to_value(#eg)
} }
}); });
mutators.push(quote! { mutators.push(quote! {