Skip to content

Automatic getters for #[graphql_object] #553

@Victor-Savu

Description

@Victor-Savu
Contributor

Is your feature request related to a problem? Please describe.
I'm always frustrated when I need to replace:

#[derive(juniper::GraphQLObject)]
struct Foo {
    name: String
}

by

struct Foo {
    name: String
}
#[juniper::graphql_object]
impl Foo {
    fn computed(&self) -> String {
        ...
    }
}

because I lose the nice default field accessor for name and I have to manually add it to the impl:

#[juniper::graphql_object]
impl Foo {
    ...
    fn name(&self) -> &String {
        &self.name
    }
}

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 the impl 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! 💯

Activity

LegNeato

LegNeato commented on Mar 9, 2020

@LegNeato
Member

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 #[juniper::graphql_object] for you with the boilerplate included.

Victor-Savu

Victor-Savu commented on Mar 9, 2020

@Victor-Savu
ContributorAuthor

@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?

LegNeato

LegNeato commented on Mar 9, 2020

@LegNeato
Member

Yep!

Victor-Savu

Victor-Savu commented on Mar 29, 2020

@Victor-Savu
ContributorAuthor

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 struct item or to the impl item so it would be impossible to get information about both without using a function-like procedural macro. I opened a draft PR #592 with an implementation that demonstrates the results.

@LegNeato The PR is just to get an idea of whether you and the other juniper maintainers would be open to this direction.

Victor-Savu

Victor-Savu commented on Apr 11, 2020

@Victor-Savu
ContributorAuthor

I had an idea how to just make #[derive(GraphQLObject)] and #[graphql_object]) work together, but the implementation depends on rust-lang/rfcs#1210 (or rather on #![feature(specialization)] in nightly).

Here is the general design:
We can have two intermediate traits:

  • GraphQLData derrived from the struct/enum by #[derive(GraphQLObject)]; and
  • GraphQLImpl the implementation of which is generated from the impl block by #[graphql_object].

The two traits would each specify similar methods as the GraphQLType. We can then have a default implementations of GraphQLType based on objects that implement GraphQLData and a specialization for those that implement GraphQLImpl. For coherence reasons, we need GraphQLImpl to require GraphQLData:

trait GraphQLImpl : GraphQLData {
...
}

Here is a mock implementation in the rust playground.

Victor-Savu

Victor-Savu commented on Apr 11, 2020

@Victor-Savu
ContributorAuthor

The downside of the design in my comment above is that impl GraphQLData becomes necessary even for cases where users don't want to expose some (or any) of the fields in a struct. Also, it is needed for rust enums as well. A hopefully convenient way to solve this is to provide default implementations for the methods in GraphQLData such that the user would only need to write:

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 MyType. This option is shown in the playground example as well.

jmpunkt

jmpunkt commented on Apr 14, 2020

@jmpunkt
Contributor

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.

Victor-Savu

Victor-Savu commented on Apr 16, 2020

@Victor-Savu
ContributorAuthor

@jmpunkt This sounds great as well! The design space for this problem seems bountiful.

However, this could require to remove non-async code first, because we can not distinguish between async and non-async functions.

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 {
        ...
    }
}
jmpunkt

jmpunkt commented on Apr 18, 2020

@jmpunkt
Contributor

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 self.
To fix this issue, an impl macro for this special case is required. This impl macro provides self binding and context handling. Therefore, the mod block would look very similar to an impl block. The macro would transform the function into same format as the example, but would use a similar input as the impl macro.

Victor-Savu

Victor-Savu commented on Apr 19, 2020

@Victor-Savu
ContributorAuthor

@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?

jmpunkt

jmpunkt commented on Apr 19, 2020

@jmpunkt
Contributor

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

A current implementation which compiles (nothing tested) looks like the following

refers to my testing branch.

zhenwenc

zhenwenc commented on May 1, 2020

@zhenwenc

I am slightly disagree the proposed approach above. For instance, I am using diesel to query the database:

#[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 full_name field in User struct as there is no such column in the users table. As a workaround, I have to define another struct that doesn't contain the full_name field for diesel, which really just delegating the boilerplates...

jmpunkt

jmpunkt commented on May 1, 2020

@jmpunkt
Contributor

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.

35 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement of existing features or bugfix

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @LegNeato@Victor-Savu@zhenwenc@josejachuf@tyranron

      Issue actions

        Automatic getters for `#[graphql_object]` · Issue #553 · graphql-rust/juniper