Skip to content

Commit

Permalink
Allow example attribute value to be any expression (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
GREsau authored Nov 20, 2024
1 parent a479e6c commit e516881
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 21 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions docs/_includes/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,15 @@ Set on a container, variant or field to set the generated schema's `title` and/o

<h3 id="example">

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

</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">

Expand Down
12 changes: 3 additions & 9 deletions schemars/tests/integration/examples.rs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"null"
],
"examples": [
null
null,
"foo"
]
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"null"
],
"examples": [
null
null,
"foo"
]
}
},
Expand Down
9 changes: 9 additions & 0 deletions schemars/tests/ui/example_fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use schemars::JsonSchema;

#[derive(JsonSchema)]
#[schemars(example = "my_fn")]
pub struct Struct;

fn my_fn() {}

fn main() {}
7 changes: 7 additions & 0 deletions schemars/tests/ui/example_fn.stderr
Original file line number Diff line number Diff line change
@@ -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")]
| ^^^^^^^
2 changes: 1 addition & 1 deletion schemars/tests/ui/transform_str.stderr
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
25 changes: 21 additions & 4 deletions schemars_derive/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct CommonAttrs {
pub deprecated: bool,
pub title: Option<Expr>,
pub description: Option<Expr>,
pub examples: Vec<Path>,
pub examples: Vec<Expr>,
pub extensions: Vec<(String, TokenStream)>,
pub transforms: Vec<Expr>,
}
Expand Down Expand Up @@ -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::<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" => {
Expand Down Expand Up @@ -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()
),
)
Expand Down Expand Up @@ -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! {
Expand Down

0 comments on commit e516881

Please sign in to comment.