-
Notifications
You must be signed in to change notification settings - Fork 426
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
Automatic getters for #[graphql_object]
#553
Comments
Could have sworn we had an enhancement request for this alrteady but can't find it. I agree this would be very nice to have. As a workaround you can make your own macro or derive that spits out a |
@LegNeato Thanks for the quick reply! I quickly went through all of the open items before posting just because I was really expecting to see this issue :) I hope I didn't miss anything and if I did I'm sorry. I will have a look at how to achieve that with my own macro for now. Would it be useful to leave this issue open and see if anyone else may want this feature? |
Yep! |
I tried looking into rolling my own macro for generating the necessary fields and found a few things that I wanted to share in case you or someone else has an idea for how to best approach this. The problem I ran into was that I could not solve this problem with a derive macro. In order to have since derive macros only apply to either the @LegNeato The PR is just to get an idea of whether you and the other juniper maintainers would be open to this direction. |
I had an idea how to just make Here is the general design:
The two traits would each specify similar methods as the trait GraphQLImpl : GraphQLData {
...
} |
The downside of the design in my comment above is that struct Y { y: usize } // don't want to expose Y::y
impl GraphQLData for MyType {} to satisfy the condition and to hide all the members in |
Another possibility is to use handler functions. The user defines structures which contain data and handler fields. For each data fields code is generated. A Handler field references a function, similar to Serde. However, this could require to remove non-async code first, because we can not distinguish between async and non-async functions. #[derive(juniper::GraphQLObject)]
struct Foo {
name: String
#[graphql(Handler = foo::computed)]
computed: String,
}
mod foo {
fn computed(&self) -> String {
...
}
} Maybe this approach fits better, but I am not sure. |
@jmpunkt This sounds great as well! The design space for this problem seems bountiful.
I think if we can tolerate a bit more typing, we can specify when a handler is synchronous. The default could be async: #[derive(juniper::GraphQLObject)]
struct Foo {
name: String
#[graphql(Handler = foo::computed, synchronous = true)]
computed: String,
#[graphql(Handler = foo::requested)]
requested: String,
}
mod foo {
fn computed(&self) -> String {
...
}
async fn requested(&self) -> String {
...
}
} |
A current implementation which compiles (nothing tested) looks like the following: #[derive(GraphQLObject, Debug, PartialEq)]
pub struct HandlerFieldObj {
regular_field: bool,
#[graphql(Handler = handler_field_obj::skipped, noasync)]
skipped: i32,
#[graphql(Handler = handler_field_obj::skipped_result, noasync)]
skipped_result: i32,
#[graphql(Handler = handler_field_obj::skipped_async)]
skipped_async: i32,
#[graphql(Handler = handler_field_obj::skipped_async_result)]
skipped_async_result: i32,
}
mod handler_field_obj {
pub fn skipped(obj: &super::HandlerFieldObj, ctx: &()) -> i32 {
0
}
pub fn skipped_result(obj: &super::HandlerFieldObj, ctx: &()) -> juniper::FieldResult<i32> {
Ok(0)
}
pub async fn skipped_async(obj: &super::HandlerFieldObj, ctx: &()) -> i32 {
0
}
pub async fn skipped_async_result(
obj: &super::HandlerFieldObj,
ctx: &(),
) -> juniper::FieldResult<i32> {
Ok(0)
}
} A main different to the examples before, is that it is required to have a context and to use the type of the object rather than |
@jmpunkt I'm impressed and surprised. Was this possible all along? I thought I went over the book with a comb and couldn't find the graphql attribute macro on struct fields taking a Handler. Is this documented somewhere and I totally missed it? |
No it is not possible with the code on the master branch. Due to the structure of Juniper, it does not require that much changes to support this behavior. The sentence
refers to my testing branch. |
I am slightly disagree the proposed approach above. For instance, I am using #[derive(Debug, Clone, PartialEq, Queryable, Identifiable)]
pub struct User {
pub id: i32,
pub first_name: String,
pub last_name: String,
// pub full_name: String,
}
#[juniper::graphql_object]
impl User {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
} In the above example, I won't want to put the |
I did not think of that. Since the original idea was to reduce the boilerplate, what about this? #[derive(Debug, Clone, PartialEq, Queryable, Identifiable)]
pub struct User {
pub id: i32,
pub first_name: String,
pub last_name: String,
}
#[juniper::graphql_object(id: i32, first_name: String, last_name: String)]
impl User {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
} We define trivial fields inside graphql_object. The macro provides trivial implementations for the specified fields. |
Explicitly defining trivial fields also works. Or I am not sure if it's possible that the macro provides trivial implementations for all fields in the User struct by default (like I had been using type-graphql with TypeScript, which has very similar experience like this. It would be awesome if we can combine with this feature request (#647). |
The overall problem with this feature is that a macro can only see the underlying AST, e.g., impl macros can only see the impl block. There is now way for the |
My last statement is maybe not 100% true. We could get information from structs if we generate code which provide these information. We define a trait Notice that #[derive(juniper::GraphQLObjectInfo)]
struct Obj {
field: String,
} impl juniper::JuniperObjectInfo for Obj {
fn fields() -> &'static [Field] {
&[ /* generated by macro */ ]
}
} #[juniper::graphql_object]
#[grapqhl(partial)]
impl User {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
} Not sure if this works. |
@Victor-Savu Thanks! I did had a look the PR, its indeed a very nice idea! But I think it requires a bit too much changes on the current API. I do like the current way of defining field resolvers with I think the idea of having a We can even make the changes compatible with the current behaviour if #[juniper::graphql_object]
#[grapqhl(auto)]
impl User {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
} Gonna work on a P.O.C with my very limited rust knowledge to see if its possible. Cheers! |
Looks like the approach indeed works!! Here is my POC. One can opt-in this feature by using #[derive(GraphQLObjectInfo, Debug, PartialEq)] // <-- here
#[graphql(scalar = DefaultScalarValue)]
struct Obj {
regular_field: bool,
#[graphql(
name = "renamedField",
description = "descr",
deprecated = "field deprecation"
)]
c: i32,
}
#[juniper::graphql_object(
name = "MyObj",
description = "obj descr",
scalar = DefaultScalarValue,
derive_fields // <-- here
)]
impl Obj {
fn custom_field(&self) -> &str {
"custom field"
}
} The derived impl juniper::GraphQLTypeInfo<DefaultScalarValue> for Obj {
type Context = ();
type TypeInfo = ();
fn fields<'r>(
info: &Self::TypeInfo,
registry: &mut juniper::Registry<'r, DefaultScalarValue>,
) -> Vec<juniper::meta::Field<'r, DefaultScalarValue>>
where
DefaultScalarValue: 'r,
{
<[_]>::into_vec(box [
registry.field_convert::<bool, _, Self::Context>("regularField", info),
registry
.field_convert::<i32, _, Self::Context>("renamedField", info)
.description("descr")
.deprecated(Some("field deprecation")),
])
}
#[allow(unused_variables)]
#[allow(unused_mut)]
fn resolve_field(
self: &Self,
_info: &(),
field: &str,
args: &juniper::Arguments<DefaultScalarValue>,
executor: &juniper::Executor<Self::Context, DefaultScalarValue>,
) -> juniper::ExecutionResult<DefaultScalarValue> {
match field {
"regularField" => {
let res = (|| &self.regular_field)();
juniper::IntoResolvable::into(res, executor.context()).and_then(|res| match res
{
Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r),
None => Ok(juniper::Value::null()),
})
}
"renamedField" => {
let res = (|| &self.c)();
juniper::IntoResolvable::into(res, executor.context()).and_then(|res| match res
{
Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r),
None => Ok(juniper::Value::null()),
})
}
_ => {
{
::std::rt::begin_panic_fmt(&::core::fmt::Arguments::new_v1(
&["Field ", " not found on type "],
&match (
&field,
&<Self as juniper::GraphQLType<DefaultScalarValue>>::name(_info),
) {
(arg0, arg1) => [
::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt),
::core::fmt::ArgumentV1::new(arg1, ::core::fmt::Debug::fmt),
],
},
))
};
}
}
}
} And the generated impl juniper::GraphQLTypeAsync<DefaultScalarValue> for Obj
where
DefaultScalarValue: Send + Sync,
Self: Send + Sync,
{
fn resolve_field_async<'b>(
&'b self,
info: &'b Self::TypeInfo,
field: &'b str,
args: &'b juniper::Arguments<DefaultScalarValue>,
executor: &'b juniper::Executor<Self::Context, DefaultScalarValue>,
) -> juniper::BoxFuture<'b, juniper::ExecutionResult<DefaultScalarValue>>
where
DefaultScalarValue: Send + Sync,
{
use futures::future;
use juniper::GraphQLType;
match field {
"customField" => {
let res: &str = (|| "custom field")();
let res2 = juniper::IntoResolvable::into(res, executor.context());
let f = async move {
match res2 {
Ok(Some((ctx, r))) => {
let sub = executor.replaced_context(ctx);
sub.resolve_with_ctx_async(&(), &r).await
}
Ok(None) => Ok(juniper::Value::null()),
Err(e) => Err(e),
}
};
use futures::future;
future::FutureExt::boxed(f)
}
_ => {
let v = <Self as juniper::GraphQLTypeInfo<DefaultScalarValue>>::resolve_field(
self, info, field, args, executor,
);
future::FutureExt::boxed(future::ready(v))
}
}
}
} Feels like it requires a lot of changes, but most of them are really just minor refactoring to be able to reuse the existing functions. Please let me know hows my implementation and if there is any better approach. Thanks! |
Looks good. Currently it is not clear what happens on duplicate fields. I would assume that the registry (https://github.com/zhenwenc/juniper/blob/4cdf13396c8c68841d08bb8319cca4055c217980/juniper_codegen/src/util/mod.rs#L888) overrides if the field is already defined. In my opinion silently overriding is not the best approach. We should test upfront if there are possible duplicates and abort (hard error). The user would skip or remove the field in the |
Thanks @jmpunkt ! Yes, the derived field resolvers act as fallbacks, where user defined resolvers (in I will wrap up a proper PR with some tests. Cheers! |
I thought about the duplicate issue and I think there is no perfect solution. However, I had two ideas in my mind.
Maybe someone gets inspired and finds a better solution. |
@jmpunkt Thanks for the advice! Could you please review this PR #652 when available. At the moment I perform the duplicate fields check on the juniper/juniper_codegen/src/util/mod.rs Lines 911 to 918 in 2e5805f
which is a runtime check since I couldn't get the struct field information baked in the I agree the solution is not perfect though, keen to know if there is any better solution. Cheers! |
@jmpunkt @zhenwenc @Victor-Savu I was thinking about this issue too for quite a while, and here is something to share... I've thought about the following design: #[derive(GraphQLObject)] // <-- mandatory
#[graphql(
name = "MyObj",
description = "obj descr",
scalar = DefaultScalarValue,
)]
struct Obj {
regular_field: bool,
#[graphql(
name = "renamedField",
description = "descr",
deprecated = "field deprecation"
)]
c: i32,
#[graphql(ignore)] // or #[graphql(skip)] ?
hidden: bool,
}
#[graphql_object(scalar = DefaultScalarValue)] // <- optional
impl Obj {
fn custom_field(&self) -> &str {
"custom field"
}
fn hidden(&self) -> bool {
self.hidden
}
} So, the As we cannot access from This way, we can have even multiple As already has been described above in the discussion, the 2 questions arise here:
In addition, I think it would be nice to preserve |
Thanks for the input. I did not know that the Some notes on your other ideas
|
No. What I've meant is that macro expansion contains something like fn fields<'r>(
info: &Self::TypeInfo,
registry: &mut juniper::Registry<'r, DefaultScalarValue>,
) -> Vec<juniper::meta::Field<'r, DefaultScalarValue>>
where
DefaultScalarValue: 'r,
{
for field in inventory::iter::<ObjResolvers> {
// fill up registry with info
}
}
This way there won't be any compatibility problems, as The only incompatibility is that to codegen implementation you should always have
Hmmm.... I've thought about something like this: // Preserved code that has been fed into `#[graphql_object]` macro.
impl Obj {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
// Generated code.
mod gql_Obj_fullName {
use super::Obj;
fn resolve(obj: &Obj, /* unified parameters to call resolver */) -> Result</* unified possible result */> {
// extract required for call here
let res = Obj::full_name(obj);
// wrap into required result
}
}
// Generated code.
inventory::submit! {
__ObjResolver {
name: "fullName",
// and other metadata here...
resolve_fn: gql_Obj_fullName::resolve,
}
} This way the original code is untouched, while we generate all the required machinery around. |
Thanks for the clarifications and examples. Overall I really like the design since it is more "like" Rust. Not sure how well this works at the end.
I would like to know what @LegNeato thinks about the whole situation. A small side note. In terms of unification, there is a similar issue (#631 (comment)). The PR did not unify the interface since it would require a lot of changes in the tests. However, if we are planing to uniform Juniper, then we should enforce this. |
@jmpunkt I've read the discussion and I'm a bit more convinced to allow resolvers without Things like
are quite vital. And supporting them along seems not to be a big deal. I always prefer best possible user experience. |
@tyranron Thanks for your great ideas!
Sounds awesome! I didn't thought of that before.. if I understand correctly, we can generate a dummy Beside, I actually prefer enforcing |
This can be a struct too, but I see a bit more use in generating modules. See the second example
But what if field doesn't really requires
In my practice having But, if we want more distinction, I think there is a reason to consider the following variant, and drop #[derive(GraphQLObject)]
#[graphql(
name = "MyObj",
description = "obj descr",
scalar = DefaultScalarValue,
)]
struct Obj {
regular_field: bool,
#[graphql(
name = "renamedField",
description = "descr",
deprecated = "field deprecation"
)]
c: i32,
#[graphql(ignore)]
hidden: bool,
}
impl Obj {
#[graphql_field(scalar = DefaultScalarValue)]
const fn const_field() -> &str {
"const field"
}
#[graphql_field(scalar = DefaultScalarValue)]
fn hidden(&self) -> bool {
self.hidden
}
fn not_a_field(&self) {
panic!("I'm not a field!")
}
} |
I noticed that one use-case is not covered by this design. The "old" design was quite handy in situation where the struct is private. Instead of deriving the GraphQLType it was possible to "implement" it, thus we do not have to wrap the type. In situations where types come from external crates, GraphQLType can be implemented without changes in the external crate. The design allows some kind of layered implementations. For example, someone create a database model and provides two API's, one of them is GraphQL. The other API does not know about Juniper, thus does not require the implemented traits. Not sure how common this design is. Maybe Regarding the optional |
Yes. I'm even in favor of leaving the second variant only. To have a computable field, we mark a concrete method rather than an Hmmm... thinking about it a bit more shows a downside of this way, as we cannot know about impl Obj {
#[graphql_field(object = Obj, scalar = DefaultScalarValue)]
const fn const_field() -> &str {
"const field"
}
#[graphql_field(object = Obj, scalar = DefaultScalarValue)]
fn hidden(&self) -> bool {
self.hidden
}
fn not_a_field(&self) {
panic!("I'm not a field!")
}
} Which is not quite ergonomic. So, maybe the second variant really should be an optional one.
As I've written above, the described design has nothing to do with it and is fully orthogonal to such issues as the current design is. It doesn't disallow you manual implementations of |
In term of generating dummy mod test_1 {
#[derive(juniper::GraphQLObjectInfo)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct Object {
field: i32,
}
/// Generated by `GraphQLObjectInfo` derive macro:
struct __juniper_Object_field;
}
mod test_2 {
use super::test_1::Object;
#[juniper::graphql_object(scalar = juniper::DefaultScalarValue)]
impl Object {
fn field(&self) -> i32 {
10
}
}
/// Generated by `graphql_object` macro:
struct __juniper_Object_field;
} The above code will compile without any error, since the duplicated Beside, I am a bit conservative about using this approach. As a newbie to rust and as a normal user of the Juniper crate (without knowing its implementation details), I might be confused when seeing error message like this:
since these structs are generated by the library, instead of pointing me to the root caused of the issue. It would be way more useful if I see hints like this:
|
🤔... unfortunately, Rust's proc macros doesn't give any type level information, they work only with raw syntax tokens. So we're unable to deeply inspect the information about type outside current declaration, and seems to be that we're unable to ensure any uniqueness outside current declaration. So modules really don't work... meh 😞 The only way I see to resolve this, at the moment, is a not nice and bad hack, but it will work well and will allow to acomplish the described above:
|
I would not recommend generating files and hoping that the compiler executes the macros in the right order. Randomly breaking compilation is annoying. It looks like Rust can not do what we want to do (maybe in the future?). There are currently three different options to go from here
|
Nope, the whole idea is not rely on that. Using the file is a way to achieve that for uniqueness checks, but the downsides are:
However, investigating this a bit further, it seems that we don't need use files at all. A global static in proc macro crate will do the job, at least for a while. Need more investigation on a question... At the end, we can support compile-time checks behind a |
Ah ok you use files for collisions checks only. I read
wrong. Thanks for the clarification. Is there anything special (or limited) regarding the creation of files during the compilation. I am not familiar with the internals during compilation. Maybe this is a crazy idea, but if it is possible to auto-generate a separate crate then this crate contains our typing information. Instead putting the generate structs/modules in the current path we put them in a separate crate where we have control over the path. Maybe using build scripts to pre-process the source files? |
My original intention was introducing a small improvement to the existing However, in practice, I believe this kind of improvement is necessary as field resolvers for object types are so commonly used and critical in GraphQL (otherwise it doesn't seem like a graph..), that said Personally, I don't mind doing runtime check (option 3) as I don't see any truely downside of this approach, especially comparing to the tradeoff for the other approaches.. |
With juniper, you can either the derive or the proc macro, *not both*. See graphql-rust/juniper#553 for some discussion as to why.
With juniper, you can use either the derive or the proc macro, *not both*. See graphql-rust/juniper#553 for some discussion as to why.
Hi We can say that today we cannot have the introspection of the structure, to obtain the fields, along with the possibility of having custom fields? |
Is your feature request related to a problem? Please describe.
I'm always frustrated when I need to replace:
by
because I lose the nice default field accessor for
name
and I have to manually add it to theimpl
:Describe the solution you'd like
I'd like for
#[derive(juniper::GraphQLObject)]
and#[juniper::graphql_object]
to work together so I don't have to add the dummy accessors for every field in theimpl
block.I would imagine it is complicated to achieve this because both macros currently generate the same
impl
block underneath.Describe alternatives you've considered
I couldn't come up with any alternative to this 😿
Additional context
This is extra painful when wrapping REST interfaces which have many simple fields (like strings) and a few complex fields (references to other resources).
P.S.:
juniper
is such a great crate! many thanks to all the contributors! 💯The text was updated successfully, but these errors were encountered: