Skip to content

Commit fe3dfaa

Browse files
authored
Support @oneOf input objects (#1354, #1062, #1055, #1347, graphql/graphql-spec#825)
- support `enum`s in `#[derive(GraphQLInputObject)]` macro - add `@oneOf` built-in directive - add `schema::meta::InputObjectMeta::is_one_of` field Additionally: - support `#[graphql(rename_all = "snake_case")]` attribute in macros - remove unnecessary `lib.rs` from output when generating Book
1 parent 7725a27 commit fe3dfaa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1847
-581
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ endif
138138

139139
book.build:
140140
mdbook build book/ $(if $(call eq,$(out),),,-d $(out))
141+
rm -rf $(or $(out),book/_rendered)/lib.rs
141142

142143

143144
# Spellcheck Book.

book/src/types/enums.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ enum Episode {
7070
#
7171
# fn main() {}
7272
```
73-
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
73+
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming).
7474
7575

7676
### Documentation and deprecation

book/src/types/input_objects.md

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ In [Juniper], defining a [GraphQL input object][0] is quite straightforward and
1414
#[derive(GraphQLInputObject)]
1515
struct Coordinate {
1616
latitude: f64,
17-
longitude: f64
17+
longitude: f64,
1818
}
1919

2020
struct Root;
@@ -32,27 +32,48 @@ impl Root {
3232
# fn main() {}
3333
```
3434

35+
[`@oneOf`] [input objects][0] could be defined by using the [`#[derive(GraphQLInputObject)]` attribute][2] on a [Rust enum][enum]:
36+
```rust
37+
# #![expect(unused_variables, reason = "example")]
38+
# extern crate juniper;
39+
# use juniper::{GraphQLInputObject, ID};
40+
#
41+
#[derive(GraphQLInputObject)]
42+
enum UserBy {
43+
Id(ID), // Every `enum` variant declares a `Null`able input object field,
44+
Name(String), // so there is no need to use `Option<String>` explicitly.
45+
}
46+
#
47+
# fn main() {}
48+
```
49+
3550

3651
### Renaming
3752

38-
Just as with [defining GraphQL objects](objects/index.md#renaming), by default [struct] fields are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
53+
Just as with [defining GraphQL objects](objects/index.md#renaming), by default [struct] fields (or [enum] variants) are converted from [Rust]'s standard naming convention into [GraphQL]'s `camelCase` convention:
3954
```rust
4055
# extern crate juniper;
41-
# use juniper::GraphQLInputObject;
56+
# use juniper::{GraphQLInputObject, ID};
4257
#
4358
#[derive(GraphQLInputObject)]
4459
struct Person {
4560
first_name: String, // exposed as `firstName` in GraphQL schema
4661
last_name: String, // exposed as `lastName` in GraphQL schema
4762
}
63+
64+
#[derive(GraphQLInputObject)]
65+
enum UserBy {
66+
Id(ID), // exposed as `id` in GraphQL schema
67+
Name(String), // exposed as `name` in GraphQL schema
68+
}
4869
#
4970
# fn main() {}
5071
```
5172

5273
We can override the name by using the `#[graphql(name = "...")]` attribute:
5374
```rust
5475
# extern crate juniper;
55-
# use juniper::GraphQLInputObject;
76+
# use juniper::{GraphQLInputObject, ID};
5677
#
5778
#[derive(GraphQLInputObject)]
5879
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
@@ -62,14 +83,22 @@ struct Person {
6283
#[graphql(name = "websiteURL")]
6384
website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
6485
}
86+
87+
#[derive(GraphQLInputObject)]
88+
#[graphql(name = "By")] // now exposed as `By` in GraphQL schema
89+
enum UserBy {
90+
#[graphql(name = "ID")]
91+
Id(ID), // now exposed as `ID` in GraphQL schema
92+
Name(String),
93+
}
6594
#
6695
# fn main() {}
6796
```
6897

6998
Or provide a different renaming policy for all the [struct] fields:
7099
```rust
71100
# extern crate juniper;
72-
# use juniper::GraphQLInputObject;
101+
# use juniper::{GraphQLInputObject, ID};
73102
#
74103
#[derive(GraphQLInputObject)]
75104
#[graphql(rename_all = "none")] // disables any renaming
@@ -78,18 +107,25 @@ struct Person {
78107
age: i32,
79108
website_url: Option<String>, // exposed as `website_url` in GraphQL schema
80109
}
110+
111+
#[derive(GraphQLInputObject)]
112+
#[graphql(rename_all = "none")] // disables any renaming
113+
enum UserBy {
114+
Id(ID), // exposed as `Id` in GraphQL schema
115+
Name(String), // exposed as `Name` in GraphQL schema
116+
}
81117
#
82118
# fn main() {}
83119
```
84-
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
120+
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming).
85121
86122

87123
### Documentation and deprecation
88124

89125
Similarly, [GraphQL input fields][1] may also be [documented][7] and [deprecated][9] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
90126
```rust
91127
# extern crate juniper;
92-
# use juniper::GraphQLInputObject;
128+
# use juniper::{GraphQLInputObject, ID};
93129
#
94130
/// This doc comment is visible only in Rust API docs.
95131
#[derive(GraphQLInputObject)]
@@ -112,6 +148,28 @@ struct Person {
112148
#[deprecated]
113149
another: Option<f64>, // has no description in GraphQL schema
114150
}
151+
152+
/// This doc comment is visible only in Rust API docs.
153+
#[derive(GraphQLInputObject)]
154+
#[graphql(description = "This description is visible only in GraphQL schema.")]
155+
enum UserBy {
156+
/// This doc comment is visible only in Rust API docs.
157+
#[graphql(desc = "This description is visible only in GraphQL schema.")]
158+
// ^^^^ shortcut for a `description` argument
159+
Id(ID),
160+
161+
/// This doc comment is visible in both Rust API docs and GraphQL schema
162+
/// descriptions.
163+
// `enum` variants represent `Null`able input fields already, so can be naturally
164+
// deprecated without any default values.
165+
#[graphql(deprecated = "Just because.")]
166+
Name(String),
167+
168+
// If no explicit deprecation reason is provided,
169+
// then the default "No longer supported" one is used.
170+
#[deprecated]
171+
Bio(String), // has no description in GraphQL schema
172+
}
115173
#
116174
# fn main() {}
117175
```
@@ -120,11 +178,11 @@ struct Person {
120178

121179
### Ignoring
122180

123-
By default, all [struct] fields are included into the generated [GraphQL input object][0] type. To prevent inclusion of a specific field annotate it with the `#[graphql(ignore)]` attribute:
181+
By default, all [struct] fields (or [enum] variants) are included into the generated [GraphQL input object][0] type. To prevent inclusion of a specific field/variant annotate it with the `#[graphql(ignore)]` attribute:
124182
> **WARNING**: Ignored fields must either implement `Default` or be annotated with the `#[graphql(default = <expression>)]` argument.
125183
```rust
126184
# extern crate juniper;
127-
# use juniper::GraphQLInputObject;
185+
# use juniper::{GraphQLInputObject, ID};
128186
#
129187
enum System {
130188
Cartesian,
@@ -146,6 +204,15 @@ struct Point2D {
146204
// ^^^^ alternative naming, up to your preference
147205
shift: f64,
148206
}
207+
208+
#[derive(GraphQLInputObject)]
209+
enum UserBy {
210+
Id(ID),
211+
// Ignored `enum` variants naturally doesn't require `Default` implementation or
212+
// `default` value being specified, as they're just never constructed from an input.
213+
#[graphql(ignore)]
214+
Name(String),
215+
}
149216
#
150217
# fn main() {}
151218
```
@@ -154,6 +221,9 @@ struct Point2D {
154221
155222

156223

224+
225+
[`@oneOf`]: https://spec.graphql.org/September2025#sec--oneOf
226+
[enum]: https://doc.rust-lang.org/stable/reference/items/enumerations.html
157227
[GraphQL]: https://graphql.org
158228
[Juniper]: https://docs.rs/juniper
159229
[Rust]: https://www.rust-lang.org

book/src/types/interfaces.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ trait Person {
312312
#
313313
# fn main() {}
314314
```
315-
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
315+
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming).
316316
317317

318318
### Documentation and deprecation

book/src/types/objects/complex_fields.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ impl Person {
111111
#
112112
# fn main() {}
113113
```
114-
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
114+
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming).
115115
116116

117117
### Documentation and deprecation

book/src/types/objects/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ struct Person {
123123
#
124124
# fn main() {}
125125
```
126-
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
126+
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming).
127127
128128

129129
### Deprecation

juniper/CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
2424
### Added
2525

2626
- [September 2025] GraphQL spec: ([#1347])
27-
- `__Type.isOneOf` field. ([#1348], [graphql/graphql-spec#825])
27+
- `@oneOf` input objects: ([#1354], [#1062], [#1055], [graphql/graphql-spec#825])
28+
- `@oneOf` built-in directive.
29+
- `__Type.isOneOf` field. ([#1348])
30+
- `schema::meta::InputObjectMeta::is_one_of` field.
31+
- `enum`s support to `#[derive(GraphQLInputObject)]` macro.
2832
- `SCHEMA`, `OBJECT`, `ARGUMENT_DEFINITION`, `INTERFACE`, `UNION`, `ENUM`, `INPUT_OBJECT` and `INPUT_FIELD_DEFINITION` values to `__DirectiveLocation` enum. ([#1348])
2933
- Arguments and input object fields deprecation: ([#1348], [#864], [graphql/graphql-spec#525], [graphql/graphql-spec#805])
3034
- Placing `#[graphql(deprecated)]` and `#[deprecated]` attributes on struct fields in `#[derive(GraphQLInputObject)]` macro.
@@ -37,6 +41,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
3741
- Full Unicode range support. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687])
3842
- Support parsing descriptions on operations, fragments and variable definitions. ([#1349], [graphql/graphql-spec#1170])
3943
- Support for [block strings][0180-1]. ([#1349])
44+
- Support of `#[graphql(rename_all = "snake_case")]` attribute in macros. ([#1354])
4045

4146
### Changed
4247

@@ -50,10 +55,13 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
5055
- Incorrect double escaping in `ScalarToken::String` `Display`ing. ([#1349])
5156

5257
[#864]: /../../issues/864
58+
[#1055]: /../../issues/1055
59+
[#1062]: /../../issues/1062
5360
[#1347]: /../../issues/1347
5461
[#1348]: /../../pull/1348
5562
[#1349]: /../../pull/1349
5663
[#1353]: /../../pull/1353
64+
[#1354]: /../../pull/1354
5765
[graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525
5866
[graphql/graphql-spec#687]: https://github.com/graphql/graphql-spec/issues/687
5967
[graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805

juniper/src/schema/meta.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ pub struct InputObjectMeta<S> {
348348
pub description: Option<ArcStr>,
349349
#[doc(hidden)]
350350
pub input_fields: Vec<Argument<S>>,
351+
#[doc(hidden)]
352+
pub is_one_of: bool,
351353
#[debug(ignore)]
352354
pub(crate) try_parse_fn: InputValueParseFn<S>,
353355
}
@@ -364,6 +366,7 @@ impl<S> InputObjectMeta<S> {
364366
name: name.into(),
365367
description: None,
366368
input_fields: input_fields.to_vec(),
369+
is_one_of: false,
367370
try_parse_fn: try_parse_fn::<S, T>,
368371
}
369372
}
@@ -377,6 +380,15 @@ impl<S> InputObjectMeta<S> {
377380
self
378381
}
379382

383+
/// Marks this [`InputObjectMeta`] type as [`@oneOf`].
384+
///
385+
/// [`@oneOf`]: https://spec.graphql.org/September2025#sec--oneOf
386+
#[must_use]
387+
pub fn one_of(mut self) -> Self {
388+
self.is_one_of = true;
389+
self
390+
}
391+
380392
/// Wraps this [`InputObjectMeta`] type into a generic [`MetaType`].
381393
pub fn into_meta(self) -> MetaType<S> {
382394
MetaType::InputObject(self)

juniper/src/schema/model.rs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -250,14 +250,16 @@ impl<S> SchemaType<S> {
250250

251251
registry.get_type::<SchemaType<S>>(&());
252252

253+
let deprecated_directive = DirectiveType::new_deprecated(&mut registry);
253254
let include_directive = DirectiveType::new_include(&mut registry);
255+
let one_of_directive = DirectiveType::new_one_of();
254256
let skip_directive = DirectiveType::new_skip(&mut registry);
255-
let deprecated_directive = DirectiveType::new_deprecated(&mut registry);
256257
let specified_by_directive = DirectiveType::new_specified_by(&mut registry);
257258
directives.insert(include_directive.name.clone(), include_directive);
258259
directives.insert(skip_directive.name.clone(), skip_directive);
259260
directives.insert(deprecated_directive.name.clone(), deprecated_directive);
260261
directives.insert(specified_by_directive.name.clone(), specified_by_directive);
262+
directives.insert(one_of_directive.name.clone(), one_of_directive);
261263

262264
let mut meta_fields = vec![
263265
registry.field::<SchemaType<S>>(arcstr::literal!("__schema"), &()),
@@ -585,28 +587,33 @@ impl<S> DirectiveType<S> {
585587
}
586588
}
587589

588-
fn new_include(registry: &mut Registry<S>) -> Self
590+
fn new_deprecated(registry: &mut Registry<S>) -> Self
589591
where
590592
S: ScalarValue,
591593
{
592594
Self::new(
593-
arcstr::literal!("include"),
595+
arcstr::literal!("deprecated"),
594596
&[
595-
DirectiveLocation::Field,
596-
DirectiveLocation::FragmentSpread,
597-
DirectiveLocation::InlineFragment,
597+
DirectiveLocation::FieldDefinition,
598+
DirectiveLocation::ArgumentDefinition,
599+
DirectiveLocation::InputFieldDefinition,
600+
DirectiveLocation::EnumValue,
598601
],
599-
&[registry.arg::<bool>(arcstr::literal!("if"), &())],
602+
&[registry.arg_with_default::<String>(
603+
arcstr::literal!("reason"),
604+
&"No longer supported".into(),
605+
&(),
606+
)],
600607
false,
601608
)
602609
}
603610

604-
fn new_skip(registry: &mut Registry<S>) -> Self
611+
fn new_include(registry: &mut Registry<S>) -> Self
605612
where
606613
S: ScalarValue,
607614
{
608615
Self::new(
609-
arcstr::literal!("skip"),
616+
arcstr::literal!("include"),
610617
&[
611618
DirectiveLocation::Field,
612619
DirectiveLocation::FragmentSpread,
@@ -617,23 +624,30 @@ impl<S> DirectiveType<S> {
617624
)
618625
}
619626

620-
fn new_deprecated(registry: &mut Registry<S>) -> Self
627+
fn new_one_of() -> Self
621628
where
622629
S: ScalarValue,
623630
{
624631
Self::new(
625-
arcstr::literal!("deprecated"),
632+
arcstr::literal!("oneOf"),
633+
&[DirectiveLocation::InputObject],
634+
&[],
635+
false,
636+
)
637+
}
638+
639+
fn new_skip(registry: &mut Registry<S>) -> Self
640+
where
641+
S: ScalarValue,
642+
{
643+
Self::new(
644+
arcstr::literal!("skip"),
626645
&[
627-
DirectiveLocation::FieldDefinition,
628-
DirectiveLocation::ArgumentDefinition,
629-
DirectiveLocation::InputFieldDefinition,
630-
DirectiveLocation::EnumValue,
646+
DirectiveLocation::Field,
647+
DirectiveLocation::FragmentSpread,
648+
DirectiveLocation::InlineFragment,
631649
],
632-
&[registry.arg_with_default::<String>(
633-
arcstr::literal!("reason"),
634-
&"No longer supported".into(),
635-
&(),
636-
)],
650+
&[registry.arg::<bool>(arcstr::literal!("if"), &())],
637651
false,
638652
)
639653
}

0 commit comments

Comments
 (0)