-
Notifications
You must be signed in to change notification settings - Fork 423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🔬 Make interfaces great again! #682
Conversation
@LegNeato @jmpunkt this, at the moment, isn't even a WIP, but rather a searching for a suitable design and a proof-of-concept manual GraphQLType implementation. I'm feeling being gone too far with it, that's why I just need to share with you my results and discuss the directions... Interfaces appeared to be much more tough for implementation than previous work. Really, unions were quite easy. The main pain point about interfaces (applicable to traits only), is that we don't know all the downstream types we want to resolve into at the point when we're implementing
I've implemented a PoC implementation here, but it works only a half-way (you can use I has turned out, that challenge 1 is not too hard. I've decoupled But, I've kinda stuck on the challenge 2.
|
Thanks for doing this! I've been thinking about this a lot and landed on something very similar. I think we need some sort of side channel to pass information at compile time to not have a central registry. I believe a combination of naming convention, trait bounds, dynamic dispatch, and static functions could be put together in some way to build that side channel, but I have yet to come up with it. |
@LegNeato @jmpunkt meh... after playing/poking with it much more, I can conclude that challenge 2 is impossible to be resolved at the current state of Rust and its ecosystem. Even if we've found a way to deal with that generic So, the library user will have to enumerate all the interface's implementors on the interface declaration. I'll try to do it as much ergonomic as possible. I'll update the design in description shortly. Regarding the object safety, I'll decouple those changes into a separate PR, to not pollute this one with a quite big side-refactoring. |
https://github.com/dtolnay/inventory looks pretty sweet. How about we do one method for wasm (without ctor stuff and needing a central listing) and one for non-wasm? |
Just thinking out loud, I wonder...is there some way we can store similar info to inventory? The limitation with it is it requires runtime support for ctors (which wasm doesn't have). But we don't need runtime, we need to aggregate all implementors at compile-time and output code that is than compiled and run (and can be simple enums). Hmmm |
I know it was suggested before and it is gross, but reading/writing data to |
But then we are back to incremental compilation not working. Lame. |
More post-beer musings. What about using naming convention as a pre-determined sidechannel? Something like (massively simplified): // #[juniper::graphql_interface]
trait Rideable {
fn num_wheels() -> u8;
}
// Generated
trait crate::GraphQLInterfaceRideable: Rideable {
fn as_graphql_rideable(&self) -> &Self {
&self
}
}
struct Bicycle;
// #[juniper::graphql_interface]
impl Rideable for Bicycle {
fn num_wheels() -> u8 {
2
}
}
// Generated
impl crate::GraphQLInterfaceRideable for Bicycle {
fn as_graphql_rideable(&self) -> &Self {
&self
}
}
// Juniper code assumes `crate::GraphQLInterfaceRideable` is implemented for any trait named "Rideable" outside of the type system. So basically inverting control...the user's code would define a trait like Anyway, pretty sure we run in some of the same problems above...but if we control both sides I feel like we should be able to do this is some way. |
# Conflicts: # juniper/Cargo.toml
@LegNeato returning to this...
Yeap, I guess we can hide such thing behind a feature flag. But, I'd rather do it as a separate PR on top of this one, which is going to contain only the base "works everywhere" implementation.
However,
That requires further investigation. Maybe there will be some way with invalidation. Can't say for sure atm.
Maybe I haven't understood your idea completely, but it seems to me that this only shifts the problem to another point (another interface declaration), rather than solves it. The initial problem is that we should enumerate all the trait implementors statically, without specifying all of them manually in the definition. Those implementors potentially may live even in separate crates. I don't know the way to register such distributed implementors except the "distributed plugin registration" provided by |
Update! Code polishing and code documentation is done! Only left:
|
Let's land this and play around with it? I'm worried about having to rebase while we iterate and this is materially better than what we currently have. Thanks again for all your work! |
@LegNeato that hasn't been finished yet! 😱 |
Fixed Book in b1a0366 |
Amazing work @tyranron! 🎊 |
Part of #295
Fixes #544, #605, #658, #748
Related to #421, #2
...or not so great? 🤔
Motivation
We want GraphQL interfaces which just feel right like usual ones in many PLs.
They should be able to work with an open set of types without requiring a user to specify all the concrete types an interface can downcasts into on the interface side.We're unable to do that, at the moment 😕
But we still could have a nice proc macros as convenient as they can be 🙃
New design
Traits
The nearest analogue of GraphQL interfaces are, definitely, traits. However, they are a little different beasts than may seem at first glance. The main difference goes that in GraphQL interface type serves both as an abstraction and a value dispatching to concrete implementers, while in Rust, a trait is an abstraction only and you need a separate type to dispatch to a concrete implemeter, like enum or trait object, because trait is not even a type itself. This difference imposes a lot of unobvious corner cases when we try to express GraphQL interfaces in Rust. This PR tries to implement a reasonable middleground.
So, poking with GraphQL interfaces will require two parts:
Declaring an abstraction is almost as simple as declaring trait:
And then, you need to operate in a code with a Rust implementing this GraphQL interface's value:
Trait objects
By default, an enum dispatch is used, because we should enumerate all the implementers anyway.
However, a dyn dispatch via trait is also possible to be used. As this requires a llitle bit more code transformation and aiding during macros expansion, we shoud specify it explicitly both on the interface and the implementers:
Checklist
derive(GraphQLObject)
/graphql_object
macros:implements
attribute's argument to enumerate all traits this object implements.#[graphql_interface(dyn)]
):trait
transformation:AsGraphQLValue
in a object-safe manner;#[graphql_interface(for = [Type1, Type2])]
attribute;#[graphql_interface(name = "Type")]
and#[graphql_interface(description = "A Type desc.")]
attributes;#[graphql_interface(deprecated = "Text")]
attributes;#[graphql_interface(context = Type)]
attribute;Context
argument and allow to mark it with#[graphq_interface(context)]
attribute;#[graphq_interface(executor)]
attribute;ScalarValue
type#[graphql_interface(scalar = Type)]
attribute;ScalarValue
type patameter;#[graphql_interface(ignore)]
attribute;#[graphql_interface(downcast)]
attribute;Context
in methods;#[graphql_interface(on Type = downcast_fn)]
attribute;support trivial deriving via(will be implemented in a separate PR)#[graphql_interface(derive(Type))]
attribute.impl
transformation:support custom context type with(not required)#[graphql_interface(context = Type)]
attribute;ScalarValue
type#[graphql_interface(scalar = Type)]
attribute;ScalarValue
type patameter.#[graphql_interface(enum)]
):enum
type of all implementers;From
impls to thisenum
for each implementer;enum
.