Omar Abid

Code Generation with GraphQL in Rust

I happen to be working on an application that uses the Github API. Now, that might be straightforward, but my stack of choice isn't. Github has several SDK libraries and three of them are official. For Rust, there are two libraries though only one is maintained.

Problem is, my environment is a bit constrained. I am using Cloudflare Workers, and the compilation target for Rust is wasm32-unknown-unknown. You can't use regular Tokio networking; mainly because networking doesn't exist in this sandbox. Instead, web requests are made through the fetch API. Yes, the Web Browser fetch API, called from your WASM binary.

Create your own?

Since Octocrab, the only viable Github API library for Rust doesn't compile to wasm32-unknown-unknown, and making it play nice with fetch is an exercise in frustration, maybe one should create a new library?

Except the Github API is humongous: The challenge is not technical, but rather the sheer amount of endpoints and data structure that have to be implemented. Also, these implementations still have to be kept up-to-date.

Why a Library anyway?

There are two main reasons why I prefer to use libraries rather than call the API directly:

  1. Navigation: I don't want to be reading REST documentation and figuring out the inputs/outputs for every endpoint. Give me a type and I'll figure it out from there.

  2. Strong Typing: If the returned value is an integer, I want the variable to be typed in Rust as an integer as well.

These two points kind of converge. Essentially I want to be able to do something like response.repository.stargazers_count and be sure it's an integer, and the correct one.

GraphQL to the Rescue?

Can GraphQL solve this conundrum? Github has a GraphQL Endpoint. Queries, Mutations and the different types can be explored there. Here is the query signature for repository:

repository(
	owner: String!
	name: String!
	followRenames: Boolean = true
): Repository

And there we have it, the two of my concerns addressed. Question is, how do we convert these into Rust types?

Using a GraphQL Client

There are few GraphQL clients for Rust, and only one is eligible: GraphQL-Client. It's from the same guys who made Juniper, a GraphQL server.

The Schema

First, we need to get the schema. It defines both the types and the queries/mutations. This is best done through introspection. For simplicity, we'll pick the GraphQL schema directly from Github.

The Query

I am a bit ambivalent about whether this part is better or worse than using a library. For every particular request, you need to define your query. Both with the input and output for such a query. While this gives you more control and clarity about each request, it is less flexible when it comes to code.

query RepoView($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    homepageUrl
    stargazers {
      totalCount
    }
    issues(first: 20, states: OPEN) {
      nodes {
        title
        comments {
          totalCount
        }
      }
    }
    pullRequests(first: 20, states: OPEN) {
      nodes {
        title
        commits {
          totalCount
        }
      }
    }
  }
}

Code Generation

And here comes the magic of Rust derived macros. We only need to define an empty struct and ask graphql-client to generate the corresponding types for it.

#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "assets/schema.graphql",
    query_path = "assets/query.graphql",
    response_derives = "Debug"
)]
struct RepoView;

Then, an instance of RepoView will encode the return type of the query.

let total_count: i64 = repo_view.stargazers.total_count;

The generated types can be retrieved using cargo-expand. This is the work we would have had to do manually. Here is an excerpt from the expanded code.

        }
    }
    pub struct RepoViewRepositoryPullRequests {
        pub nodes: Option<Vec<Option<RepoViewRepositoryPullRequestsNodes>>>,
    }
    #[doc(hidden)]
    #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
    const _: () = {
        #[allow(unused_extern_crates, clippy::useless_attribute)]
        extern crate serde as _serde;
        #[automatically_derived]
        impl<'de> _serde::Deserialize<'de> for RepoViewRepositoryPullRequests {
            fn deserialize<__D>(

Yes, it's this simple. If you need to convince someone to use GraphQL to expose their API, feel free to refer them to this article.