juniper icon indicating copy to clipboard operation
juniper copied to clipboard

Unable to add custom attributes on fields

Open Dispersia opened this issue 4 years ago • 5 comments

Is your feature request related to a problem? Please describe. Unable to add custom proc_macro_attribute's to fields, so can't do any rewrite logic

Describe the solution you'd like juniper::graphql_object to keep custom attributes, not just the graphql(description/deprecation) ones

Describe alternatives you've considered Having the proc macro go over the entire impl, then defining at the top which fields get rewritten

Additional context I'm not sure what happens to the field methods, so this may not be possible if it would be transformed in some way. I started going through the codegen, but it's quite a bit at first glance so forgive me if it's not possible.

Example of how some things I would like to do:

struct Query;

#[juniper::graphql_object]
impl Query {
    #[authorize(Policy = policies::Whatever)]
    async fn foo() -> FieldResult<foo> {}
}

and being able to have a partial method rewrite that automatically handles 401/403 type responses

Dispersia avatar Apr 03 '20 07:04 Dispersia

Yep, we should totally do this.

LegNeato avatar Apr 05 '20 06:04 LegNeato

Hi, we'd like to implement this too! We already discussed it here and started looking at how it could be done but didn't go much further, it would be great if we could eventually collaborate to make this possible.

ghost avatar Apr 16 '20 09:04 ghost

Maybe I'm wrong, but I don not think it is possible to append proc macro attributes. However, the most common use-case related to this, is some kind of Guard for a field. A Guard allows to intercept the resolution of the type. If the guard fails, the underlying type is not resolved. The following example shows how to define group checks:

trait GraphQLGuard<S: juniper::ScalarValue> {
    type Context: juniper::Context;
    type Error: juniper::IntoFieldError<S>;

    fn guard(&self, ctx: &Self::Context) -> Result<(), Self::Error>;
}

struct Group(String);

impl <S: juniper::ScalarValue> GraphQLGuard<S> for Group {
    type Context = ();
    type Error = juniper::FieldError<S>;

    fn guard(&self, ctx: &Self::Context) -> Result<(), Self::Error> {
        use juniper::graphql_value;

        match ctx.get_group() {
            Some(group) if self.0 == group => Ok(()),
            _ => juniper::FieldError::new(
                "User is not part of the group",
                graphql_value!({ "policy_error": "Wrong group" })
            ),
        }
    }
}

struct Test;

#[juniper::graphql_object]
impl Test {
    #[graphql(Guard = Group("ADMIN"))]
    fn group(&self, ctx: &()) -> String {
        // user is in group
    }
}

An extension to this approach would be context conversion. Lets say a database query is required to determine if the user has a certain group. After successfully retrieving the group of the user, we want to use the group inside the Test::group(...) function. The existing context is wrapped into another context which provides additional information.

struct GeneralContext {
    item: usize,
}
struct AuthenticatedContext<T: juniper::GraphQLContext> {
    old: T,
    group: String,
}

#[juniper::graphql_object]
impl Test {
    #[graphql(Guard = Group("ADMIN"))]
    fn group(&self, ctx: &AuthenticatedContext<GeneralContext>) -> String {
        // user is in group
    }
}

If it is possible to append existing proc macro attributes, please let me know. I can also not think of anything else than authentication related use-cases, are there any more?

jmpunkt avatar Apr 17 '20 18:04 jmpunkt

I was also unable to find a way to do this after quite some effort. However, the more I thought about it, the more I felt this is a problem with rust. Shouldn't the macros execute from least scope to most? Take a struct macro:

#[foo]
struct Foo {
    #[bar]
    bar: Bar
}

bar executes first. Why would that not be the same with impl blocks as well?

#[foo]
impl Foo {
    #[bar]
    fn bar() {
        #[foobar]
    }
}

I would figure order of operation would be foobar -> bar -> foo, as that's how you would want operations to go. Am I missing something obvious that would make that not the case?

Dispersia avatar Apr 17 '20 20:04 Dispersia

At least in Juniper's case, there is no execution of "#[bar]". Each impl block macro looks for "#[graphql(...)]" attributes (here). Skipping unknown attributes would allow to use custom macro attributes (must be ensured for all impl macros). Based on this implementation it should be already possible. Is there are an example with a custom proc attribute macro which does not work?

However, this does not work for derive macros. There are definitely use-cases, e.g., data in a "simple" (derive macro) struct must be protected. In order to do that, I have to use the impl block which seems unnecessary.

jmpunkt avatar Apr 18 '20 11:04 jmpunkt