-
Notifications
You must be signed in to change notification settings - Fork 429
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Codegen reimplementation for GraphQL unions #666
Conversation
- add marker::GraphQLUnion trait in 'juniper' Additionally: - re-export 'futures' crate in 'juniper' for convenient reuse in generated code without requiring library user to provide 'futures' crate by himself
And no... PR number doesn't have any subtle meanings 🙃 |
I'll take a closer look but this looks awesome! |
The How do these two attributes differ:
I've put two variants into example, to show that both are possible, and there is a duplication check for them. |
@LegNeato ok, I've stuck a bit with SynopsisIt turns out that the following is totally OK when using #[graphq_union]
#[graphql(name = "MyCharacter", desc = "Traited character")]
trait Character {
fn as_human(&self) -> Option<&Human>;
} The problem is that
We could have been define such attribute separately, but it raises 2 other major downsides:
I see the following possible solutions... 1. Use only
|
- support multiple #[graphql_union] attributes in non-ambiguous way - relax Sized requirement on GraphQLType
I've bootstrapped traits support basing on option 1. As an another side effect: The description is updated properly. |
Ugh, that's a bummer. Neither are ideal. Let me play around and see if I can come up with something. |
That issue seems to think they should be supported by the attribute proc macro and not throwing an error? |
Yup. If we could have something like proposed in rust-lang/rust#65823, the described above would be non-issue, and the initial design would be possible. At the moment I think the option 1 is quite enough. It's not ideal, but does the job relatively obviously without subtleties. The most regular form (90% cases) in the code will be something like that: #[graphq_union(
name = "Character",
description = "Traited character",
context = MyContext,
)]
trait Character {
fn as_human(&self) -> Option<&Human> { None }
fn as_droid(&self, _: &Context) -> Option<&Droid> { None }
} I think users will hit the necessity to use multiple attributes for traits quite rare, in practice. And, also, once Rust will land something like rust-lang/rust#65823 (or allow |
For me it is not clear what happens in the following case. #[derive(GraphQLUnion)]
pub enum Character {
One(Human),
Two(Droid),
#[graphql(ignore)]
Three(Three),
}
#[graphql_object]
impl Test {
fn test(&self) -> Character {
Character::Three(todo!()) // ignore the todo!()
}
} The resolver would fail and return an error? If Im wondering if it is possible to have a custom resolver with the signature |
At the moment it will panic. This is inherited from the existing implmentation, and, I'm afraid, is not possible to change without changing the the design of
It shouldn't. If you don't want to make it a part of enum, then you're not pushed to. The reason of having I can quite easily imagine the situation, when library user should deal with some enum or trait, where not all methods should be exposed in schema, but they need to be there to express some additional invariants or something. Making the user in such situation to roll out manual
The point is that this is "opt-in" action, not "opt-out". So to do this accidentially, the library user should explicitly put the
At the moment, no. But, in theory, we could have been support In this PR I'm trying to re-introduce capabilities of existing code generation in a better usage form. To extend those capabilities, I think we should go with a separate PR, as there are a lot questions to bikeshed and discuss. |
@LegNeato to sum up the current progress:
|
Awesome work, I'll take a look in-depth today. I too am a bit suspect about |
@LegNeato both As for other macros, I'm planning to refactor/rework them too in near future. So, at the end, with a new release they will be fully consistent, I swear 😉
Actually, I've provided that, because there is a use case for ignoring in our closed codebase. This usecase involves binding some type parameters, which are not really used, but required for comfortable type-level "magic". enum UserCreationResult<T> {
Ok(User),
Err(UserCreationError),
#[graphql(ignore)]
_State(PhatomData<T>),
} where |
Cool, makes sense. As long as we have a real usecase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the CI failures are real.
@LegNeato thanks for the corrections, and sorry for my bad English 🙃
Yup, tests aren't there yet. Working on it... |
While reading the documentation I found the behavior of the following example odd. #[derive(GraphQLUnion)]
#[graphql(Context = CustomContext)]
#[graphql(on Ewok = Character::ewok_from_context)]
enum Character {
Human(Human),
Droid(Droid),
}
impl Character {
fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> {
Some(&ctx.ewok)
}
} Makes sense that we have custom resolver. But in this case it is not possible to have #[derive(GraphQLUnion)]
#[graphql(Context = CustomContext)]
#[graphql(on Ewok = Character::ewok_from_context)]
enum Character {
Human(Human),
Droid(Droid),
Ewok,
}
impl Character {
fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> {
Some(&ctx.ewok)
}
} |
@jmpunkt thanks! Makes sense. I've changed the example to reflect that. |
@LegNeato @jmpunkt I've also added " |
Phew... almost there! 🎉 Existing tests are fixed and renewed. Now, it's only the matter of adding new tests. |
Additionally: - remove redundant PascalCasing on union name - refactor GraphQLScope::custom() usage - provide better error messages when codegen fails - refactor terminology 'custom resolver' -> 'external resolver function'
Additionally: - use unit type () as default for EmptyMutation and EmptySubscriptions
@LegNeato finally, done! Please review, and I'll squash-merge it once approved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some small tweaks but let's land this!
Fixes #549
As side effect, also fixes #663
Also, related to #295
Background and motivation
TL;DR: current codegen for GraphQL unions is not much flexible and is too magic.
#[derive(GraphQLUnion)]
macro:#[graphql]
attributes, just pickups first and silently ignores others.From
implementations, which leads to painful conflicts.#[graphql_union]
macro:GraphQLUnion
), and declaresimpl
blocks which don't exist after macro expansion too.We definitelly want less magic, applying custom behavior in an ergonomic and convenient way and operate with valid Rust code for better IDE experience.
New design
In a nutshell, it provides a single
#[derive(GraphQLUnion)]
macro which allows to modify resolution logic with custom functions. This is the same whatserde
does with#[derive(Serialize)]
macro and#[serde(with = "my_func")]
attribute.All the described above is applicable in the same manner to struct types too.
Also, for enums, the innerhood
From
implementations are removed. It's better to usedderive_more::From
for that purpose, which does stuff in much more clever way, and will preserve a bit more of SRP for our macro.However, to support traits, we still need to provide
#[graphql_union]
macro, asproc_macro_derive
cannot be placed on trait declarations. And to avoid collisions with#[graphql]
helper attribute (which will result in compilation error), we shoud use#[graphql_union]
even for methods.Also, this PR:
juniper::marker::GraphQLUnion
trait, to not reffer in the code to unexistent trait/types.Sized
requirement onGraphQLType
trait to allow implement it for bare trait objects.Features
#[attr(with = func_name)]
.#[graphql]
attributes are supported.Checklist
#[graphql]
attributes.futures
crate insidejuniper
with#[doc(hidden)]
for internal use in macros.GraphQLUnion
a real marker trait.Sized
requirement onGraphQLType
trait.#[derive(GraphQLUnion)]
):#[graphq(ignore)]
attribute;#[graphql(context = Type)]
attribute;ScalarValue
type#[graphql(scalar = Type)]
attribute;#[graphql(name = "Type")]
and#[graphql(description = "A Type desc.")]
attributes;#[grapqh(on Type = resolver_fn)]
attribute;#[grapqh(with = resolver_fn)]
attribute;#[grapqh(on Type = resolver_fn)]
and#[grapqh(with = resolver_fn)]
attributes for the same type;From
implementations for enum variants.#[derive(GraphQLUnion)]
):#[grapqh(on Type = resolver_fn)]
attribute;#[graphql(context = Type)]
attribute;ScalarValue
type#[graphql(scalar = Type)]
attribute;#[graphql(name = "Type")]
and#[graphql(description = "A Type desc.")]
attributes.#[graphql_union]
):Option<&Type>
as union variant resolvers;Context
in methods;#[graphql_union(on Type = resolver_fn)]
attribute;#[graphql_union(context = Type)]
attribute;ScalarValue
type#[graphql_union(scalar = Type)]
attribute;#[graphql_union(name = "Type")]
and#[graphql_union(description = "A Type desc.")]
attributes;#[graphql_union(ignore)]
attribute.