Skip to content
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

Add gql! procedural macro #592

Closed
wants to merge 2 commits into from

Conversation

Victor-Savu
Copy link
Contributor

⚠️ This is just a draft PR to support the discussion around a proposal. Suggestions for improving the code are welcome, but please keep in mind that it is pretty much the first version that worked so it will need a few iterations.

To avoid the boilerplate necessary for exposing struct members as fields whenever a complex field is needed as well, the gql! function-like procedural macro offers a unified syntax for simple and complex fields:

use juniper::graphql_value;

struct Textcon {
    accounts: HashMap<i32, Account>,
}

impl juniper::Context for Textcon {}

juniper::gql! {

#[derive(Debug)]
#[graphql(Context = Textcon)]
Account(
    usd_cents: i32,  // usd_cents *is not* exposed as a field
) {
    // balance *is* exposed as a complex field
    fn balance(&self) -> String {
        format!("${}.{:2}", self.dollars(), self.cents()
    }
}


#[derive(Debug)]
#[graphql(Context = Textcon)]
Customer(
    account_ids: Vec<i32>,   // account_ids *is not* exposed as a field
) {
    name: String   // name *is* exposed as a simple field

    // accounts *is* exposed as a complex field
    fn accounts(&self, context: &Textcon) -> juniper::FieldResult<Vec<&Account>, _> {
        let mut acc = vec![];
        for account_id in &self.account_ids {
            acc.push(
                context.accounts.get(account_id).ok_or(
                    juniper::FieldError::new(
                        format!("Invalid account id: {}", account_id),
                        graphql_value!({"": ""}))
                )?
            )
        }
        Ok(acc)
    }
}

#[graphql(context=Textcon)]
MutationRoot{}  // has no fields

#[graphql(context=Textcon)]
QueryRoot{
    // customers *is* exposed as a complex field
    async fn customers() -> Vec<Customer> {
        vec![Customer{
            name: "X".to_owned(),
            account_ids: vec![1, 4, 0],
        },
        Customer{
            name: "Y".to_owned(),
            account_ids: vec![2, 3, 7],
        },
        Customer{
            name: "Z".to_owned(),
            account_ids: vec![5, 6],
        }]
    }
}

}

impl Account {
    fn cents(&self) -> i32 {
        self.usd_cents % 100
    }

    fn dollars(&self) -> i32 {
        self.usd_cents / 100
    }
}

type Schema = RootNode<'static, QueryRoot, MutationRoot, EmptySubscription<Textcon>>;

There are a few particularities that stand out:

  1. Simple and complex fields are defined between curly braces { ... }
  2. Members that are not exposed as fields are optionally defined between parentheses ( ... ). I considered using individual specifiers (similar to how pub works) to tell fields from non-fields, but went with this approach as a matter of preference. Note that this applies only to members. There can always be an impl block to add methods, just like the impl Account { .. } in the example above.
  3. There is no struct or impl specification. This is just because I didn't want to bother with what token to use there. Any suggestions :)?

Resolves #553

@LegNeato
Copy link
Member

I've been thinking about this a lot. Not sure I like wrapping everything in a macro, but also not sure how two proc macro / derive invocations would pass data back and forth without doing so.

@Victor-Savu
Copy link
Contributor Author

Victor-Savu commented Apr 11, 2020

Thank you for the feedback! I also have the same worries. One of the great things about juniper_codegen is that it looks so much like rust. The only point where the illusion breaks a bit is that it looks like we have two impl blocks for the same type (one decorated with the macro).
I had a strong feeling that the design I went with in this PR is way too DSL-like to match the feeling that juniper_codegen users are accustomed to.

Do you think it would be useful if I created a separate crate that offers this alternative syntax? It could be outside of juniper, and in my own account and its macros would produce implementations of juniper traits. I am generally weary of software alternatives because they tend to divide communities and discourage collaboration (or at least spread developers across). On the other hand, such alternatives offer the possibility to discover preferences that we might otherwise not be able to predict. Since you have a better feeling about the community around juniper and I just want to make shre that there is no risk of alienating / splitting the community, your opinion is in my view the most informed. I want people to adopt graphql (when it makes sense for their projects) and I think juniper should be the library they choose when they do that.

(... meanwhile, back on 🌏) : Of course, this concern is only assuming such a crate is worth using and that it would get some traction, both of which are huge assumptions. So there is a 99.999% chance that it does not matter anyway 😅 I just want to make sure that I don't do anything harmful, regardless of how unlikely it is to pan out.

(... aand were back on 🪐) : If I do end up with a new crate, the implementation in this PR depends on some private modules from juniper_generator but I wouldn't mind maintaining an alternative implementation.

Thank you again for your feedback and I would really appreciate your input.

@Victor-Savu
Copy link
Contributor Author

Victor-Savu commented Apr 11, 2020

Regarding the second part of your reply, please see my comment here.

@Victor-Savu
Copy link
Contributor Author

I am closing this PR to avoid any further overhead. Thanks again for the feedback and for making juniper such great library!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Automatic getters for #[graphql_object]
2 participants