Skip to content

Converting compiler to ppx.  #3

@sainthkh

Description

@sainthkh

A little story

When I first released and promoted reasonql, I got this question a lot.

Why is it a compiler? Not a ppx? 

My answer was this:

To support fragments, we need to open all files and check the graph of GraphQL codes in files. 

And I thought it was impossible to support fragments with ppx, because ppx only replaces some elements in abstract syntax tree, so it cannot walk through entire file tree and find the appropriate fragment.

I had believed that idea until this morning and was thinking how to persuade other developers. But somehow, I realized that I was wrong if I give up some compile speed for initial and clean compilation. (And I'm not so sure now, but it seems that this speed down isn't that big.)

The Goal

Let's use the fragments snippet in this repo. (I removed Button.re here because it's not necessary for now.)

/* App.re */
let query = ReasonQL.gql({|
  query AppQuery {
    posts @singular(name: "post") {
      ...PostFragment_post
    }
  }
|})
/* Post.re */
let query = ReasonQL.gql({|
  fragment PostFragment_post on Post {
    title
    summary
    slug
  }
|})

Currently, it generates modules like below:

/* AppQuery.re */
/* Generated by ReasonQL Compiler, PLEASE EDIT WITH CARE */

/* Original Query
query AppQuery {
    posts @singular(name: "post") {
      ...PostFragment_post
    }
  }
fragment PostFragment_post on Post {
    title
    summary
    slug
  }
*/
let query = {|query AppQuery{posts{...F0}}fragment F0 on Post{title
summary
slug}|}

type post = {
  f_post: PostFragment.post,
};

type queryResult = {
  posts: array(post),
};

type variablesType = Js.Dict.t(Js.Json.t);
let encodeVariables: variablesType => Js.Json.t = vars => Js.Json.object_(vars);

[%%raw {|
var decodePost = function (res) {
  return [
    PostFragment_decodePost(res),
  ]
}

var decodeQueryResult = function (res) {
  return [
    decodePostArray(res.posts),
  ]
}

var decodePostArray = function (arr) {
  return arr.map(item =>
    decodePost(item)
  )
}

var PostFragment_decodePost = function (res) {
  return [
    res.title,
    res.summary,
    res.slug,
  ]
}
|}]

[@bs.val]external decodeQueryResultJs: Js.Json.t => queryResult = "decodeQueryResult";
let decodeQueryResult = decodeQueryResultJs;
/* PostFragment.re */
/* Generated by ReasonQL Compiler, PLEASE EDIT WITH CARE */

type post = {
  title: string,
  summary: string,
  slug: string,
};

Currently, it copies fragment codes to the query module. But when we change it like this, we don't need this copying process.

/* AppQuery.re */

module AppQuery = {
  let query = {|query AppQuery{posts{...Post_Fragment_post}}|} ++ Post.Fragment.query;

  type post = {
    f_post: Post.Fragment.post,
  };

  type queryResult = {
    posts: array(post),
  };

  type variablesType = Js.Dict.t(Js.Json.t);
  let encodeVariables: variablesType => Js.Json.t = vars => Js.Json.object_(vars);

  type postJs = Js.Json.t;

  type queryResultJs = Js.t({.
    posts: array(postJs),
  });

  let decodePost: postJs => post  = res => {
    f_post: Post.Fragment.decodePost(Obj.magic(res)),
  };

  let decodeQueryResult: queryResultJs => queryResult = res => {
    posts: res##posts |> Array.map(post => decodePost(post)),
  };
}
/* PostFragment.re */

module Fragment = {
  let query = {|fragment Post_Fragment_post on Post{
  title
  summary
  slug}|}

  type post = {
    title: string,
    summary: string,
    slug: string,
  };

  type postJs = Js.t({.
    title: string,
    summary: string,
    slug: string,
  });

  let decodePost: postJs => post = res => {
    title: res##title,
    summary: res##summary,
    slug: res##slug,
  }
}

Then, it would work without any problem. To handle fragments, we need Obj.magic. Even if a fragment changes, every other code will call the changed code without any problem when we set this rule:

The name of fragment should be written in this format:
[FileName]_[ModuleName]_[DataTypeName]

So, the PostSummary_post should be named in new reasonql_ppx like this:

Post_Fragment_post.

When we add this small rule, everything will be OK.

Todo

  • ReasonML port of graphql-js. Currently, graphql_ppx can parse graphql client query but cannot parse the schema. reason-graphql has the most similar parser. But it doesn't parse location. Maybe we need to create a reference implementation of the graphql-js. -> Result: Needs implementation. 1) graphql_ppx doesn't parse schema definition. 2) reason-graphql and ocaml-graphql-server don't compile interface. => Check https://github.com/sainthkh/graphql-reason

  • ppx that generates types and codecs for graphql queries.

  • move the reasonql.config.js to bsconfig.json

  • add the option to show generate code on files. -> As reasonql_ppx generates a lot of things, sometimes, we want to see what is going on.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions