Full Code of sikanhe/reason-graphql for AI

master 07459caab70a cached
36 files
160.7 KB
42.3k tokens
1 requests
Download .txt
Repository: sikanhe/reason-graphql
Branch: master
Commit: 07459caab70a
Files: 36
Total size: 160.7 KB

Directory structure:
gitextract_17cd7pmq/

├── .circleci/
│   └── config.yml
├── .github/
│   └── workflows/
│       └── nodejs.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── examples/
│   └── starwars-api/
│       ├── .gitignore
│       ├── README.md
│       ├── bsconfig.json
│       ├── package.json
│       └── src/
│           ├── schema.re
│           ├── server.re
│           └── swapi.re
├── reason-graphql/
│   ├── __tests__/
│   │   ├── GraphqlJsPromise.re
│   │   ├── StarWarsData.re
│   │   ├── StarWarsSchema.re
│   │   ├── parser_test.re
│   │   └── schema_test.re
│   ├── bsconfig.json
│   ├── package.json
│   └── src/
│       ├── Graphql.re
│       ├── Graphql_Interface.re
│       ├── Graphql_Json.re
│       ├── Graphql_Schema.re
│       ├── Graphql_Schema.rei
│       └── language/
│           ├── Graphql_Language.re
│           ├── Graphql_Language_Ast.re
│           ├── Graphql_Language_Error.re
│           ├── Graphql_Language_Lexer.re
│           ├── Graphql_Language_Parser.re
│           └── Graphql_Language_Printer.re
└── reason-graphql-bs-express/
    ├── bsconfig.json
    ├── examples/
    │   └── server.re
    ├── package.json
    └── src/
        ├── Graphiql.re
        └── GraphqlExpress.re

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
  build:
    docker:
      - image: circleci/node:11.8.0

    environment:
      TEST_REPORTS: /tmp/test-reports

    steps:
      - checkout

      - restore_cache:
          name: Restore Yarn Package Cache
          keys:
            - yarn-packages-{{ checksum "yarn.lock" }}
      - run:
          name: Install Dependencies
          command: cd ./reason-graphql && yarn install --frozen-lockfile

      - save_cache:
          name: Save Yarn Package Cache
          key: yarn-packages-{{ checksum "yarn.lock" }}
          paths:
            - ~/.cache/yarn

      - run:
          name: tests
          command: cd ./reason-graphql && yarn test


================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node CI

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [8.x, 10.x, 12.x]

    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: yarn install, build, and test
      run: |
        cd ./reason-graphql
        yarn install 
        yarn build 
        yarn test


================================================
FILE: .gitignore
================================================
.DS_Store
.merlin
.bsb.lock
npm-debug.log
lib
node_modules
.vscode
*.bs.js
.pnp
.pnp.js

================================================
FILE: CHANGELOG.md
================================================
## DEV
### *reason-graphql*
* (Breaking) Removed "Variations". Instead, we provide guidance on how to integrate this with different IO/Async libraries like `reason-promise` or `reason-future`. This prevents version conflicts due to users wanting to upgrade those libraries independently from `reason-graphql`. 
* (Breaking) Changed runtime representation of `List` graphql type to be Array to align better with reality of JS environment and upcoming Bucklescript changes (to prioritize array over list). (https://github.com/sikanhe/reason-graphql/pull/41)

## 0.6.1 
### *reason-graphql*
* Fixed introspection not working due to subscription type missing ([#33](https://github.com/sikanhe/reason-graphql/pull/33))

## 0.6.0
### *reason-graphql*
* Upgraded bs-platform to v7.0.1 ([#29](https://github.com/sikanhe/reason-graphql/pull/29))
* Fixed erroring on undefined variables by coercing missing variables to `null` ([#29](https://github.com/sikanhe/reason-graphql/pull/29))

## 0.4.2
### *reason-graphql*
* Added __typename support ([#24](https://github.com/sikanhe/reason-graphql/pull/24))

## 0.4.0 

### *reason-graphql*
* Added `GraphqlPromise` which is a pre-configured variation  that uses `Js.Promise` as the IO type. 


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2019 Sikan He

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
[![CircleCI](https://circleci.com/gh/sikanhe/reason-graphql/tree/master.svg?style=svg)](https://circleci.com/gh/sikanhe/reason-graphql/tree/master)

Type safe GraphQL server in pure reason. Compiles to nodejs. A direct port from https://github.com/andreas/ocaml-graphql-server to make it work with Javascript backend.



### Motivation

Bucklescript is an amazing alternative to other compie-to-js languages. But it's a tragedy that we have such few libraries and bindings written for the server-side. 

In the browser, we can get away with just one great binding for React. However, for the server side, people want major building blocks - a solid database abstractions, a fast http server and a Graphql layer are usually must haves for non-trivial projects. 

For these type of frameworks, I believe we should do more than just writing simple bindings to existing npm libraries, because then we are not taking advantage of the expressive type system and rich features of OCaml/Reason. We also lose the chance to show what a language like Reason can do for developer happiness and why its worth it to use it over existing alternatives. 

### What we get over the traditional JS/TS/Flow + graphql-js combination

#### Type safety without manual work
Like the ReasonReact implementation, GraphQL schemas defined by this library can express more things and place more constraint on the graphql schema than the vanilla javascript or typescript counterparts. 

With Typescript (or Flow), you are required to manually write out the types for all your fields, or use a type generation cli tool like https://graphql-code-generator.com - on top of a lot of manual typecasting, because its typesystem cannot express advanced concepts like heterogenous lists (essentially what arguments and fields are, in GraphQL). 

To give you a taste of what this means - if we define a field argument called `id` with type `string` with graphql-js, even with typescript or flow, it cannot infer the type of `args` inside the resolver to be `{id: string}`. We have to manually type it, which makes it very error-prone. Forget about tracking the nullability of the fields - I have seen many production errors where the manually casted types are out of sync with the schema definition, and vice versa, when the schema gets out of sync with the underlying database models.

```typescript
  type PersonByIdArgs = {
    id: string
  }
  
  personById: {
    type: Person,
    args: { id: GraphqlNonNull(GraphqQLString) },
    resolve: (ctx, parent, args: PersonByIdArgs) => {
                           ^^^^ this is inferred as `Any`, so we need to manually cast it to `PersonByIdArgs`
    }
  }
```

In Reason, we can use a more advanced feature of the type system called GADT (Generalized Algebriac Data Types) to express our schema types. 

What GADT allows us to do is to have type-safe definitions without needing to manually write types for field arguments and resolver. It can "unfold" the types for resolver as you write out field args! (https://drup.github.io/2016/08/02/difflists/)

```reason 
 field("PersonById", 
  ~typ=person, 
  ~args=Args.[arg("id", nonnull(string))] 
  ~resolve=(_ctx, _parent, id) => {
//                         ^^ Unfolds args into resolver arguments and correctly types it as a string!
  }
```

This works for as many arguments as you like, it infers nullability for you as well and give you an `option('a)` if its nullable! 

```reason 
 field("PersonById", 
  ~typ=person, 
  ~args=Args.[arg("id", nonnull(string)), arg("age", int)] 
  ~resolve=(_ctx, _parent, id,     age) => {
                           ^^      ^^^ 
                           string  option(int) because age is not non-null
  }
```
 
### Features todolist:
  - [x] Query 
  - [x] Mutation 
  - [x] Async Fields
  - [x] Directives
  - [ ] Subscription
  - [ ] Non-type Validations (unused fragment, unused variables, and etc)


================================================
FILE: examples/starwars-api/.gitignore
================================================
.DS_Store
.merlin
.bsb.lock
npm-debug.log
/lib/bs/
/node_modules/


================================================
FILE: examples/starwars-api/README.md
================================================
# StarWars API using reason-graphql 

Live demo: https://reason-graphql-swapi.onrender.com/graphql

================================================
FILE: examples/starwars-api/bsconfig.json
================================================
{
  "name": "starwars-api",
  "version": "0.1.0",
  "sources": {
    "dir" : "src",
    "subdirs" : true
  },
  "package-specs": {
    "module": "commonjs",
    "in-source": true
  },
  "suffix": ".bs.js",
  "bs-dependencies": [
    "bs-express",
    "bs-fetch",
    "@glennsl/bs-json",
    "reason-future",
    "reason-graphql",
    "reason-graphql-bs-express",
    "reason-dataloader"
  ],
  "warnings": {
    "number": "-44-45",
    "error" : "+101"
  },
  "namespace": true,
  "refmt": 3
}


================================================
FILE: examples/starwars-api/package.json
================================================
{
  "name": "starwars-api",
  "version": "0.1.0",
  "scripts": {
    "build": "bsb -make-world",
    "start": "bsb -make-world -w",
    "clean": "bsb -clean-world"
  },
  "keywords": [
    "BuckleScript"
  ],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "bs-platform": "^5.0.2"
  },
  "dependencies": {
    "@glennsl/bs-json": "^3.0.0",
    "bs-express": "^0.12.0",
    "bs-fetch": "^0.3.1",
    "bs-json": "^1.0.1",
    "isomorphic-fetch": "^2.2.1",
    "reason-dataloader": "^0.1.1",
    "reason-graphql": "^0.3.2",
    "reason-graphql-bs-express": "^0.2.0"
  }
}


================================================
FILE: examples/starwars-api/src/schema.re
================================================
open GraphqlFuture;

let starship =
  Schema.(
    obj("starship", ~fields=_ =>
      [
        field(
          "name",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.name
        ),
        field(
          "model",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.model
        ),
        field(
          "starshipClass",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.starshipClass
        ),
        field(
          "manufacturers",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.manufacturers
        ),
        field(
          "costInCredits",
          float,
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.costInCredits
        ),
        field(
          "length",
          float,
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.length
        ),
        field(
          "crew",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.crew
        ),
        field(
          "passengers",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.passengers
        ),
        field(
          "maxAtmospheringSpeed",
          int,
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.maxAtmospheringSpeed
        ),
        field(
          "hyperdriveRating",
          float,
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.hyperdriveRating
        ),
        field(
          "mglt", int, ~args=Arg.[], ~resolve=((), starship: Swapi.starship) =>
          starship.mglt
        ),
        field(
          "cargoCapacity",
          float,
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.cargoCapacity
        ),
        field(
          "consumables",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), starship: Swapi.starship) =>
          starship.consumables
        ),
      ]
    )
  );

let rec film =
  lazy
    Schema.(
      obj("film", ~fields=_ =>
        [
          field(
            "title",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            film.title
          ),
          field(
            "episodeID",
            nonnull(int),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            film.episodeID
          ),
          field(
            "openingCrawl",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            film.openingCrawl
          ),
          field(
            "director",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            film.director
          ),
          field(
            "producers",
            nonnull(list(nonnull(string))),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            film.producers
          ),
          field(
            "releaseDate",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            film.releaseDate
          ),
          async_field(
            "characters",
            nonnull(list(nonnull(Lazy.force(person)))),
            ~args=Arg.[],
            ~resolve=((), film: Swapi.film) =>
            Swapi.getEntitiesByUrls(Swapi.decodePerson, film.characterUrls)
          ),
        ]
      )
    )

and person =
  lazy
    Schema.(
      obj("person", ~fields=_ =>
        [
          field(
            "name",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            person.name
          ),
          field(
            "birthYear",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            person.birthYear
          ),
          field(
            "eyeColor",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            person.eyeColor
          ),
          field(
            "gender",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            person.gender
          ),
          field(
            "hairColor",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            person.hairColor
          ),
          field(
            "height", int, ~args=Arg.[], ~resolve=((), person: Swapi.person) =>
            person.height
          ),
          field(
            "mass", float, ~args=Arg.[], ~resolve=((), person: Swapi.person) =>
            person.mass
          ),
          field(
            "skinColor",
            nonnull(string),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            person.skinColor
          ),
          async_field(
            "films",
            nonnull(list(nonnull(Lazy.force(film)))),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            Swapi.getEntitiesByUrls(Swapi.decodeFilm, person.filmUrls)
          ),
          async_field(
            "starships",
            nonnull(list(nonnull(starship))),
            ~args=Arg.[],
            ~resolve=((), person: Swapi.person) =>
            Swapi.getEntitiesByUrls(Swapi.decodeStarship, person.starshipUrls)
          ),
        ]
      )
    );

let planet =
  Schema.(
    obj("planet", ~fields=_ =>
      [
        field(
          "name",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.name
        ),
        field(
          "diameter", int, ~args=Arg.[], ~resolve=((), planet: Swapi.planet) =>
          planet.diameter
        ),
        field(
          "rotationPeriod",
          int,
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.rotationPeriod
        ),
        field(
          "orbitalPeriod",
          int,
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.orbitalPeriod
        ),
        field(
          "gravity",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.gravity
        ),
        field(
          "population",
          float,
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.population
        ),
        field(
          "climates",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.climates
        ),
        field(
          "terrains",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.terrains
        ),
        field(
          "surfaceWater",
          float,
          ~args=Arg.[],
          ~resolve=((), planet: Swapi.planet) =>
          planet.surfaceWater
        ),
      ]
    )
  );

let species =
  Schema.(
    obj("species", ~fields=_ =>
      [
        field(
          "name",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.name
        ),
        field(
          "classification",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.classification
        ),
        field(
          "designation",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.designation
        ),
        field(
          "averageHeight",
          float,
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.averageHeight
        ),
        field(
          "averageLifespan",
          int,
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.averageLifespan
        ),
        field(
          "eyeColors",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.eyeColors
        ),
        field(
          "hairColors",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.hairColors
        ),
        field(
          "skinColors",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.skinColors
        ),
        field(
          "language",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), species: Swapi.species) =>
          species.language
        ),
      ]
    )
  );

let vehicle =
  Schema.(
    obj("vehicle", ~fields=_ =>
      [
        field(
          "name",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.name
        ),
        field(
          "model",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.model
        ),
        field(
          "starshipClass",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.vehicleClass
        ),
        field(
          "manufacturers",
          nonnull(list(nonnull(string))),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.manufacturers
        ),
        field(
          "costInCredits",
          float,
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.costInCredits
        ),
        field(
          "length", float, ~args=Arg.[], ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.length
        ),
        field(
          "crew",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.crew
        ),
        field(
          "passengers",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.passengers
        ),
        field(
          "maxAtmospheringSpeed",
          int,
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.maxAtmospheringSpeed
        ),
        field(
          "cargoCapacity",
          float,
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.cargoCapacity
        ),
        field(
          "consumables",
          nonnull(string),
          ~args=Arg.[],
          ~resolve=((), vehicle: Swapi.vehicle) =>
          vehicle.consumables
        ),
      ]
    )
  );

Lazy.force(film);
Lazy.force(person);

let rootQuery =
  Schema.(
    query([
      async_field(
        "allFilms",
        nonnull(list(nonnull(Lazy.force(film)))),
        ~args=Arg.[],
        ~resolve=((), ()) =>
        Swapi.getAllFilms()
      ),
      async_field(
        "film",
        Lazy.force(film),
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=((), (), id) =>
        Swapi.getFilm(id)
      ),
      async_field(
        "allPeople",
        nonnull(list(nonnull(Lazy.force(person)))),
        ~args=Arg.[],
        ~resolve=((), ()) =>
        Swapi.getAllPeople()
      ),
      async_field(
        "person",
        Lazy.force(person),
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=((), (), id) =>
        Swapi.getPerson(id)
      ),
      async_field(
        "allPlanets",
        nonnull(list(nonnull(planet))),
        ~args=Arg.[],
        ~resolve=((), ()) =>
        Swapi.getAllPlanets()
      ),
      async_field(
        "planet",
        planet,
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=((), (), id) =>
        Swapi.getPlanet(id)
      ),
      async_field(
        "allSpecies",
        nonnull(list(nonnull(species))),
        ~args=Arg.[],
        ~resolve=((), ()) =>
        Swapi.getAllSpecies()
      ),
      async_field(
        "species",
        species,
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=((), (), id) =>
        Swapi.getSpecies(id)
      ),
      async_field(
        "allStarships",
        nonnull(list(nonnull(starship))),
        ~args=Arg.[],
        ~resolve=((), ()) =>
        Swapi.getAllStarships()
      ),
      async_field(
        "starship",
        starship,
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=((), (), id) =>
        Swapi.getStarship(id)
      ),
      async_field(
        "allVehicles",
        nonnull(list(nonnull(vehicle))),
        ~args=Arg.[],
        ~resolve=((), ()) =>
        Swapi.getAllVehicles()
      ),
      async_field(
        "vehicle",
        vehicle,
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=((), (), id) =>
        Swapi.getVehicle(id)
      ),
    ])
  );

let schema = Schema.create(rootQuery);

================================================
FILE: examples/starwars-api/src/server.re
================================================
open Express;

let app = express();

App.use(app, Middleware.json(~limit=ByteLimit.mb(5.0), ()));

App.useOnPath(app, ~path="/graphql") @@
GraphqlExpress.middleware(Schema.schema, ~provideCtx=_ => (), ~graphiql=true);

App.get(app, ~path="*") @@
Middleware.from((_, _) => Response.redirect("/graphql"));

let onListen = e =>
  switch (e) {
  | exception (Js.Exn.Error(e)) =>
    Js.log(e);
    Node.Process.exit(1);
  | _ => Js.log("Listening at http://127.0.0.1:3000")
  };

App.listen(app, ~port=3000, ~onListen, ());

================================================
FILE: examples/starwars-api/src/swapi.re
================================================
type film = {
  title: string,
  episodeID: int,
  openingCrawl: string,
  director: string,
  producers: list(string),
  releaseDate: string,
  characterUrls: list(string),
};

type person = {
  name: string,
  birthYear: string,
  eyeColor: string,
  gender: string,
  hairColor: string,
  height: option(int),
  mass: option(float),
  skinColor: string,
  filmUrls: list(string),
  speciesUrls: list(string),
  vehicleUrls: list(string),
  starshipUrls: list(string),
};

type planet = {
  name: string,
  diameter: option(int),
  rotationPeriod: option(int),
  orbitalPeriod: option(int),
  gravity: string,
  population: option(float),
  climates: list(string),
  terrains: list(string),
  surfaceWater: option(float),
};

type species = {
  name: string,
  classification: string,
  designation: string,
  averageHeight: option(float),
  averageLifespan: option(int),
  eyeColors: list(string),
  hairColors: list(string),
  skinColors: list(string),
  language: string,
};

type starship = {
  name: string,
  model: string,
  starshipClass: string,
  manufacturers: list(string),
  costInCredits: option(float),
  length: option(float),
  crew: string,
  passengers: string,
  maxAtmospheringSpeed: option(int),
  hyperdriveRating: option(float),
  mglt: option(int),
  cargoCapacity: option(float),
  consumables: string,
};

type vehicle = {
  name: string,
  model: string,
  vehicleClass: string,
  manufacturers: list(string),
  costInCredits: option(float),
  length: option(float),
  crew: string,
  passengers: string,
  maxAtmospheringSpeed: option(int),
  cargoCapacity: option(float),
  consumables: string,
};

[%raw "require('isomorphic-fetch')"];

let baseUrl = "https://swapi.co/api/";

exception RecordNotFound;

let removeGrouping = numberString =>
  Js.String.replaceByRe([%re "/,/"], "", numberString);

let parseFloat = n =>
  n |> Js.String.replaceByRe([%re "/,/"], "") |> Js.Float.fromString;
let parseFloatOpt =
  fun
  | "n/a"
  | "unknown" => None
  | n =>
    try (Some(n |> parseFloat)) {
    | Failure(_) => None
    };

let parseInt = n => {
  (n |> Js.String.replaceByRe([%re "/,/"], ""))->int_of_string;
};
let parseIntOpt =
  fun
  | "n/a"
  | "unknown" => None
  | n =>
    try (Some(n |> parseInt)) {
    | Failure(_) => None
    };

let rec futureAll =
  Future.(
    fun
    | [] => value([])
    | [future, ...rest] =>
      flatMap(future, value => map(futureAll(rest), acc => [value, ...acc]))
  );

let urlDataloader =
  Dataloader.make(urls =>
    urls
    |> List.map(url =>
         Js.Promise.(
           Fetch.fetch(url)
           |> then_(res =>
                Fetch.Response.status(res) == 404
                  ? reject(RecordNotFound) : resolve(res)
              )
           |> then_(Fetch.Response.json)
           |> FutureJs.fromPromise(_, Js.String.make)
         )
       )
    |> futureAll
  );

let getEntityByUrl = (decoder, url) =>
  Future.map(
    urlDataloader.load(url),
    fun
    | Ok(thing) => Belt.Result.Ok(Some(decoder(thing)))
    | Error(_) => Ok(None),
  );

let getEntityById = (path, decoder, id) =>
  getEntityByUrl(decoder, baseUrl ++ path ++ "/" ++ string_of_int(id));

let getAllEntitiesForType = (path, decoder, ()) => {
  let rec aux = (acc, nextUrl) => {
    urlDataloader.load(nextUrl)
    ->Future.flatMapOk(json => {
        let results = Json.Decode.(json |> field("results", list(decoder)));
        let nextUrl = Json.Decode.(json |> field("next", optional(string)));

        switch (nextUrl) {
        | Some(url) => aux(List.concat([acc, results]), url)
        | None => Future.value(Belt.Result.Ok(List.concat([acc, results])))
        };
      });
  };

  aux([], baseUrl ++ path);
};

let getEntitiesByUrls = (decoder, urls) => {
  urlDataloader.loadMany(urls)
  ->Future.map(
      List.fold_left(
        acc =>
          fun
          | Belt.Result.Ok(result) => [decoder(result), ...acc]
          | Error(_) => acc,
        [],
      ),
    )
  ->Future.map(list => Belt.Result.Ok(list));
};

let decodeFilm = (json: Js.Json.t): film =>
  Json.Decode.{
    title: json |> field("title", string),
    episodeID: json |> field("episode_id", int),
    openingCrawl: json |> field("opening_crawl", string),
    director: json |> field("director", string),
    producers:
      json
      |> field("producer", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    releaseDate: json |> field("release_date", string),
    characterUrls: json |> field("characters", list(string)),
  };

let getFilm = getEntityById("films", decodeFilm);
let getAllFilms = getAllEntitiesForType("films", decodeFilm);

let decodePerson = (json: Js.Json.t): person =>
  Json.Decode.{
    name: json |> field("name", string),
    birthYear: json |> field("birth_year", string),
    eyeColor: json |> field("eye_color", string),
    gender: json |> field("gender", string),
    hairColor: json |> field("hair_color", string),
    height: json |> field("height", string) |> parseIntOpt,
    mass: json |> field("mass", string) |> parseFloatOpt,
    skinColor: json |> field("skin_color", string),
    filmUrls: json |> field("films", list(string)),
    starshipUrls: json |> field("starships", list(string)),
    vehicleUrls: json |> field("vehicles", list(string)),
    speciesUrls: json |> field("species", list(string)),
  };

let getPerson = getEntityById("people", decodePerson);
let getAllPeople = getAllEntitiesForType("people", decodePerson);

let decodePlanet = (json: Js.Json.t): planet =>
  Json.Decode.{
    name: json |> field("name", string),
    diameter: json |> field("diameter", string) |> parseIntOpt,
    rotationPeriod: json |> field("rotation_period", string) |> parseIntOpt,
    orbitalPeriod: json |> field("orbital_period", string) |> parseIntOpt,
    gravity: json |> field("gravity", string),
    population: json |> field("population", string) |> parseFloatOpt,
    climates:
      json
      |> field("climate", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    terrains:
      json
      |> field("terrain", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    surfaceWater: json |> field("surface_water", string) |> parseFloatOpt,
  };

let getPlanet = getEntityById("planets", decodePlanet);
let getAllPlanets = getAllEntitiesForType("planets", decodePlanet);

let decodeSpecies = (json: Js.Json.t): species =>
  Json.Decode.{
    name: json |> field("name", string),
    classification: json |> field("classification", string),
    designation: json |> field("designation", string),
    averageHeight: json |> field("average_height", string) |> parseFloatOpt,
    averageLifespan: json |> field("average_lifespan", string) |> parseIntOpt,
    eyeColors:
      json
      |> field("eye_colors", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    hairColors:
      json
      |> field("hair_colors", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    skinColors:
      json
      |> field("skin_colors", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    language: json |> field("language", string),
  };

let getSpecies = getEntityById("species", decodeSpecies);
let getAllSpecies = getAllEntitiesForType("species", decodeSpecies);

let decodeStarship = (json: Js.Json.t): starship =>
  Json.Decode.{
    name: json |> field("name", string),
    model: json |> field("model", string),
    starshipClass: json |> field("starship_class", string),
    manufacturers:
      json
      |> field("manufacturer", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    costInCredits: json |> field("cost_in_credits", string) |> parseFloatOpt,
    length: json |> field("length", string) |> parseFloatOpt,
    crew: json |> field("crew", string),
    passengers: json |> field("passengers", string),
    maxAtmospheringSpeed:
      json |> field("max_atmosphering_speed", string) |> parseIntOpt,
    hyperdriveRating:
      json |> field("hyperdrive_rating", string) |> parseFloatOpt,
    mglt: json |> field("MGLT", string) |> parseIntOpt,
    cargoCapacity: json |> field("cargo_capacity", string) |> parseFloatOpt,
    consumables: json |> field("consumables", string),
  };

let getStarship = getEntityById("starships", decodeStarship);
let getAllStarships = getAllEntitiesForType("starships", decodeStarship);

let decodeVehicle = (json: Js.Json.t): vehicle =>
  Json.Decode.{
    name: json |> field("name", string),
    model: json |> field("model", string),
    vehicleClass: json |> field("vehicle_class", string),
    manufacturers:
      json
      |> field("manufacturer", string)
      |> Js.String.split(", ")
      |> Array.to_list,
    costInCredits: json |> field("cost_in_credits", string) |> parseFloatOpt,
    length: json |> field("length", string) |> parseFloatOpt,
    crew: json |> field("crew", string),
    passengers: json |> field("passengers", string),
    maxAtmospheringSpeed:
      json |> field("max_atmosphering_speed", string) |> parseIntOpt,
    cargoCapacity: json |> field("cargo_capacity", string) |> parseFloatOpt,
    consumables: json |> field("consumables", string),
  };

let getVehicle = getEntityById("vehicles", decodeVehicle);
let getAllVehicles = getAllEntitiesForType("vehicles", decodeVehicle);

================================================
FILE: reason-graphql/__tests__/GraphqlJsPromise.re
================================================
module Schema =
  Graphql.Schema.Make({
    type t('a) = Js.Promise.t('a);

    let return = Js.Promise.resolve;
    let map = (p, continuation) => {
      p |> Js.Promise.then_(value => continuation(value)->Js.Promise.resolve);
    };
    let bind = (p, continuation) => {
      Js.Promise.then_(continuation, p);
    };
  });


================================================
FILE: reason-graphql/__tests__/StarWarsData.re
================================================
type episode =
  | NEWHOPE
  | EMPIRE
  | JEDI;

type human = {
  id: int,
  name: string,
  friends: list(int),
  appearsIn: array(episode),
  homePlanet: option(string),
};

type droid = {
  id: int,
  name: string,
  friends: list(int),
  appearsIn: array(episode),
  primaryFunction: string,
};

type character =
  | Human(human)
  | Droid(droid);

let luke = {
  id: 1000,
  name: "Luke Skywalker",
  friends: [1002, 1003, 2000, 2001],
  appearsIn: [|NEWHOPE, EMPIRE, JEDI|],
  homePlanet: Some("Tatooine"),
};

let vader = {
  id: 1001,
  name: "Darth Vader",
  friends: [1004],
  appearsIn: [|NEWHOPE, EMPIRE, JEDI|],
  homePlanet: Some("Tatooine"),
};

let han = {
  id: 1002,
  name: "Han Solo",
  friends: [1000, 1003, 2001],
  appearsIn: [|NEWHOPE, EMPIRE, JEDI|],
  homePlanet: None,
};

let leia = {
  id: 1003,
  name: "Leia Organa",
  friends: [1000, 1002, 2000, 2001],
  appearsIn: [|NEWHOPE, EMPIRE, JEDI|],
  homePlanet: Some("Alderaan"),
};

let threepio = {
  id: 2000,
  name: "C-3PO",
  friends: [1000, 1002, 1003, 2001],
  appearsIn: [|NEWHOPE, EMPIRE, JEDI|],
  primaryFunction: "Protocol",
};

let artoo = {
  id: 2001,
  name: "R2-D2",
  friends: [1000, 1002, 1003],
  appearsIn: [|NEWHOPE, EMPIRE, JEDI|],
  primaryFunction: "Astromech",
};

let getHuman = id =>
  Js.Promise.resolve(Belt.List.getBy([luke, han, leia, vader], human => human.id == id));
let getDroid = id =>
  Js.Promise.resolve(Belt.List.getBy([threepio, artoo], droid => droid.id == id));
let getCharacter = id => {
  getHuman(id)
  |> Js.Promise.(
       then_(
         fun
         | Some(human) => resolve(Some(Human(human)))
         | None => {
             getDroid(id)
             |> then_(
                  fun
                  | Some(droid) => resolve(Some(Droid(droid)))
                  | None => resolve(None),
                );
           },
       )
     );
};

type updateCharacterNameError =
  | CharacterNotFound(int);

type updateCharacterResult = Belt.Result.t(character, updateCharacterNameError);

let updateCharacterName = (id, name): Js.Promise.t(updateCharacterResult) => {
  getCharacter(id)
  |> Js.Promise.(
       then_(
         fun
         | Some(Human(human)) => resolve(Belt.Result.Ok(Human({...human, name})))
         | Some(Droid(droid)) => resolve(Belt.Result.Ok(Droid({...droid, name})))
         | None => resolve(Belt.Result.Error(CharacterNotFound(id))),
       )
     );
};

let rec futureAll =
  fun
  | [] => Js.Promise.resolve([])
  | [x, ...xs] =>
    Js.Promise.then_(
      xs' => Js.Promise.then_(x' => Js.Promise.resolve([x', ...xs']), x),
      futureAll(xs),
    );

let getFriends = ids => {
  Belt.List.map(ids, id => {
    id |> getCharacter |> Js.Promise.(then_(x => resolve(Belt.Option.getExn(x))))
  })
  |> Array.of_list
  |> Js.Promise.all
};


================================================
FILE: reason-graphql/__tests__/StarWarsSchema.re
================================================
open Belt.Result;
open GraphqlJsPromise;

module StarWars = StarWarsData;

let episodeEnum =
  Schema.(
    makeEnum(
      "Episode",
      [
        enumValue("NEWHOPE", ~value=StarWars.NEWHOPE, ~description="Released in 1977"),
        enumValue("EMPIRE", ~value=StarWars.EMPIRE, ~description="Released in 1980"),
        enumValue("JEDI", ~value=StarWars.JEDI, ~description="Released in 1983"),
      ],
    )
  );

let rec characterInterface: Schema.abstractType('ctx, [ | `Character]) =
  Schema.(
    interface("Character", ~fields=character =>
      [
        abstractField("id", nonnull(int), ~args=[]),
        abstractField("name", nonnull(string), ~args=[]),
        abstractField("appearsIn", nonnull(list(nonnull(episodeEnum.fieldType))), ~args=[]),
        abstractField("friends", nonnull(list(nonnull(character))), ~args=[]),
      ]
    )
  )

and humanAsCharacterInterface =
  lazy(Schema.addType(characterInterface, Lazy.force(humanTypeLazy)))
and droidAsCharacterInterface =
  lazy(Schema.addType(characterInterface, Lazy.force(droidTypeLazy)))
and asCharacterInterface =
  fun
  | StarWars.Human(human) => Lazy.force(humanAsCharacterInterface, human)
  | Droid(droid) => Lazy.force(droidAsCharacterInterface, droid)

and humanTypeLazy =
  lazy(
    Schema.(
      obj("Human", ~description="A humanoid creature in the Star Wars universe.", ~fields=_ =>
        [
          field("id", nonnull(int), ~args=[], ~resolve=(_ctx, human: StarWars.human) => human.id),
          field("name", nonnull(string), ~args=[], ~resolve=(_ctx, human: StarWars.human) =>
            human.StarWars.name
          ),
          field(
            "appearsIn",
            nonnull(list(nonnull(episodeEnum.fieldType))),
            ~args=[],
            ~resolve=(_ctx, human: StarWars.human) =>
            human.StarWars.appearsIn
          ),
          async_field(
            "friends",
            nonnull(list(nonnull(characterInterface))),
            ~args=[],
            ~resolve=(_ctx, human: StarWars.human) =>
            Js.Promise.(
              StarWars.getFriends(human.friends)
              |> then_(list => resolve(Ok(Belt.Array.map(list, asCharacterInterface))))
            )
          ),
          field("homePlanet", string, ~args=[], ~resolve=(_ctx, human: StarWars.human) =>
            human.homePlanet
          ),
        ]
      )
    )
  )

and droidTypeLazy =
  lazy(
    Schema.(
      obj("Droid", ~description="A mechanical creature in the Star Wars universe.", ~fields=_ =>
        [
          field("id", nonnull(int), ~args=[], ~resolve=(_ctx, droid: StarWars.droid) => droid.id),
          field("name", nonnull(string), ~args=[], ~resolve=(_ctx, droid: StarWars.droid) =>
            droid.StarWars.name
          ),
          field(
            "appearsIn",
            nonnull(list(nonnull(episodeEnum.fieldType))),
            ~args=[],
            ~resolve=(_ctx, droid: StarWars.droid) =>
            droid.StarWars.appearsIn
          ),
          field(
            "primaryFunction", nonnull(string), ~args=[], ~resolve=(_ctx, droid: StarWars.droid) =>
            droid.primaryFunction
          ),
          async_field(
            "friends",
            nonnull(list(nonnull(characterInterface))),
            ~args=[],
            ~resolve=(_ctx, droid: StarWars.droid) =>
            Js.Promise.(
              StarWars.getFriends(droid.friends)
              |> then_(list => resolve(Belt.Array.map(list, asCharacterInterface)))
              |> then_(list => resolve(Ok(list)))
            )
          ),
        ]
      )
    )
  );

let humanType = Lazy.force(humanTypeLazy);
let droidType = Lazy.force(droidTypeLazy);
let humanAsCharacterInterface = Lazy.force(humanAsCharacterInterface);
let droidAsCharacterInterface = Lazy.force(droidAsCharacterInterface);

let query =
  Schema.(
    query([
      field(
        "hero",
        nonnull(characterInterface),
        ~args=
          Arg.[
            arg(
              "episode",
              episodeEnum.argTyp,
              ~description=
                "If omitted, returns the hero of the whole saga. "
                ++ "If provided, returns the hero of that particular episode.",
            ),
          ],
        ~resolve=(_ctx, (), episode) =>
        switch (episode) {
        | Some(EMPIRE) => humanAsCharacterInterface(StarWarsData.luke)
        | _ => droidAsCharacterInterface(StarWarsData.artoo)
        }
      ),
      async_field(
        "human",
        humanType,
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=(_ctx, (), argId) => {
          let id = argId;
          StarWarsData.getHuman(id) |> Js.Promise.(then_(human => resolve(Ok(human))));
        },
      ),
      async_field(
        "droid",
        droidType,
        ~args=Arg.[arg("id", nonnull(int))],
        ~resolve=(_ctx, (), argId) => {
          let id = argId;
          StarWarsData.getDroid(id) |> Js.Promise.(then_(human => resolve(Ok(human))));
        },
      ),
    ])
  );

let updateCharacterResponse =
  Schema.(
    obj("UpdateCharacterResponse", ~fields=_ =>
      [
        field(
          "error",
          string,
          ~args=[],
          ~resolve=(_, updateCharResult: StarWars.updateCharacterResult) =>
          switch (updateCharResult) {
          | Ok(_) => None
          | Error(CharacterNotFound(id)) =>
            Some("Character with ID " ++ string_of_int(id) ++ " not found")
          }
        ),
        field(
          "character",
          characterInterface,
          ~args=[],
          ~resolve=(_, updateCharResult: StarWars.updateCharacterResult) =>
          switch (updateCharResult) {
          | Ok(char) => Some(char->asCharacterInterface)
          | Error(_) => None
          }
        ),
      ]
    )
  );

let mutation =
  Schema.(
    mutation([
      async_field(
        "updateCharacterName",
        nonnull(updateCharacterResponse),
        ~args=
          Arg.[
            arg("characterId", nonnull(int)),
            arg("name", nonnull(string)),
            arg("appearsIn", string),
          ],
        ~resolve=(_ctx, (), charId, name, _appearsIn) =>
        StarWarsData.updateCharacterName(charId, name)
        |> Js.Promise.(then_(x => resolve(Ok(x))))
      ),
    ])
  );

let schema: Schema.schema(unit) = Schema.create(query, ~mutation);


================================================
FILE: reason-graphql/__tests__/parser_test.re
================================================
open Jest;
open Graphql_Language;

describe("Parse and print a graphql query", () => {
  open Expect;

  let query = {|
    query My1stQuery($id: Int, $another: [String!]!) { field {
                hello: name
                stringField(name:"\"test\"",nameagain:"汉字")
        # Commentsssss
                age(default: 4.5, a: [4, 5, 5])
        ... on Foo {
                bar(h:{hello:5,nested:{world:1}})
                      ... on BarType {
                baz    @skip(if: $another) @skip(if: $another)
                ...bazFields
        }
        }
      }
    }

    fragment bazFields on FragmentModel {
      foo
      baz {
                g @skip(if: $another)
        ... on Bar {
          # dfffdfdfd
          b @skip(if: $another) @skip(if: {a: [1, 3, 4]})
                  c
            a
        }
      }
    }
 |};

  let document = Parser.parse(query);
  let out = document->Belt.Result.getExn->Printer.print;

  test("Should prettify the query correctly", () => {
    let pretty = {|query My1stQuery($id: Int, $another: [String!]!) {
  field {
    hello: name
    stringField(name: "\"test\"", nameagain: "汉字")
    age(default: 4.5, a: [4, 5, 5])
    ... on Foo {
      bar(h: {hello: 5, nested: {world: 1}})
      ... on BarType {
        baz @skip(if: $another) @skip(if: $another)
        ...bazFields
      }
    }
  }
}

fragment bazFields on FragmentModel {
  foo
  baz {
    g @skip(if: $another)
    ... on Bar {
      b @skip(if: $another) @skip(if: {a: [1, 3, 4]})
      c
      a
    }
  }
}|};

    expect(out) |> toBe(pretty);
  });
});

describe("Ast mapper", () => {
  let query = {|query My1stQuery($id: Int, $another: [String!]!) {
  field {
    hello: name
    stringField(name: "\"test\"", nameagain: "汉字")
    age(default: 4.5, a: [4, 5, 5])
    ... on Foo {
      bar(h: {hello: 5, nested: {world: 1}})
      ... on BarType {
        baz @skip(if: $another) @skip(if: $another)
        ...bazFields
      }
    }
  }
}

fragment bazFields on FragmentModel {
  foo
  baz {
    g @skip(if: $another)
    ... on Bar {
      b @skip(if: $another) @skip(if: {a: [1, 3, 4]})
      c
      a
    }
  }
}|};

  let expected = {|query My1stQuery($id: Int, $another: [String!]!) {
  HI {
    hello: HI
    HI(name: "\"test\"", nameagain: "汉字")
    HI(default: 4.5, a: [4, 5, 5])
    ... on Foo {
      HI(h: {hello: 5, nested: {world: 1}})
      ... on BarType {
        HI @skip(if: $another) @skip(if: $another)
        ...bazFields
      }
    }
  }
}

fragment bazFields on FragmentModel {
  HI
  HI {
    HI @skip(if: $another)
    ... on Bar {
      HI @skip(if: $another) @skip(if: {a: [1, 3, 4]})
      HI
      HI
    }
  }
}|};

  test("default mapper", () => {
    open Expect;

    let document = Parser.parse(query)->Belt.Result.getExn;

    let modified = Ast.visit(~enter=Ast.defaultMapper, document);

    let out = Printer.print(modified);

    expect(out) |> toBe(query);
  });

  test("enter", () => {
    open Expect;

    let document = Parser.parse(query)->Belt.Result.getExn;

    let modified =
      Ast.visit(
        ~enter={
          ...Ast.defaultMapper,
          field: field => {...field, name: "HI"},
        },
        document,
      );

    let out = Printer.print(modified);

    expect(out) |> toBe(expected);
  });

    test("leave", () => {
    open Expect;

    let document = Parser.parse(query)->Belt.Result.getExn;

    let modified =
      Ast.visit(
        ~leave={
          ...Ast.defaultMapper,
          field: field => {...field, name: "HI"},
        },
        document,
      );

    let out = Printer.print(modified);

    expect(out) |> toBe(expected);
  });
});

================================================
FILE: reason-graphql/__tests__/schema_test.re
================================================
open Jest;
open Expect;
open Graphql.Language;
open GraphqlJsPromise;

let schema = StarWarsSchema.schema;

let okResponse = a => `Object([("data", a)]);

describe("Basic Queries", () => {
  testAsync("Correctly identifies R2-D2 as the hero of the Star Wars Saga", assertion => {
    let query = {|
      query HeroNameQuery {
        hero {
          name
        }
      }
    |};

    let expected =
      okResponse(`Object([("hero", `Object([("name", `String("R2-D2"))]))]))
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });

  testAsync("Allows us to query for the ID and friends of R2-D2", assertion => {
    let query = {|
        query HeroNameAndFriendsQuery {
          hero {
            id
            name
            friends {
              name
            }
          }
        }
    |};

    let expected =
      okResponse(
        `Object([
          (
            "hero",
            `Object([
              ("id", `Int(2001)),
              ("name", `String("R2-D2")),
              (
                "friends",
                `List([
                  `Object([("name", `String("Luke Skywalker"))]),
                  `Object([("name", `String("Han Solo"))]),
                  `Object([("name", `String("Leia Organa"))]),
                ]),
              ),
            ]),
          ),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });
});

describe("Nested Queries", () =>
  testAsync("Allows us to query for the friends of friends of R2-D2", assertion => {
    let query = {|
      query NestedQuery {
        hero {
          name
          friends {
            name
            appearsIn
            friends {
              name
            }
          }
        }
      }
    |};

    let expected =
      okResponse(
        `Object([
          (
            "hero",
            `Object([
              ("name", `String("R2-D2")),
              (
                "friends",
                `List([
                  `Object([
                    ("name", `String("Luke Skywalker")),
                    (
                      "appearsIn",
                      `List([`String("NEWHOPE"), `String("EMPIRE"), `String("JEDI")]),
                    ),
                    (
                      "friends",
                      `List([
                        `Object([("name", `String("Han Solo"))]),
                        `Object([("name", `String("Leia Organa"))]),
                        `Object([("name", `String("C-3PO"))]),
                        `Object([("name", `String("R2-D2"))]),
                      ]),
                    ),
                  ]),
                  `Object([
                    ("name", `String("Han Solo")),
                    (
                      "appearsIn",
                      `List([`String("NEWHOPE"), `String("EMPIRE"), `String("JEDI")]),
                    ),
                    (
                      "friends",
                      `List([
                        `Object([("name", `String("Luke Skywalker"))]),
                        `Object([("name", `String("Leia Organa"))]),
                        `Object([("name", `String("R2-D2"))]),
                      ]),
                    ),
                  ]),
                  `Object([
                    ("name", `String("Leia Organa")),
                    (
                      "appearsIn",
                      `List([`String("NEWHOPE"), `String("EMPIRE"), `String("JEDI")]),
                    ),
                    (
                      "friends",
                      `List([
                        `Object([("name", `String("Luke Skywalker"))]),
                        `Object([("name", `String("Han Solo"))]),
                        `Object([("name", `String("C-3PO"))]),
                        `Object([("name", `String("R2-D2"))]),
                      ]),
                    ),
                  ]),
                ]),
              ),
            ]),
          ),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  })
);

describe("Mutation operation", () => {
  open Expect;

  let mutation = {|
    mutation MyMutation($id: Int!, $name: String!, $appearsIn: String){
      updateCharacterName(characterId: $id, name: $name, appearsIn: $appearsIn) {
        character {
          id
          name
        }
        error
      }
    }
  |};

  let variables =
    "{\"id\": 1000, \"name\": \"Sikan Skywalker\"}"
    ->Js.Json.parseExn
    ->Graphql_Json.toVariables
    ->Belt.Result.getExn;

  let result =
    schema
    |> Schema.execute(
         _,
         ~document=Parser.parse(mutation)->Belt.Result.getExn,
         ~ctx=(),
         ~variables,
       )
    |> Schema.resultToJson;

  testAsync("returns the right data", assertion => {
    let expected =
      okResponse(
        `Object([
          (
            "updateCharacterName",
            `Object([
              (
                "character",
                `Object([("id", `Int(1000)), ("name", `String("Sikan Skywalker"))]),
              ),
              ("error", `Null),
            ]),
          ),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    Schema.Io.map(result, res => assertion(expect(res) |> toEqual(expected)))->ignore;
  });
});

describe("Using aliases to change the key in the response", () => {
  open Expect;

  testAsync("Allows us to query for Luke, changing his key with an alias", assertion => {
    let query = {|
      query FetchLukeAliased {
        luke: human(id: 1000) {
          name
        }
      }
    |};

    let expected =
      okResponse(`Object([("luke", `Object([("name", `String("Luke Skywalker"))]))]))
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });

  testAsync(
    "Allows us to query for both Luke and Leia, using two root fields and an alias", assertion => {
    let query = {|
      query FetchLukeAndLeiaAliased {
        luke: human(id: 1000) {
          name
        }
        leia: human(id: 1003) {
          name
        }
      }
    |};

    let expected =
      okResponse(
        `Object([
          ("luke", `Object([("name", `String("Luke Skywalker"))])),
          ("leia", `Object([("name", `String("Leia Organa"))])),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });
});

describe("Uses fragments to express more complex queries", () => {
  open Expect;

  testAsync("Allows us to query using duplicated content", assertion => {
    let query = {|
      query DuplicateFields {
        luke: human(id: 1000) {
          name
          homePlanet
        }
        leia: human(id: 1003) {
          name
          homePlanet
        }
      }
    |};

    let expected =
      okResponse(
        `Object([
          (
            "luke",
            `Object([
              ("name", `String("Luke Skywalker")),
              ("homePlanet", `String("Tatooine")),
            ]),
          ),
          (
            "leia",
            `Object([("name", `String("Leia Organa")), ("homePlanet", `String("Alderaan"))]),
          ),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });

  testAsync("Allows us to use a fragment to avoid duplicating content", assertion => {
    let query = {|
    query UseFragment {
      luke: human(id: 1000) {
        ...HumanFragment
      }
      leia: human(id: 1003) {
        ...HumanFragment
      }
    }

    fragment HumanFragment on Human {
      name
      homePlanet
    }
  |};

    let schema = StarWarsSchema.schema;

    let expected =
      okResponse(
        `Object([
          (
            "luke",
            `Object([
              ("name", `String("Luke Skywalker")),
              ("homePlanet", `String("Tatooine")),
            ]),
          ),
          (
            "leia",
            `Object([("name", `String("Leia Organa")), ("homePlanet", `String("Alderaan"))]),
          ),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });

  testAsync("Allows us to use __typename to get a containing object's type name", assertion => {
    let query = {|
    query UseFragment {
      luke: human(id: 1000) {
        __typename
      }
    }
  |};

    let schema = StarWarsSchema.schema;

    let expected =
      okResponse(`Object([("luke", `Object([("__typename", `String("Human"))]))]))
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  });
});

describe("introspection query", () => {
  testAsync("should reply with queries, mutations, and subscriptions", assertion => {
    let query = {|
      query IntrospectionQuery {
        __schema {
          queryType {
            name
            fields {
              name
            }
          }
          mutationType {
            name
            fields {
              name
            }
          }
          subscriptionType {
            name
            fields {
              name
            }
          }
        }
      }
    |};

    let expected =
      okResponse(
        `Object([
          (
            "__schema",
            `Object([
              (
                "queryType",
                `Object([
                  ("name", `String("Query")),
                  (
                    "fields",
                    `List([
                      `Object([("name", `String("hero"))]),
                      `Object([("name", `String("human"))]),
                      `Object([("name", `String("droid"))]),
                    ]),
                  ),
                ]),
              ),
              (
                "mutationType",
                `Object([
                  ("name", `String("Mutation")),
                  ("fields", `List([`Object([("name", `String("updateCharacterName"))])])),
                ]),
              ),
              ("subscriptionType", `Null),
            ]),
          ),
        ]),
      )
      |> Graphql_Json.fromConstValue;

    schema
    ->Schema.execute(~document=Parser.parse(query)->Belt.Result.getExn, ~ctx=())
    ->Schema.resultToJson
    ->Schema.Io.map(res => assertion(expect(res) |> toEqual(expected)))
    ->ignore;
  })
});


================================================
FILE: reason-graphql/bsconfig.json
================================================
{
  "name": "reason-graphql",
  "version": "0.6.1",
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    },
    {
      "dir": "__tests__",
      "type": "dev",
      "subdirs": true
    }
  ],
  "package-specs": {
    "module": "commonjs",
    "in-source": true
  },
  "suffix": ".bs.js",
  "bs-dependencies": [],
  "bs-dev-dependencies": ["@glennsl/bs-jest"],
  "ppx-flags": ["bs-let/ppx"],
  "warnings": {
    "number": "-45-44-30",
    "error": "+101"
  },
  "namespace": false,
  "refmt": 3
}


================================================
FILE: reason-graphql/package.json
================================================
{
  "name": "reason-graphql",
  "version": "0.6.1",
  "author": "Sikan He",
  "license": "MIT",
  "repository": "https://github.com/sikanhe/reason-graphql",
  "homepage": "https://github.com/sikanhe/reason-graphql#readme",
  "keywords": [
    "BuckleScript",
    "GraphQL",
    "Reason",
    "ReasonML"
  ],
  "scripts": {
    "build": "bsb -make-world",
    "start": "bsb -make-world -w",
    "clean": "bsb -clean-world",
    "test": "yarn build && jest"
  },
  "files": [
    "src/",
    "bsconfig.json"
  ],
  "jest": {
    "testMatch": [
      "**/?(*_)(spec|test).bs.js?(x)"
    ]
  },
  "devDependencies": {
    "@glennsl/bs-jest": "^0.4.9",
    "bs-platform": "^8.2.0"
  },
  "dependencies": {
    "bs-let": "^0.1.11"
  }
}


================================================
FILE: reason-graphql/src/Graphql.re
================================================
module Schema = Graphql_Schema;
module Language = Graphql_Language;
module Json = Graphql_Json;
module Interface = Graphql_Interface;


================================================
FILE: reason-graphql/src/Graphql_Interface.re
================================================
module type IO = {
  type t(+'a);
  let return: 'a => t('a);
  let bind: (t('a), 'a => t('b)) => t('b);
  let map: (t('a), 'a => 'b) => t('b);
};

module type Schema = {
  module Io: IO;

  type variableList = list((string, Graphql_Language.Ast.constValue));

  type deprecation =
    | NotDeprecated
    | Deprecated(option(string));
  type enumValue('a) = {
    name: string,
    description: option(string),
    deprecated: deprecation,
    value: 'a,
  };
  type enum('a) = {
    name: string,
    description: option(string),
    values: list(enumValue('a)),
  };
  module Arg: {
    type arg(_) =
      | Arg(argument('a)): arg('a)
      | DefaultArg(argumentWithDefault('a)): arg('a)
    and argTyp(_) =
      | Scalar(scalar('a)): argTyp(option('a))
      | Enum(enum('a)): argTyp(option('a))
      | InputObject(inputObject('a, 'b)): argTyp(option('a))
      | List(argTyp('a)): argTyp(option(array('a)))
      | NonNull(argTyp(option('a))): argTyp('a)
    and scalar('a) = {
      name: string,
      description: option(string),
      parse: Graphql_Language.Ast.constValue => result('a, string),
    }
    and inputObject('a, 'b) = {
      name: string,
      description: option(string),
      fields: arglist('a, 'b),
      coerce: 'b,
    }
    and argument('a) = {
      name: string,
      description: option(string),
      typ: argTyp('a),
    }
    and argumentWithDefault('a) = {
      name: string,
      description: option(string),
      typ: argTyp(option('a)),
      default: 'a,
    }
    and arglist(_, _) =
      | []: arglist('a, 'a)
      | ::(arg('a), arglist('b, 'c)): arglist('b, 'a => 'c);
    let arg: (~description: string=?, string, argTyp('a)) => arg('a);
    let defaultArg:
      (~description: string=?, ~default: 'a, string, argTyp(option('a))) =>
      arg('a);
    let string: argTyp(option(string));
    let int: argTyp(option(int));
    let float: argTyp(option(float));
    let boolean: argTyp(option(bool));
    let list: argTyp('a) => argTyp(option(array('a)));
    let nonnull: argTyp(option('a)) => argTyp('a);
  };
  type typ(_, _) =
    | Scalar(scalar('src)): typ('ctx, option('src))
    | Enum(enum('src)): typ('ctx, option('src))
    | List(typ('ctx, 'src)): typ('ctx, option(array('src)))
    | Object(obj('ctx, 'src)): typ('ctx, option('src))
    | Abstract(abstract): typ('ctx, option(abstractValue('ctx, 'a)))
    | NonNull(typ('ctx, option('src))): typ('ctx, 'src)
  and scalar('src) = {
    name: string,
    description: option(string),
    serialize: 'src => Graphql_Language.Ast.constValue,
  }
  and obj('ctx, 'src) = {
    name: string,
    description: option(string),
    fields: Lazy.t(list(field('ctx, 'src))),
    abstracts: ref(list(abstract)),
  }
  and field(_, _) =
    | Field(fieldDefinition('src, 'out, 'ctx, 'a, 'args)): field('ctx, 'src)
  and fieldDefinition('src, 'out, 'ctx, 'a, 'args) = {
    name: string,
    description: option(string),
    deprecated: deprecation,
    typ: typ('ctx, 'out),
    args: Arg.arglist('a, 'args),
    resolve: ('ctx, 'src) => 'args,
    lift: 'a => Io.t(result('out, string)),
  }
  and anyTyp =
    | AnyTyp(typ('a, 'b)): anyTyp
    | AnyArgTyp(Arg.argTyp('c)): anyTyp
  and abstract = {
    name: string,
    description: option(string),
    mutable types: list(anyTyp),
    kind: [ | `Interface(Lazy.t(list(abstractField))) | `Union],
  }
  and abstractField =
    | AbstractField(field('a, 'b)): abstractField
  and abstractValue('ctx, 'a) =
    | AbstractValue((typ('ctx, option('src)), 'src))
      : abstractValue('ctx, 'a);
  type abstractType('ctx, 'a) = typ('ctx, option(abstractValue('ctx, 'a)));
  type directiveLocation = [
    | `Field
    | `FragmentDefinition
    | `FragmentSpread
    | `InlineFragment
    | `Mutation
    | `Query
    | `Subscription
    | `VariableDefinition
  ];
  type directiveInfo('args) = {
    name: string,
    description: option(string),
    locations: list(directiveLocation),
    args: Arg.arglist([ | `Include | `Skip], 'args),
    resolve: 'args,
  };
  type directive =
    | Directive(directiveInfo('args)): directive;
  let skipDirective: directive;
  let includeDirective: directive;
  type schema('ctx) = {
    query: obj('ctx, unit),
    mutation: option(obj('ctx, unit)),
  };
  type combinedEnum('ctx, 'a) = {
    argTyp: Arg.argTyp('a),
    fieldType: typ('ctx, 'a),
  };
  let makeEnum:
    (string, ~description: string=?, list(enumValue('a))) =>
    combinedEnum('b, option('a));
  let enumValue:
    (~description: string=?, ~deprecated: deprecation=?, ~value: 'a, string) =>
    enumValue('a);
  let obj:
    (
      ~description: string=?,
      ~implements: ref(list(abstract))=?,
      ~fields: typ('a, option('b)) => list(field('a, 'b)),
      string
    ) =>
    typ('a, option('b));
  let field:
    (
      ~description: string=?,
      ~deprecated: deprecation=?,
      ~args: Arg.arglist('a, 'b),
      ~resolve: ('c, 'd) => 'b,
      string,
      typ('c, 'a)
    ) =>
    field('c, 'd);
  let async_field:
    (
      ~description: string=?,
      ~deprecated: deprecation=?,
      ~args: Arg.arglist(Io.t(result('a, string)), 'b),
      ~resolve: ('c, 'd) => 'b,
      string,
      typ('c, 'a)
    ) =>
    field('c, 'd);
  let abstractField:
    (
      ~description: string=?,
      ~deprecated: deprecation=?,
      ~args: Arg.arglist('a, 'b),
      string,
      typ('c, 'a)
    ) =>
    abstractField;
  let union:
    (~description: string=?, string) =>
    typ('a, option(abstractValue('a, 'b)));
  let interface:
    (
      ~description: string=?,
      ~fields: typ('a, option(abstractValue('a, 'b))) =>
               list(abstractField),
      string
    ) =>
    typ('a, option(abstractValue('a, 'b)));
  let addType:
    (typ('a, option(abstractValue('a, 'b))), typ('c, option('d)), 'd) =>
    abstractValue('c, 'e);
  let query: list(field('ctx, unit)) => obj('ctx, unit);
  let mutation: list(field('ctx, unit)) => obj('ctx, unit);
  let create: (~mutation: obj('a, unit)=?, obj('a, unit)) => schema('a);
  let string: typ('ctx, option(string));
  let int: typ('ctx, option(int));
  let float: typ('ctx, option(float));
  let boolean: typ('ctx, option(bool));
  let list: typ('a, 'b) => typ('a, option(array('b)));
  let nonnull: typ('a, option('b)) => typ('a, 'b);

  type path = list(string);
  
  type error = (string, path);

  type resolveError = [
    | `ArgumentError(string)
    | `ResolveError(error)
    | `ValidationError(string)
  ];
  type executeError = [
    | `ArgumentError(string)
    | `MutationsNotConfigured
    | `NoOperationFound
    | `OperationNameRequired
    | `OperationNotFound
    | `ResolveError(error)
    | `SubscriptionsNotConfigured
    | `ValidationError(string)
  ];

  let execute:
    (
      ~variables: variableList=?,
      ~document: Graphql_Language.Ast.document,
      schema('ctx),
      ~ctx: 'ctx
    ) =>
    Io.t(Graphql_Language.Ast.constValue);
    
  let resultToJson: Io.t(Graphql_Language.Ast.constValue) => Io.t(Js.Json.t);
};


================================================
FILE: reason-graphql/src/Graphql_Json.re
================================================
let rec fromConstValue: Graphql_Language_Ast.constValue => Js.Json.t =
  fun
  | `String(string)
  | `Enum(string) => Js.Json.string(string)
  | `Float(float) => Js.Json.number(float)
  | `Int(int) => Js.Json.number(float_of_int(int))
  | `Boolean(bool) => Js.Json.boolean(bool)
  | `List(list) =>
    Belt.List.map(list, item => fromConstValue(item)) |> Belt.List.toArray |> Js.Json.array
  | `Object(rows) => {
      let dict =
        Belt.List.reduceReverse(
          rows,
          Js.Dict.empty(),
          (dict, (name, value)) => {
            Js.Dict.set(dict, name, fromConstValue(value));
            dict;
          },
        );
      Js.Json.object_(dict);
    }
  | `Null => Js.Json.null;

let rec toConstValue = (json: Js.Json.t): Graphql_Language_Ast.constValue =>
  switch (Js.Json.classify(json)) {
  | JSONString(value) => `String(value)
  | JSONNumber(num) when Js.Math.floor_float(num) == num => `Int(int_of_float(num))
  | JSONNumber(num) => `Float(num)
  | JSONTrue => `Boolean(true)
  | JSONFalse => `Boolean(false)
  | JSONNull => `Null
  | JSONArray(array) =>
    `List(Belt.Array.map(array, item => toConstValue(item)) |> Belt.List.fromArray)
  | JSONObject(dict) =>
    `Object(
      Js.Dict.entries(dict)
      ->Belt.Array.map(((k, v)) => (k, toConstValue(v)))
      ->Belt.List.fromArray,
    )
  };

let toVariables =
    (json: Js.Json.t): Belt.Result.t(list((string, Graphql_Language_Ast.constValue)), string) => {
  switch (Js.Json.classify(json)) {
  | JSONObject(dict) =>
    Ok(
      Js.Dict.entries(dict)
      ->Belt.Array.map(((k, v)) => (k, toConstValue(v)))
      ->Belt.List.fromArray
    )
  | _ => Error("Variables must be a JSON object")
  };
};

================================================
FILE: reason-graphql/src/Graphql_Schema.re
================================================
open Graphql_Language;

module Result = Belt.Result;
module Option = Belt.Option;

module List = {
  include Belt.List;

  module Result = {
    include Belt.Result;

    let rec join = (~memo=[]) =>
      fun
      | [] => Ok(Belt.List.reverse(memo))
      | [Error(_) as err, ..._] => err
      | [Ok(x), ...xs] => join(~memo=[x, ...memo], xs);

    let all = (list, f) => Belt.List.map(list, f) |> join;
  };
};

module StringMap = Belt.Map.String;
module StringSet = Set.Make(String);

type variableMap = StringMap.t(Ast.constValue);
type fragmentMap = StringMap.t(Ast.fragmentDefinition);

module type IO = {
  type t(+'a);
  let return: 'a => t('a);
  let bind: (t('a), 'a => t('b)) => t('b);
};

let id: 'a. 'a => 'a = x => x;

module Make = (Io: IO) => {
  open Result;

  module Io = {
    include Io;

    let ok = x => return(Ok(x));
    let error = x => return(Error(x));
    let map = (x, f) => bind(x, x' => return(f(x')));

    let rec all =
      fun
      | [] => return([])
      | [x, ...xs] => bind(all(xs), xs' => map(x, x' => [x', ...xs']));

    module Result = {
      let bind = (x, f) =>
        bind(
          x,
          fun
          | Ok(x') => f(x')
          | Error(_) as err => return(err),
        );

      let mapError = (x, f) =>
        map(
          x,
          fun
          | Ok(_) as ok => ok
          | Error(err) => Error(f(err)),
        );

      let map = (x, f) =>
        map(
          x,
          fun
          | Ok(x') => Ok(f(x'))
          | Error(_) as err => err,
        );

      let let_ = bind;
    };

    let rec mapSerial = (~memo=[], list, f) =>
      switch (list) {
      | [] => Io.return(List.reverse(memo))
      | [x, ...xs] => bind(f(x), x' => mapSerial(~memo=[x', ...memo], xs, f))
      };

    let mapParalell = (list, f) => list->List.map(f)->all;

    let let_ = bind;
  };

  type variableList = list((string, Ast.constValue));

  type deprecation =
    | NotDeprecated
    | Deprecated(option(string));

  type enumValue('a) = {
    name: string,
    description: option(string),
    deprecated: deprecation,
    value: 'a,
  };

  type enum('a) = {
    name: string,
    description: option(string),
    values: list(enumValue('a)),
  };

  module Arg = {
    type arg(_) =
      | Arg(argument('a)): arg('a)
      | DefaultArg(argumentWithDefault('a)): arg('a)
    and argTyp(_) =
      | Scalar(scalar('a)): argTyp(option('a))
      | Enum(enum('a)): argTyp(option('a))
      | InputObject(inputObject('a, 'b)): argTyp(option('a))
      | List(argTyp('a)): argTyp(option(array('a)))
      | NonNull(argTyp(option('a))): argTyp('a)
    and scalar('a) = {
      name: string,
      description: option(string),
      parse: Ast.constValue => Result.t('a, string),
    }
    and inputObject('a, 'b) = {
      name: string,
      description: option(string),
      fields: arglist('a, 'b),
      coerce: 'b,
    }
    and argument('a) = {
      name: string,
      description: option(string),
      typ: argTyp('a),
    }
    and argumentWithDefault('a) = {
      name: string,
      description: option(string),
      typ: argTyp(option('a)),
      default: 'a,
    }
    and arglist(_, _) =
      | []: arglist('a, 'a)
      | ::(arg('a), arglist('b, 'c)): arglist('b, 'a => 'c);

    let arg = (~description=?, name, typ) => Arg({name, typ, description});

    let defaultArg = (~description=?, ~default, name, typ) =>
      DefaultArg({name, typ, description, default});

    /* Built in scalars */

    let string =
      Scalar({
        name: "String",
        description: None,

        parse: input =>
          switch (input) {
          | `String(str) => Ok(str)
          | _ => Error("Invalid string")
          },
      });

    let int =
      Scalar({
        name: "Int",
        description: None,
        parse: input =>
          switch (input) {
          | `Int(int) => Ok(int)
          | _ => Error("Invalid integer")
          },
      });

    let float =
      Scalar({
        name: "Float",
        description: None,
        parse: input =>
          switch (input) {
          | `Float(float) => Ok(float)
          | _ => Error("Invalid float")
          },
      });

    let boolean =
      Scalar({
        name: "Boolean",
        description: None,
        parse: input =>
          switch (input) {
          | `Boolean(bool) => Ok(bool)
          | _ => Error("Invalid boolean")
          },
      });

    let list = a => List(a);
    let nonnull = a => NonNull(a);
  };

  type typ(_, _) =
    | Scalar(scalar('src)): typ('ctx, option('src))
    | Enum(enum('src)): typ('ctx, option('src))
    | List(typ('ctx, 'src)): typ('ctx, option(array('src)))
    | Object(obj('ctx, 'src)): typ('ctx, option('src))
    | Abstract(abstract): typ('ctx, option(abstractValue('ctx, 'a)))
    | NonNull(typ('ctx, option('src))): typ('ctx, 'src)
  and scalar('src) = {
    name: string,
    description: option(string),
    serialize: 'src => Ast.constValue,
  }
  and obj('ctx, 'src) = {
    name: string,
    description: option(string),
    fields: Lazy.t(list(field('ctx, 'src))),
    abstracts: ref(list(abstract)),
  }
  and field(_, _) =
    | Field(fieldDefinition('src, 'out, 'ctx, 'a, 'args)): field('ctx, 'src)
  and fieldDefinition('src, 'out, 'ctx, 'a, 'args) = {
    name: string,
    description: option(string),
    deprecated: deprecation,
    typ: typ('ctx, 'out),
    args: Arg.arglist('a, 'args),
    resolve: ('ctx, 'src) => 'args,
    lift: 'a => Io.t(Result.t('out, string)),
  }
  and anyTyp =
    | AnyTyp(typ(_, _)): anyTyp
    | AnyArgTyp(Arg.argTyp(_)): anyTyp
  and abstract = {
    name: string,
    description: option(string),
    mutable types: list(anyTyp),
    kind: [ | `Union | `Interface(Lazy.t(list(abstractField)))],
  }
  and abstractField =
    | AbstractField(field(_, _)): abstractField
  and abstractValue('ctx, 'a) =
    | AbstractValue((typ('ctx, option('src)), 'src)): abstractValue('ctx, 'a);

  type abstractType('ctx, 'a) = typ('ctx, option(abstractValue('ctx, 'a)));

  type directiveLocation = [
    | `Query
    | `Mutation
    | `Subscription
    | `Field
    | `FragmentDefinition
    | `FragmentSpread
    | `InlineFragment
    | `VariableDefinition
  ];

  type directiveInfo('args) = {
    name: string,
    description: option(string),
    locations: list(directiveLocation),
    args: Arg.arglist([ | `Skip | `Include], 'args),
    resolve: 'args,
  };

  type directive =
    | Directive(directiveInfo('args)): directive;

  let skipDirective =
    Directive({
      name: "skip",
      description:
        Some(
          "Directs the executor to skip this field or fragment when the `if` argument is true.",
        ),
      locations: [`Field, `FragmentSpread, `InlineFragment],
      args: Arg.[arg("if", nonnull(boolean), ~description="Skipped when true.")],
      resolve:
        fun
        | true => `Skip
        | false => `Include,
    });

  let includeDirective =
    Directive({
      name: "include",
      description:
        Some(
          "Directs the executor to include this field or fragment only when the `if` argument is true.",
        ),
      locations: [`Field, `FragmentSpread, `InlineFragment],
      args: Arg.[arg("if", nonnull(boolean), ~description="Included when true.")],
      resolve:
        fun
        | true => `Include
        | false => `Skip,
    });

  type schema('ctx) = {
    query: obj('ctx, unit),
    mutation: option(obj('ctx, unit)),
  };

  type combinedEnum('ctx, 'a) = {
    argTyp: Arg.argTyp('a),
    fieldType: typ('ctx, 'a),
  };

  let makeEnum = (name, ~description=?, values) => {
    argTyp: Arg.Enum({name, description, values}),
    fieldType: Enum({name, description, values}),
  };

  let enumValue = (~description=?, ~deprecated=NotDeprecated, ~value, name) => {
    name,
    description,
    deprecated,
    value,
  };

  let obj = (~description=?, ~implements: ref(list(abstract))=ref([]), ~fields, name) => {
    let rec self =
      Object({name, description, fields: lazy(fields(self)), abstracts: implements});
    self;
  };

  let field = (~description=?, ~deprecated=NotDeprecated, ~args, ~resolve, name, typ) =>
    Field({name, typ, resolve, deprecated, description, args, lift: Io.ok});

  let async_field = (~description=?, ~deprecated=NotDeprecated, ~args, ~resolve, name, typ) =>
    Field({name, typ, resolve, deprecated, description, args, lift: id});

  let abstractField = (~description=?, ~deprecated=NotDeprecated, ~args, name, typ) =>
    AbstractField(
      Field({lift: Io.ok, name, description, deprecated, typ, args, resolve: Obj.magic()}),
    );

  let union = (~description=?, name) => Abstract({name, description, types: [], kind: `Union});

  let interface = (~description=?, ~fields, name) => {
    let rec t = Abstract({name, description, types: [], kind: `Interface(lazy(fields(t)))});
    t;
  };

  let addType = (abstractType, typ) => {
    switch (abstractType, typ) {
    | (Abstract(a), Object(o)) =>
      a.types = [AnyTyp(typ), ...a.types];
      o.abstracts := [a, ...o.abstracts^];
      (src => AbstractValue((typ, src)));
    | _ => invalid_arg("Arguments must be Interface/Union and Object")
    };
  };

  let query = (fields): obj('ctx, unit) => {
    name: "Query",
    description: None,
    fields: lazy(fields),
    abstracts: ref([]),
  };

  let mutation = (fields): obj('ctx, unit) => {
    name: "Mutation",
    description: None,
    fields: lazy(fields),
    abstracts: ref([]),
  };

  let create = (~mutation=?, query) => {query, mutation};

  /* Built in scalars */
  let string: 'ctx. typ('ctx, option(string)) =
    Scalar({name: "String", description: None, serialize: str => `String(str)});
  let int: 'ctx. typ('ctx, option(int)) =
    Scalar({name: "Int", description: None, serialize: int => `Int(int)});
  let float: 'ctx. typ('ctx, option(float)) =
    Scalar({name: "Float", description: None, serialize: float => `Float(float)});
  let boolean: 'ctx. typ('ctx, option(bool)) =
    Scalar({name: "Boolean", description: None, serialize: bool => `Boolean(bool)});
  let list = typ => List(typ);
  let nonnull = typ => NonNull(typ);

  module Introspection = {
    /* anyTyp, anyField and anyArg hide type parameters to avoid scope escaping errors */
    type anyField =
      | AnyField(field(_, _)): anyField
      | AnyArgField(Arg.arg(_)): anyField;
    type anyArg =
      | AnyArg(Arg.arg(_)): anyArg;
    type anyEnumValue =
      | AnyEnumValue(enumValue(_)): anyEnumValue;

    let unlessVisited = ((result, visited), name, f) =>
      if (StringSet.mem(name, visited)) {
        (result, visited);
      } else {
        f((result, visited));
      };

    /* Extracts all types contained in a single type */


    let rec types:
      type ctx src.
        (~memo: (list(anyTyp), StringSet.t)=?, typ(ctx, src)) => (list(anyTyp), StringSet.t) =
      (~memo=([], StringSet.empty), typ) =>
        switch (typ) {
        | List(typ) => types(~memo, typ)
        | NonNull(typ) => types(~memo, typ)
        | Scalar(s) as scalar =>
          unlessVisited(memo, s.name, ((result, visited)) =>
            ([AnyTyp(scalar), ...result], StringSet.add(s.name, visited))
          )
        | Enum(e) as enum =>
          unlessVisited(memo, e.name, ((result, visited)) =>
            ([AnyTyp(enum), ...result], StringSet.add(e.name, visited))
          )
        | Object(o) as obj =>
          unlessVisited(
            memo,
            o.name,
            ((result, visited)) => {
              let result' = [AnyTyp(obj), ...result];
              let visited' = StringSet.add(o.name, visited);
              let reducer = (memo, Field(f)) => {
                let memo' = types(~memo, f.typ);
                arg_list_types(memo', f.args);
              };

              List.reduceReverse(Lazy.force(o.fields), (result', visited'), reducer);
            },
          )
        | Abstract(a) as abstract =>
          unlessVisited(
            memo,
            a.name,
            ((result, visited)) => {
              let result' = [AnyTyp(abstract), ...result];
              let visited' = StringSet.add(a.name, visited);
              List.reduceReverse(a.types, (result', visited'), (memo, typ) =>
                switch (typ) {
                | AnyTyp(typ) => types(~memo, typ)
                | AnyArgTyp(_) => failwith("Abstracts can't have argument types")
                }
              );
            },
          )
        }

    and arg_types:
      type a. ((list(anyTyp), StringSet.t), Arg.argTyp(a)) => (list(anyTyp), StringSet.t) =
      (memo, argtyp) =>
        switch (argtyp) {
        | Arg.List(typ) => arg_types(memo, typ)
        | Arg.NonNull(typ) => arg_types(memo, typ)
        | Arg.Scalar(s) as scalar =>
          unlessVisited(memo, s.name, ((result, visited)) =>
            ([AnyArgTyp(scalar), ...result], StringSet.add(s.name, visited))
          )
        | Arg.Enum(e) as enum =>
          unlessVisited(memo, e.name, ((result, visited)) =>
            ([AnyArgTyp(enum), ...result], StringSet.add(e.name, visited))
          )
        | Arg.InputObject(o) as obj =>
          unlessVisited(
            memo,
            o.name,
            ((result, visited)) => {
              let memo' = ([AnyArgTyp(obj), ...result], StringSet.add(o.name, visited));
              arg_list_types(memo', o.fields);
            },
          )
        }

    and arg_list_types:
      type a b.
        ((list(anyTyp), StringSet.t), Arg.arglist(a, b)) => (list(anyTyp), StringSet.t) =
      (memo, arglist) =>
        Arg.(
          switch (arglist) {
          | [] => memo
          | [arg, ...args] =>
            let memo' =
              switch (arg) {
              | Arg(a) => arg_types(memo, a.typ)
              | DefaultArg(a) => arg_types(memo, a.typ)
              };
            arg_list_types(memo', args);
          }
        );

    let rec args_to_list: type a b. (~memo: list(anyArg)=?, Arg.arglist(a, b)) => list(anyArg) =
      (~memo=[], arglist) =>
        Arg.(
          switch (arglist) {
          | [] => memo
          | [arg, ...args] =>
            let arg = AnyArg(arg);
            let memo': list(anyArg) = [arg, ...memo];
            args_to_list(~memo=memo', args);
          }
        );

    let no_abstracts = ref([]);

    let __type_kind =
      Enum({
        name: "__TypeKind",
        description: None,
        values: [
          {name: "SCALAR", description: None, deprecated: NotDeprecated, value: `Scalar},
          {name: "OBJECT", description: None, deprecated: NotDeprecated, value: `Object},
          {name: "INTERFACE", description: None, deprecated: NotDeprecated, value: `Interface},
          {name: "UNION", description: None, deprecated: NotDeprecated, value: `Union},
          {name: "ENUM", description: None, deprecated: NotDeprecated, value: `Enum},
          {
            name: "INPUT_OBJECT",
            description: None,
            deprecated: NotDeprecated,
            value: `InputObject,
          },
          {name: "LIST", description: None, deprecated: NotDeprecated, value: `List},
          {name: "NON_NULL", description: None, deprecated: NotDeprecated, value: `NonNull},
        ],
      });

    let __enumValue: 'ctx. typ('ctx, option(anyEnumValue)) =
      Object({
        name: "__EnumValue",
        description: None,
        abstracts: no_abstracts,
        fields:
          lazy([
            Field({
              name: "name",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(string),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, AnyEnumValue(enum_value)) => enum_value.name,
            }),
            Field({
              name: "description",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, AnyEnumValue(enum_value)) => {
                enum_value.description;
              },
            }),
            Field({
              name: "isDeprecated",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(boolean),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, AnyEnumValue(enum_value)) => enum_value.deprecated != NotDeprecated,
            }),
            Field({
              name: "deprecationReason",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, AnyEnumValue(enum_value)) =>
                switch (enum_value.deprecated) {
                | Deprecated(reason) => reason
                | NotDeprecated => None
                },
            }),
          ]),
      });

    let rec __input_value: 'ctx. typ('ctx, option(anyArg)) =
      Object({
        name: "__InputValue",
        description: None,
        abstracts: no_abstracts,
        fields:
          lazy([
            Field({
              name: "name",
              typ: NonNull(string),
              args: Arg.[],
              deprecated: NotDeprecated,
              description: None,
              lift: Io.ok,
              resolve: (_, AnyArg(arg)) =>
                switch (arg) {
                | Arg.DefaultArg(a) => a.name
                | Arg.Arg(a) => a.name
                },
            }),
            Field({
              name: "description",
              typ: string,
              args: Arg.[],
              deprecated: NotDeprecated,
              description: None,
              lift: Io.ok,
              resolve: (_, AnyArg(arg)) =>
                switch (arg) {
                | Arg.DefaultArg(a) => a.description
                | Arg.Arg(a) => a.description
                },
            }),
            Field({
              name: "type",
              typ: NonNull(__type),
              args: Arg.[],
              deprecated: NotDeprecated,
              description: None,
              lift: Io.ok,
              resolve: (_, AnyArg(arg)) =>
                switch (arg) {
                | Arg.DefaultArg(a) => AnyArgTyp(a.typ)
                | Arg.Arg(a) => AnyArgTyp(a.typ)
                },
            }),
            Field({
              name: "defaultValue",
              typ: string,
              args: Arg.[],
              deprecated: NotDeprecated,
              description: None,
              lift: Io.ok,
              resolve: (_, AnyArg(_)) => None,
            }),
          ]),
      })
    and __type: 'ctx. typ('ctx, option(anyTyp)) =
      Object({
        name: "__Type",
        description: None,
        abstracts: no_abstracts,
        fields:
          lazy([
            Field({
              name: "kind",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(__type_kind),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Object(_)) => `Object
                | AnyTyp(Abstract({kind: `Union, _})) => `Union
                | AnyTyp(Abstract({kind: `Interface(_), _})) => `Interface
                | AnyTyp(List(_)) => `List
                | AnyTyp(Scalar(_)) => `Scalar
                | AnyTyp(Enum(_)) => `Enum
                | AnyTyp(NonNull(_)) => `NonNull
                | AnyArgTyp(Arg.InputObject(_)) => `InputObject
                | AnyArgTyp(Arg.List(_)) => `List
                | AnyArgTyp(Arg.Scalar(_)) => `Scalar
                | AnyArgTyp(Arg.Enum(_)) => `Enum
                | AnyArgTyp(Arg.NonNull(_)) => `NonNull
                },
            }),
            Field({
              name: "ofType",
              description: None,
              deprecated: NotDeprecated,
              typ: __type,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(NonNull(typ)) => Some(AnyTyp(typ))
                | AnyTyp(List(typ)) => Some(AnyTyp(typ))
                | AnyArgTyp(Arg.NonNull(typ)) => Some(AnyArgTyp(typ))
                | AnyArgTyp(Arg.List(typ)) => Some(AnyArgTyp(typ))
                | _ => None
                },
            }),
            Field({
              name: "name",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Object(o)) => Some(o.name)
                | AnyTyp(Scalar(s)) => Some(s.name)
                | AnyTyp(Enum(e)) => Some(e.name)
                | AnyTyp(Abstract(a)) => Some(a.name)
                | AnyArgTyp(Arg.InputObject(o)) => Some(o.name)
                | AnyArgTyp(Arg.Scalar(s)) => Some(s.name)
                | AnyArgTyp(Arg.Enum(e)) => Some(e.name)
                | _ => None
                },
            }),
            Field({
              name: "description",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Object(o)) => o.description
                | AnyTyp(Scalar(s)) => s.description
                | AnyTyp(Enum(e)) => e.description
                | AnyTyp(Abstract(a)) => a.description
                | AnyArgTyp(Arg.InputObject(o)) => o.description
                | AnyArgTyp(Arg.Scalar(s)) => s.description
                | AnyArgTyp(Arg.Enum(e)) => e.description
                | _ => None
                },
            }),
            Field({
              name: "fields",
              description: None,
              deprecated: NotDeprecated,
              typ: List(NonNull(__field)),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Object(o)) =>
                  Some(List.map(Lazy.force(o.fields), f => AnyField(f))->List.toArray)
                | AnyTyp(Abstract({kind: `Interface(fields), _})) =>
                  Some(
                    List.map(Lazy.force(fields), (AbstractField(f)) => AnyField(f))
                    ->List.toArray,
                  )
                | AnyArgTyp(Arg.InputObject(o)) =>
                  let arg_list = args_to_list(o.fields);
                  Some(List.map(arg_list, (AnyArg(f)) => AnyArgField(f))->List.toArray);
                | _ => None
                },
            }),
            Field({
              name: "interfaces",
              description: None,
              deprecated: NotDeprecated,
              typ: List(NonNull(__type)),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Object(o)) =>
                  let interfaces =
                    List.keep(
                      o.abstracts^,
                      fun
                      | {kind: `Interface(_), _} => true
                      | _ => false,
                    );
                  Some(List.map(interfaces, i => AnyTyp(Abstract(i)))->List.toArray);
                | _ => None
                },
            }),
            Field({
              name: "possibleTypes",
              description: None,
              deprecated: NotDeprecated,
              typ: List(NonNull(__type)),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Abstract(a)) => Some(a.types->List.toArray)
                | _ => None
                },
            }),
            Field({
              name: "inputFields",
              description: None,
              deprecated: NotDeprecated,
              typ: List(NonNull(__input_value)),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyArgTyp(Arg.InputObject(o)) => Some(args_to_list(o.fields)->List.toArray)
                | _ => None
                },
            }),
            Field({
              name: "enumValues",
              description: None,
              deprecated: NotDeprecated,
              typ: List(NonNull(__enumValue)),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, t) =>
                switch (t) {
                | AnyTyp(Enum(e)) => Some(List.map(e.values, x => AnyEnumValue(x))->List.toArray)
                | AnyArgTyp(Arg.Enum(e)) => Some(List.map(e.values, x => AnyEnumValue(x))->List.toArray)
                | _ => None
                },
            }),
          ]),
      })
    and __field: type ctx. typ(ctx, option(anyField)) =
      Object({
        name: "__Field",
        description: None,
        abstracts: no_abstracts,
        fields:
          lazy([
            Field({
              name: "name",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(string),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, f) =>
                switch (f) {
                | AnyField(Field(f)) => f.name
                | AnyArgField(Arg.Arg(a)) => a.name
                | AnyArgField(Arg.DefaultArg(a)) => a.name
                },
            }),
            Field({
              name: "description",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, f) =>
                switch (f) {
                | AnyField(Field(f)) => f.description
                | AnyArgField(Arg.Arg(a)) => a.description
                | AnyArgField(Arg.DefaultArg(a)) => a.description
                },
            }),
            Field({
              name: "args",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(List(NonNull(__input_value))),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, f) =>
                switch (f) {
                | AnyField(Field(f)) => args_to_list(f.args)->List.toArray
                | AnyArgField(_) => [||]
                },
            }),
            Field({
              name: "type",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(__type),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, f) =>
                switch (f) {
                | AnyField(Field(f)) => AnyTyp(f.typ)
                | AnyArgField(Arg.Arg(a)) => AnyArgTyp(a.typ)
                | AnyArgField(Arg.DefaultArg(a)) => AnyArgTyp(a.typ)
                },
            }),
            Field({
              name: "isDeprecated",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(boolean),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, f) =>
                switch (f) {
                | AnyField(Field({deprecated: Deprecated(_), _})) => true
                | _ => false
                },
            }),
            Field({
              name: "deprecationReason",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, f) =>
                switch (f) {
                | AnyField(Field({deprecated: Deprecated(reason), _})) => reason
                | _ => None
                },
            }),
          ]),
      });

    let __directiveLocation =
      Enum({
        name: "__DirectiveLocation",
        description: None,
        values: [
          {name: "QUERY", description: None, deprecated: NotDeprecated, value: `Query},
          {name: "MUTATION", description: None, deprecated: NotDeprecated, value: `Mutation},
          {
            name: "SUBSCRIPTION",
            description: None,
            deprecated: NotDeprecated,
            value: `Subscription,
          },
          {name: "FIELD", description: None, deprecated: NotDeprecated, value: `Field},
          {
            name: "FragmentDefinition",
            description: None,
            deprecated: NotDeprecated,
            value: `FragmentDefinition,
          },
          {
            name: "FragmentSpread",
            description: None,
            deprecated: NotDeprecated,
            value: `FragmentSpread,
          },
          {
            name: "InlineFragment",
            description: None,
            deprecated: NotDeprecated,
            value: `InlineFragment,
          },
          {
            name: "VariableDefinition",
            description: None,
            deprecated: NotDeprecated,
            value: `VariableDefinition,
          },
        ],
      });

    let __directive =
      Object({
        name: "__Directive",
        description: None,
        abstracts: no_abstracts,
        fields:
          lazy([
            Field({
              name: "name",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(string),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, Directive(d)) => d.name,
            }),
            Field({
              name: "description",
              description: None,
              deprecated: NotDeprecated,
              typ: string,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, Directive(d)) => d.description,
            }),
            Field({
              name: "locations",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(List(NonNull(__directiveLocation))),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, Directive(d)) => d.locations->List.toArray,
            }),
            Field({
              name: "args",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(List(NonNull(__input_value))),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, Directive(d)) => args_to_list(d.args)->List.toArray,
            }),
          ]),
      });

    let __schema: 'ctx. typ('ctx, option(schema('ctx))) =
      Object({
        name: "__Schema",
        description: None,
        abstracts: no_abstracts,
        fields:
          lazy([
            Field({
              name: "types",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(List(NonNull(__type))),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, s) => {
                let (types, _) =
                  List.reduceReverse(
                    [
                      Some(s.query),
                      s.mutation,
                      // Option.map(s.subscription, ~f=obj_of_subscription_obj),
                    ],
                    ([], StringSet.empty),
                    (memo, op) =>
                    switch (op) {
                    | None => memo
                    | Some(op) => types(~memo, Object(op))
                    }
                  );
                types->List.toArray;
              },
            }),
            Field({
              name: "queryType",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(__type),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, s) => AnyTyp(Object(s.query)),
            }),
            Field({
              name: "mutationType",
              description: None,
              deprecated: NotDeprecated,
              typ: __type,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, s) => Option.map(s.mutation, mut => AnyTyp(Object(mut))),
            }),
            Field({
              name: "subscriptionType",
              description: None,
              deprecated: NotDeprecated,
              typ: __type,
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, _) => None,
            }),
            Field({
              name: "directives",
              description: None,
              deprecated: NotDeprecated,
              typ: NonNull(List(NonNull(__directive))),
              args: Arg.[],
              lift: Io.ok,
              resolve: (_, _) => [||],
            }),
          ]),
      });

    let addSchemaField = schema => {
      {
        ...schema,
        query: {
          ...schema.query,
          fields:
            lazy([
              Field({
                name: "__schema",
                typ: NonNull(__schema),
                args: [],
                description: None,
                deprecated: NotDeprecated,
                lift: Io.ok,
                resolve: (_, _) => schema,
              }),
              ...Lazy.force(schema.query.fields),
            ]),
        },
      };
    };
  };

  // Execution

  type executionContext('ctx) = {
    schema: schema('ctx),
    operation: Ast.operationDefinition,
    fragmentMap,
    variableMap,
    ctx: 'ctx,
  };

  type path = list(string);
  type error = (string, path);

  type resolveError = [
    | `ResolveError(error)
    | `ArgumentError(string)
    | `ValidationError(string)
  ];

  type executeError = [
    resolveError
    | `MutationsNotConfigured
    | `SubscriptionsNotConfigured
    | `NoOperationFound
    | `OperationNameRequired
    | `OperationNotFound
  ];

  type executionResult = {data: Ast.constValue};

  module ArgEval = {
    open Arg;

    let rec valueToConstValue: (variableMap, Ast.value) => Ast.constValue =
      variableMap =>
        fun
        | `Null => `Null
        | `Int(_) as i => i
        | `Float(_) as f => f
        | `String(_) as s => s
        | `Boolean(_) as b => b
        | `Enum(_) as e => e
        | `Variable(v) =>
          switch (StringMap.get(variableMap, v)) {
          | Some(value) => value
          | None => `Null
          }
        | `List(xs) => `List(List.map(xs, valueToConstValue(variableMap)))
        | `Object(props) => {
            let props' =
              List.map(props, ((name, value)) =>
                (name, valueToConstValue(variableMap, value))
              );
            `Object(props');
          };

    let rec stringOfConstValue: Ast.constValue => string = (
      fun
      | `Null => "null"
      | `Int(i) => string_of_int(i)
      | `Float(f) => Js.Float.toString(f)
      | `String(s) => Printf.sprintf("\"%s\"", s)
      | `Boolean(b) => string_of_bool(b)
      | `Enum(e) => e
      | `List(l) => {
          let values = List.map(l, i => stringOfConstValue(i));
          Printf.sprintf("[%s]", String.concat(", ", values));
        }
      | `Object(a) => {
          let values =
            List.map(a, ((k, v)) => Printf.sprintf("%s: %s", k, stringOfConstValue(v)));

          Printf.sprintf("{%s}", String.concat(", ", values));
        }:
        Ast.constValue => string
    );

    let rec stringOfArgType: type a. argTyp(a) => string =
      fun
      | Scalar(a) => Printf.sprintf("%s", a.name)
      | InputObject(a) => Printf.sprintf("%s", a.name)
      | Enum(a) => Printf.sprintf("%s", a.name)
      | List(a) => Printf.sprintf("[%s]", stringOfArgType(a))
      | NonNull(a) => Printf.sprintf("%s!", stringOfArgType(a));

    let evalArgError = (~fieldType="field", ~fieldName, ~argName, argTyp, value) => {
      let foundStr =
        switch (value) {
        | Some(v) => Printf.sprintf("found %s", stringOfConstValue(v))
        | None => "but not provided"
        };

      Printf.sprintf(
        "Argument `%s` of type `%s` expected on %s `%s`, %s.",
        argName,
        stringOfArgType(argTyp),
        fieldType,
        fieldName,
        foundStr,
      );
    };


    let rec evalArgList:
      type a b.
        (
          variableMap,
          ~fieldType: string=?,
          ~fieldName: string,
          arglist(a, b),
          list((string, Ast.value)),
          b
        ) =>
        Result.t(a, string) =
      (variableMap, ~fieldType=?, ~fieldName, arglist, key_values, f) =>
        switch (arglist) {
        | [] => Ok(f)
        | [DefaultArg(arg), ...arglist'] =>
          let arglist'' = [
            Arg({name: arg.name, description: arg.description, typ: arg.typ}),
            ...arglist',
          ];

          evalArgList(
            variableMap,
            ~fieldType?,
            ~fieldName,
            arglist'',
            key_values,
            fun
            | None => f(arg.default)
            | Some(v) => f(v),
          );
        | [Arg(arg), ...arglist'] =>
          let value = List.getAssoc(key_values, arg.name, (==));
          let constValue = Option.map(value, valueToConstValue(variableMap));
          evalArg(variableMap, ~fieldType?, ~fieldName, ~argName=arg.name, arg.typ, constValue)
          ->Result.flatMap(coerced =>
              evalArgList(variableMap, ~fieldType?, ~fieldName, arglist', key_values, f(coerced))
            );
        }

    and evalArg:
      type a.
        (
          variableMap,
          ~fieldType: string=?,
          ~fieldName: string,
          ~argName: string,
          argTyp(a),
          option(Ast.constValue)
        ) =>
        Result.t(a, string) =
      (variableMap, ~fieldType=?, ~fieldName, ~argName, typ, value) =>
        switch (typ, value) {
        | (NonNull(_), None) =>
          Error(evalArgError(~fieldType?, ~fieldName, ~argName, typ, value))
        | (NonNull(_), Some(`Null)) =>
          Error(evalArgError(~fieldType?, ~fieldName, ~argName, typ, value))
        | (Scalar(_), None) => Ok(None)
        | (Scalar(_), Some(`Null)) => Ok(None)
        | (InputObject(_), None) => Ok(None)
        | (InputObject(_), Some(`Null)) => Ok(None)
        | (List(_), None) => Ok(None)
        | (List(_), Some(`Null)) => Ok(None)
        | (Enum(_), None) => Ok(None)
        | (Enum(_), Some(`Null)) => Ok(None)
        | (NonNull(typ), Some(value)) =>
          evalArg(variableMap, ~fieldType?, ~fieldName, ~argName, typ, Some(value))
          ->Result.flatMap(
              fun
              | Some(value) => Ok(value)
              | None => Error(evalArgError(~fieldType?, ~fieldName, ~argName, typ, None)),
            )
        | (Scalar(s), Some(value)) =>
          switch (s.parse(value)) {
          | Ok(coerced) => Ok(Some(coerced))
          | Error(_) => Error(evalArgError(~fieldType?, ~fieldName, ~argName, typ, Some(value)))
          }
        | (InputObject(o), Some(value)) =>
          switch (value) {
          | `Object((props: list((string, Ast.constValue)))) =>
            evalArgList(
              variableMap,
              ~fieldType?,
              ~fieldName,
              o.fields,
              (props :> list((string, Ast.value))),
              o.coerce,
            )
            ->Result.map(coerced => Some(coerced))
          | _ => Error(evalArgError(~fieldType?, ~fieldName, ~argName, typ, Some(value)))
          }
        | (List(typ), Some(value)) =>
          switch (value) {
          | `List(values) =>
            let optionValues = List.map(values, x => Some(x));
            List.Result.all(
              optionValues,
              evalArg(variableMap, ~fieldType?, ~fieldName, ~argName, typ),
            )
            ->Result.map(coerced => Some(List.toArray(coerced)));
          | value =>
            evalArg(variableMap, ~fieldType?, ~fieldName, ~argName, typ, Some(value))
            ->Result.map((coerced) => (Some([| coerced |]): a))
          }
        | (Enum(enum), Some(value)) =>
          switch (value) {
          | `Enum(v)
          | `String(v) =>
            switch (Belt.List.getBy(enum.values, enumValue => enumValue.name == v)) {
            | Some(enumValue) => Ok(Some(enumValue.value))
            | None =>
              Error(
                Printf.sprintf(
                  "Invalid enum value for argument `%s` on field `%s`",
                  argName,
                  fieldName,
                ),
              )
            }
          | _ =>
            Error(
              Printf.sprintf("Expected enum for argument `%s` on field `%s`", argName, fieldName),
            )
          }
        };
  };

  let matchesTypeCondition = (typeCondition: string, obj: obj('ctx, 'src)) =>
    typeCondition == obj.name
    || Belt.List.some(obj.abstracts^, abstract => abstract.name == typeCondition);

  let rec shouldIncludeField = (ctx, directives: list(Ast.directive)) =>
    switch (directives) {
    | [] => Ok(true)
    | [{name: "skip", arguments}, ...rest] =>
      eval_directive(ctx, skipDirective, arguments, rest)
    | [{name: "include", arguments}, ...rest] =>
      eval_directive(ctx, includeDirective, arguments, rest)
    | [{name, _}, ..._] =>
      let err = Format.sprintf("Unknown directive: %s", name);
      Error(err);
    }
  and eval_directive = (ctx, Directive({name, args, resolve, _}), arguments, rest) =>
    ArgEval.evalArgList(
      ctx.variableMap,
      ~fieldType="directive",
      ~fieldName=name,
      args,
      arguments,
      resolve,
    )
    ->Result.flatMap(
        fun
        | `Skip => Ok(false)
        | `Include => shouldIncludeField(ctx, rest),
      );

  let rec collectFields:
    (executionContext('ctx), obj('ctx, 'src), list(Ast.selection)) =>
    Result.t(list(Ast.field), string) =
    (ctx, obj, selectionSet) =>
      selectionSet
      ->List.map(
          fun
          | Ast.Field(field) =>
            shouldIncludeField(ctx, field.directives)
            ->Result.map(shouldInclude => shouldInclude ? [field] : [])
          | Ast.FragmentSpread(fragmentSpread) =>
            switch (StringMap.get(ctx.fragmentMap, fragmentSpread.name)) {
            | Some({typeCondition, selectionSet, directives})
                when matchesTypeCondition(typeCondition, obj) =>
              shouldIncludeField(ctx, directives)
              ->Result.flatMap(shouldInclude =>
                  shouldInclude ? collectFields(ctx, obj, selectionSet) : Ok([])
                )
            | _ => Ok([])
            }
          | Ast.InlineFragment({typeCondition: Some(condition), directives} as inlineFragment)
              when matchesTypeCondition(condition, obj) => {
              shouldIncludeField(ctx, directives)
              ->Result.flatMap(shouldInclude =>
                  shouldInclude ? collectFields(ctx, obj, inlineFragment.selectionSet) : Ok([])
                );
            }
          | Ast.InlineFragment({typeCondition: _}) => Ok([]),
        )
      ->List.Result.join
      ->List.Result.map(Belt.List.flatten);

  let fieldName: Ast.field => string =
    fun
    | {alias: Some(alias)} => alias
    | field => field.name;

  let getObjField = (fieldName: string, obj: obj('ctx, 'src)): option(field('ctx, 'src)) =>
    obj.fields |> Lazy.force |> Belt.List.getBy(_, (Field(field)) => field.name == fieldName);

  let coerceOrNull = (src, f) =>
    switch (src) {
    | Some(src') => f(src')
    | None => Io.ok(`Null)
    };


  let rec resolveValue:
    type ctx src.
      (executionContext(ctx), src, Ast.field, typ(ctx, src)) =>
      Io.t(Result.t(Ast.constValue, [> resolveError])) =
    (executionContext, src, field, typ) =>
      switch (typ) {
      | NonNull(typ') => resolveValue(executionContext, Some(src), field, typ')
      | Scalar(scalar) => coerceOrNull(src, src' => Io.ok(scalar.serialize(src')))
      | Enum(enum) =>
        coerceOrNull(src, src' =>
          switch (Belt.List.getBy(enum.values, enumValue => enumValue.value == src')) {
          | Some(enumValue) => Io.ok(`String(enumValue.name))
          | None => Io.ok(`Null)
          }
        )
      | Object(obj) =>
        coerceOrNull(src, src' =>
          switch (collectFields(executionContext, obj, field.selectionSet)) {
          | Ok(fields) => resolveFields(executionContext, src', obj, fields)
          | Error(e) => Io.error(`ArgumentError(e))
          }
        )
      | List(typ') =>
        coerceOrNull(src, src' =>
          Belt.Array.map(src', srcItem => resolveValue(executionContext, srcItem, field, typ'))
          ->List.fromArray
          ->Io.all
          ->Io.map(List.Result.join)
          ->Io.Result.map(list => `List(list))
        )
      | Abstract(_) =>
        coerceOrNull(
          src,
          src' => {
            let AbstractValue((typ', src')) = src';
            resolveValue(executionContext, Some(src'), field, typ');
          },
        )
      }

  and resolveField:
    type ctx src.
      (executionContext(ctx), src, Ast.field, field(ctx, src)) =>
      Io.t(Result.t((string, Ast.constValue), [> resolveError])) =
    (executionContext, src, field, Field(fieldDef)) => {
      let name = fieldName(field);
      let resolver = fieldDef.resolve(executionContext.ctx, src);

      switch (
        ArgEval.evalArgList(
          executionContext.variableMap,
          ~fieldName=fieldDef.name,
          fieldDef.args,
          field.arguments,
          resolver,
        )
      ) {
      | Ok(unlifted) =>
        let%Io.Result resolved =
          fieldDef.lift(unlifted)->Io.Result.mapError(err => `ResolveError((err, [])));

        let%Io resolvedValue = resolveValue(executionContext, resolved, field, fieldDef.typ);

        Io.return(
          switch (resolvedValue) {
          | Ok(value) => Ok((name, value))
          | Error(`ArgumentError(_) | `ValidationError(_)) as error => error
          | Error(`ResolveError(_)) as error =>
            switch (fieldDef.typ) {
            | NonNull(_) => error
            | _ => Ok((name, `Null))
            }
          },
        );

      | Error(err) => Io.error(`ArgumentError(err))
      };
    }

  and resolveFields:
    type ctx src.
      (executionContext(ctx), src, obj(ctx, src), list(Ast.field)) =>
      Io.t(Result.t(Ast.constValue, [> resolveError])) =
    (executionContext, src, obj, fields) => {
      let mapFields =
        switch (executionContext.operation.operationType) {
        | Query
        | Subscription => Io.mapParalell
        | Mutation => Io.mapSerial(~memo=[])
        };

      mapFields(fields, field =>
        if (field.name == "__typename") {
          Io.ok((fieldName(field), `String(obj.name)));
        } else {
          switch (getObjField(field.name, obj)) {
          | Some(objField) => resolveField(executionContext, src, field, objField)
          | None =>
            let err =
              Printf.sprintf("Field '%s' is not defined on type '%s'", field.name, obj.name);
            Io.error(`ValidationError(err));
          };
        }
      )
      ->Io.map(List.Result.join)
      ->Io.Result.map(assocList => `Object(assocList));
    };

  let executeOperation =
      (executionContext: executionContext('ctx), operation: Ast.operationDefinition)
      : Io.t(Result.t(Ast.constValue, [> executeError])) =>
    switch (operation.operationType) {
    | Query =>
      let%Io.Result fields =
        Io.return(
          collectFields(executionContext, executionContext.schema.query, operation.selectionSet),
        )
        ->Io.Result.mapError(e => `ArgumentError(e));

      (
        resolveFields(executionContext, (), executionContext.schema.query, fields):
          Io.t(Result.t(Ast.constValue, resolveError)) :>
          Io.t(Result.t(Ast.constValue, [> executeError]))
      );
    | Mutation =>
      switch (executionContext.schema.mutation) {
      | Some(mutation) =>
        let%Io.Result fields =
          Io.return(collectFields(executionContext, mutation, operation.selectionSet))
          ->Io.Result.mapError(e => `ArgumentError(e));

        (
          resolveFields(executionContext, (), mutation, fields):
            Io.t(Result.t(Ast.constValue, resolveError)) :>
            Io.t(Result.t(Ast.constValue, [> executeError]))
        );
      | None => Io.error(`MutationsNotConfigured)
      }
    | _ => failwith("Subscription Not implemented")
    };

  let collectOperations = (document: Ast.document) =>
    Belt.List.reduceReverse(document.definitions, [], (list, x) =>
      switch (x) {
      | Ast.Operation(operation) => [operation, ...list]
      | _ => list
      }
    );

  let collectFragments = (document: Ast.document) => {
    Belt.List.reduceReverse(document.definitions, StringMap.empty, fragmentMap =>
      fun
      | Ast.Fragment(fragment) => StringMap.set(fragmentMap, fragment.name, fragment)
      | _ => fragmentMap
    );
  };

  exception FragmentCycle(list(string));

  let rec validateFragments = fragmentMap =>
    try(
      {
        StringMap.forEach(fragmentMap, (name, _) =>
          validateFragment(fragmentMap, StringSet.empty, name)
        );
        Ok(fragmentMap);
      }
    ) {
    | FragmentCycle(fragmentNames) =>
      let cycle = String.concat(", ", fragmentNames);
      let err = Format.sprintf("Fragment cycle detected: %s", cycle);
      Error(`ValidationError(err));
    }

  and validateFragment = (fragmentMap: fragmentMap, visited, name) => {
    switch (StringMap.get(fragmentMap, name)) {
    | None => ()
    | Some(fragment) when StringSet.mem(fragment.name, visited) =>
      raise(FragmentCycle(StringSet.elements(visited)))
    | Some(fragment) =>
      let visited' = StringSet.add(fragment.name, visited);
      Belt.List.forEach(fragment.selectionSet, validateFragmentSelection(fragmentMap, visited'));
    };
  }

  and validateFragmentSelection = (fragmentMap, visited, selection) =>
    switch (selection) {
    | Field(field) =>
      Belt.List.forEach(field.selectionSet, validateFragmentSelection(fragmentMap, visited))
    | InlineFragment(inlineFragment) =>
      Belt.List.forEach(
        inlineFragment.selectionSet,
        validateFragmentSelection(fragmentMap, visited),
      )
    | FragmentSpread(fragmentSpread) =>
      validateFragment(fragmentMap, visited, fragmentSpread.name)
    };

  let collectAndValidateFragments = doc => {
    let fragments = collectFragments(doc);
    validateFragments(fragments);
  };

  let okResponse = data => {
    `Object([("data", data)]);
  };

  let errorResponse = (~path=?, msg): Ast.constValue => {
    let path' =
      switch (path) {
      | Some(path) => path
      | None => []
      };
    `Object([
      ("data", `Null),
      (
        "errors",
        `List([
          `Object([
            ("message", `String(msg)),
            ("path", `List(List.map(path', s => `String(s)))),
          ]),
        ]),
      ),
    ]);
  };

  let execute =
      (~variables: variableList=[], ~document: Ast.document, schema: schema('ctx), ~ctx: 'ctx) => {
    let execute' = (schema, ctx, document) => {
      let operations = collectOperations(document);
      let%Io.Result fragmentMap = Io.return(collectAndValidateFragments(document));

      let variableMap =
        Belt.List.reduce(variables, StringMap.empty, (map, (name, value)) =>
          StringMap.set(map, name, value)
        );

      let schema' = Introspection.addSchemaField(schema);

      List.map(
        operations,
        operation => {
          let executionContext = {schema: schema', fragmentMap, operation, variableMap, ctx};
          executeOperation(executionContext, operation);
        },
      )
      ->Belt.List.headExn;
    };

    execute'(schema, ctx, document)
    ->Io.map(
        fun
        | Ok(res) => okResponse(res)
        | Error(`NoOperationFound) => errorResponse("No operation found")
        | Error(`OperationNotFound) => errorResponse("Operation not found")
        | Error(`OperationNameRequired) => errorResponse("Operation name required")
        | Error(`SubscriptionsNotConfigured) => errorResponse("Subscriptions not configured")
        | Error(`MutationsNotConfigured) => errorResponse("Mutations not configured")
        | Error(`ValidationError(msg)) => errorResponse(msg)
        | Error(`ArgumentError(msg)) => errorResponse(msg)
        | Error(`ResolveError(msg, path)) => errorResponse(msg, ~path),
      );
  };

  let resultToJson: Io.t(Ast.constValue) => Io.t(Js.Json.t) =
    result => Io.map(result, Graphql_Json.fromConstValue);
};


================================================
FILE: reason-graphql/src/Graphql_Schema.rei
================================================
module Make: (Io: Graphql_Interface.IO) => Graphql_Interface.Schema with module Io = Io;


================================================
FILE: reason-graphql/src/language/Graphql_Language.re
================================================
module Ast = Graphql_Language_Ast;
module Lexer = Graphql_Language_Lexer;
module Parser = Graphql_Language_Parser;
module Printer = Graphql_Language_Printer;


================================================
FILE: reason-graphql/src/language/Graphql_Language_Ast.re
================================================
type primitiveValue = [
  | `Int(int)
  | `Float(float)
  | `Boolean(bool)
  | `String(string)
  | `Enum(string)
  | `Null
];

type constValue = [
  primitiveValue
  | `List(list(constValue))
  | `Object(list((string, constValue)))
];

type value = [
  primitiveValue
  | `List(list(value))
  | `Object(list((string, value)))
  | `Variable(string)
];

type argument = (string, value);

type document = {definitions: list(definition)}

and definition =
  | Operation(operationDefinition)
  | Fragment(fragmentDefinition)

and operationDefinition = {
  operationType,
  name: option(string),
  variableDefinition: list(variableDefinition),
  directives: list(directive),
  selectionSet: list(selection),
}

and operationType =
  | Query
  | Mutation
  | Subscription

and variableDefinition = {
  variable: [ | `Variable(string)],
  typ: typeReference,
  defaultValue: option(constValue),
  directives: list(directive),
}

and selection =
  | Field(field)
  | FragmentSpread(fragmentSpread)
  | InlineFragment(inlineFragmentDefinition)

and field = {
  alias: option(string),
  name: string,
  arguments: list(argument),
  selectionSet: list(selection),
  directives: list(directive),
}

and fragmentDefinition = {
  name: string,
  typeCondition: string,
  selectionSet: list(selection),
  directives: list(directive),
}

and inlineFragmentDefinition = {
  typeCondition: option(string),
  selectionSet: list(selection),
  directives: list(directive),
}

and fragmentSpread = {
  name: string,
  directives: list(directive),
}

/* Directives */
and directive = {
  name: string,
  arguments: list(argument),
}

and typeReference =
  | NamedType(string)
  | ListType(typeReference)
  | NonNullType(typeReference)

and typeSystemDefinition =
  | SchemaDefinition(schemaDefinition)
  | TypeDefinition(typeDefinition)
  | TypeExtension(typeExtensionDefinition)
  | DirectiveDefinitionNode(directiveDefinition)

and schemaDefinition = {operationTypes: operationTypeDefinition}

and operationTypeDefinition = {
  typ: string,
  operation: operationType,
}

and typeDefinition =
  | ScalarTypeDefinition(string)
  | ObjectTypeDefinition(objectTypeDefinition)
  | InterfaceTypeDefinition(interfaceTypeDefinition)
  | UnionTypeDefinition(unionTypeDefinition)
  | EnumTypeDefinition(enumTypeDefintion)
  | InputObjectTypeDefinition(inputObjectTypeDefinition)

and objectTypeDefinition = {
  name: string,
  interfaces: list(string),
  fields: list(fieldDefinition),
}

and fieldDefinition = {
  name: string,
  arguments: list(inputValueDefinition),
  typ: typeReference,
}

and inputValueDefinition = {
  name: string,
  typ: typeReference,
  defaultValue: option(constValue),
}

and interfaceTypeDefinition = {
  name: string,
  fields: list(fieldDefinition),
}

and unionTypeDefinition = {
  name: string,
  types: list(string),
}

and enumTypeDefintion = {
  name: string,
  values: list(string),
}

and inputObjectTypeDefinition = {
  name: string,
  fields: list(inputValueDefinition),
}

and typeExtensionDefinition = {definition: objectTypeDefinition}

and directiveDefinition = {
  name: string,
  arguments: list(inputValueDefinition),
};

type astMapping = {
  name: string => string,
  document: document => document,
  operationDefinition: operationDefinition => operationDefinition,
  fragmentDefinition: fragmentDefinition => fragmentDefinition,
  variableDefinition: variableDefinition => variableDefinition,
  directives: list(directive) => list(directive),
  directive: directive => directive,
  selection: selection => selection,
  selectionSet: list(selection) => list(selection),
  field: field => field,
  arguments: list(argument) => list(argument),
  argument: argument => argument,
  inlineFragmentDefinition:
    inlineFragmentDefinition => inlineFragmentDefinition,
  fragmentSpread: fragmentSpread => fragmentSpread,
  typeReference: typeReference => typeReference,
  namedType: string => typeReference,
  listType: typeReference => typeReference,
  nonNullType: typeReference => typeReference,
  constValue: constValue => constValue,
  value: value => value,
  variable: string => string,
};

let id = a => a;

let defaultMapper = {
  name: id,
  document: id,
  operationDefinition: id,
  fragmentDefinition: id,
  variableDefinition: id,
  directives: id,
  directive: id,
  selection: id,
  selectionSet: id,
  field: id,
  arguments: id,
  argument: id,
  inlineFragmentDefinition: id,
  fragmentSpread: id,
  typeReference: a => a,
  namedType: a => NamedType(a),
  constValue: id,
  value: id,
  listType: a => ListType(a),
  nonNullType: a => NonNullType(a),
  variable: id,
};

let rec visit = (~enter=defaultMapper, ~leave=defaultMapper, ast: document) => {
  let document = enter.document(ast);
  {
    definitions:
      document.definitions |> List.map(visitDefinition(~enter, ~leave)),
  }
  |> leave.document;
}
and visitDefinition = (~enter, ~leave) =>
  fun
  | Operation(operationDefinition) =>
    Operation(visitOperationDefinition(~enter, ~leave, operationDefinition))
  | Fragment(fragmentDefinition) =>
    Fragment(visitFragmentDefinition(~enter, ~leave, fragmentDefinition))
and visitOperationDefinition = (~enter, ~leave, operationDefinition) => {
  let operationDefinition = enter.operationDefinition(operationDefinition);
  {
    ...operationDefinition,
    name:
      switch (operationDefinition.name) {
      | Some(name) => Some(visitName(~enter, ~leave, name))
      | None => None
      },
    variableDefinition:
      List.map(
        visitVariableDefinition(~enter, ~leave),
        operationDefinition.variableDefinition,
      ),
    directives:
      visitDirectives(~enter, ~leave, operationDefinition.directives),
    selectionSet:
      visitSelectionSet(~enter, ~leave, operationDefinition.selectionSet),
  };
}
and visitFragmentDefinition = (~enter, ~leave, fragmentDefinition) => {
  let fragmentDefinition = enter.fragmentDefinition(fragmentDefinition);

  {
    ...fragmentDefinition,
    name: visitName(~enter, ~leave, fragmentDefinition.name),
    selectionSet:
      visitSelectionSet(~enter, ~leave, fragmentDefinition.selectionSet),
    directives:
      visitDirectives(~enter, ~leave, fragmentDefinition.directives),
  }
  |> leave.fragmentDefinition;
}
and visitVariableDefinition = (~enter, ~leave, variableDefinition) => {
  let variableDefinition = enter.variableDefinition(variableDefinition);
  let `Variable(variable) = variableDefinition.variable;

  {
    variable: `Variable(enter.variable(variable)),
    typ: visitTypeReference(~enter, ~leave, variableDefinition.typ),
    directives:
      visitDirectives(~enter, ~leave, variableDefinition.directives),
    defaultValue:
      switch (variableDefinition.defaultValue) {
      | Some(constValue) =>
        Some(visitConstValue(~enter, ~leave, constValue))
      | None => None
      },
  }
  |> leave.variableDefinition;
}
and visitDirectives = (~enter, ~leave, directives) => {
  directives
  |> enter.directives
  |> List.map(visitDirective(~enter, ~leave))
  |> leave.directives;
}
and visitDirective = (~enter, ~leave, directive) => {
  let directive = enter.directive(directive);
  {
    name: visitName(~enter, ~leave, directive.name),
    arguments: visitArguments(~enter, ~leave, directive.arguments),
  }
  |> leave.directive;
}
and visitArguments = (~enter, ~leave, arguments) => {
  arguments
  |> enter.arguments
  |> List.map(visitArgument(~enter, ~leave))
  |> leave.arguments;
}
and visitArgument = (~enter, ~leave, argument) => {
  let (name, value) = enter.argument(argument);
  (name, visitValue(~enter, ~leave, value)) |> leave.argument;
}
and visitSelectionSet = (~enter, ~leave, selectionSet) => {
  selectionSet
  |> enter.selectionSet
  |> List.map(visitSelection(~enter, ~leave))
  |> leave.selectionSet;
}
and visitSelection = (~enter, ~leave, selection) => {
  (
    switch (enter.selection(selection)) {
    | Field(field) => Field(visitField(~enter, ~leave, field))
    | FragmentSpread(fragmentSpread) =>
      FragmentSpread(visitFragmentSpread(~enter, ~leave, fragmentSpread))
    | InlineFragment(inlineFragment) =>
      InlineFragment(visitInlineFragment(~enter, ~leave, inlineFragment))
    }
  )
  |> leave.selection;
}
and visitField = (~enter, ~leave, field) => {
  let field = enter.field(field);
  {
    ...field,
    name: visitName(~enter, ~leave, field.name),
    arguments: visitArguments(~enter, ~leave, field.arguments),
    selectionSet: visitSelectionSet(~enter, ~leave, field.selectionSet),
    directives: visitDirectives(~enter, ~leave, field.directives),
  }
  |> leave.field;
}
and visitFragmentSpread = (~enter, ~leave, fragmentSpread) => {
  let fragmentSpread = enter.fragmentSpread(fragmentSpread);
  {
    name: visitName(~enter, ~leave, fragmentSpread.name),
    directives: visitDirectives(~enter, ~leave, fragmentSpread.directives),
  }
  |> leave.fragmentSpread;
}
and visitInlineFragment = (~enter, ~leave, inlineFragment) => {
  let inlineFragment = enter.inlineFragmentDefinition(inlineFragment);
  {
    ...inlineFragment,
    selectionSet:
      visitSelectionSet(~enter, ~leave, inlineFragment.selectionSet),
    directives: visitDirectives(~enter, ~leave, inlineFragment.directives),
  }
  |> leave.inlineFragmentDefinition;
}
and visitTypeReference = (~enter, ~leave, typeReference) => {
  (
    switch (enter.typeReference(typeReference)) {
    | NamedType(string) => enter.namedType(string)
    | ListType(listType) => enter.listType(listType)
    | NonNullType(nonNullType) => enter.nonNullType(nonNullType)
    }
  )
  |> leave.typeReference;
}
and visitName = (~enter, ~leave, name) => {
  enter.name(name) |> leave.name;
}
and visitValue = (~enter, ~leave, value) => {
  enter.value(value) |> leave.value;
}
and visitConstValue = (~enter, ~leave, constValue) => {
  enter.constValue(constValue) |> leave.constValue;
};

================================================
FILE: reason-graphql/src/language/Graphql_Language_Error.re
================================================
type t =
  | SyntaxError(string);


================================================
FILE: reason-graphql/src/language/Graphql_Language_Lexer.re
================================================
module Result = {
  include Belt.Result;
  let let_ = flatMap;
};

type result('a) = Result.t('a, Graphql_Language_Error.t);
let syntaxError = a => Result.Error(Graphql_Language_Error.SyntaxError(a));

type location = {
  start: int,
  end_: int,
  line: int,
  column: int,
};

type token =
  | StartOfFile
  | EndOfFile
  | Bang
  | Dollar
  | Amp
  | ParenOpen
  | ParenClose
  | Spread
  | Colon
  | Equals
  | At
  | BracketOpen
  | BracketClose
  | BraceOpen
  | BraceClose
  | Pipe
  | Name(string)
  | Int(string)
  | Float(string)
  | String(string)
  | Comment(string);

let tokenKind =
  fun
  | StartOfFile => "<SOF>"
  | EndOfFile => "<EOF>"
  | Bang => "!"
  | Dollar => "$"
  | Amp => "$"
  | ParenOpen => "("
  | ParenClose => ")"
  | Spread => "..."
  | Colon => ":"
  | Equals => "="
  | At => "@"
  | BracketOpen => "["
  | BracketClose => "]"
  | BraceOpen => "{"
  | BraceClose => "}"
  | Pipe => "|"
  | Name(_) => "Name"
  | Int(_) => "Int"
  | Float(_) => "Float"
  | String(_) => "String"
  | Comment(_) => "Comment";

type tokenResult = {
  token,
  location,
};

type t = {
  source: string,
  mutable curr: tokenResult,
  mutable line: int,
  mutable lineStart: int,
};

/* (line:col) kind: <kind>, value: <value> */
let tokenDesc = ({token, location}) => {
  "("
  ++ string_of_int(location.line)
  ++ ":"
  ++ string_of_int(location.column)
  ++ ")"
  ++ (
    switch (token) {
    | Name(v) => " kind: '" ++ tokenKind(token) ++ "', value: " ++ v
    | Int(v) => " kind: '" ++ tokenKind(token) ++ "', value: " ++ v
    | Float(v) => " kind: '" ++ tokenKind(token) ++ "', value: " ++ v
    | String(v) => " kind: '" ++ tokenKind(token) ++ "', value: " ++ v
    | Comment(v) => " kind: '" ++ tokenKind(token) ++ "', value: " ++ v
    | token => " kind: '" ++ tokenKind(token) ++ "'"
    }
  );
};

let isChar = (source, position, char) =>
  position < String.length(source) && source.[position] == char;

/**
 * Reads from source starting at startPosition until it finds a non-whitespace
 * character, then returns the position of that character for lexing.
 */
let positionAfterWhitespace = (lexer, startPosition) => {
  let {source} = lexer;

  let rec aux = position =>
    if (position >= String.length(source)) {
      position;
    } else {
      switch (source.[position]) {
      | bom when int_of_char(bom) == 0xFEFF => position + 1
      | ' '
      | ','
      | '\t' => aux(position + 1)
      | '\n' =>
        let newPosition = position + 1;
        lexer.line = lexer.line + 1;
        lexer.lineStart = newPosition;
        aux(newPosition);
      | '\r' =>
        let newPosition =
          isChar(source, position + 1, '\n') ? position + 2 : position + 1;

        lexer.line = lexer.line + 1;
        lexer.lineStart = newPosition;
        aux(newPosition);
      | _ => position
      };
    };

  aux(startPosition);
};

let isNameChar =
  fun
  | 'A'..'Z'
  | 'a'..'z'
  | '0'..'9'
  | '_' => true
  | _ => false;

/**
 * Reads an alphanumeric + underscore name from the source.
 *
 * [_A-Za-z][_0-9A-Za-z]*
 */
let readName = (source, ~start, ~line, ~column): tokenResult => {
  let rec aux = position =>
    switch (source.[position]) {
    | 'A'..'Z'
    | 'a'..'z'
    | '0'..'9'
    | '_' => aux(position + 1)
    | _ => {
        token: Name(String.sub(source, start, position - start)),
        location: {
          start,
          line,
          end_: position,
          column,
        },
      }
    };

  aux(start);
};

/**
 * #[\u0009\u0020-\uFFFF]*
 */
let readComment = (source, ~start, ~line, ~column): tokenResult => {
  let rec aux = position =>
    if (position > String.length(source)) {
      position;
    } else {
      switch (source.[position]) {
      | '\r'
      | '\n' => position
      | _ => aux(position + 1)
      };
    };

  let position = aux(start);

  {
    token: Comment(String.sub(source, start, position - start)),
    location: {
      start,
      line,
      end_: position,
      column,
    },
  };
};

let readDigits = (source, startingPosition): result(int) => {
  let rec aux = (source, pos) =>
    if (pos >= String.length(source)) {
      Result.Ok(pos);
    } else {
      switch (source.[pos]) {
      | '0'..'9' => aux(source, pos + 1)
      | c when pos === startingPosition =>
        syntaxError(
          "Invalid number, expected digit but got: " ++ String.make(1, c),
        )
      | _ => Ok(pos)
      };
    };

  aux(source, startingPosition);
};

/**
 * Reads a number token from the source file, either a float
 * or an int depending on whether a decimal point appears.
 *
 * Int:   -?(0|[1-9][0-9]*)
 * Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
 */
let readNumber = (source, ~start, ~line, ~column): result(tokenResult) => {
  let isFloat = ref(false);
  let position = ref(start);

  if (source.[start] == '-') {
    position := position^ + 1;
  };

  let%Result () =
    if (source.[position^] == '0') {
      position := position^ + 1;
      switch (source.[position^]) {
      | '0'..'9' as char =>
        syntaxError(
          "Invalid number, unexpected digit after 0: " ++ String.make(1, char),
        )
      | _ => Ok()
      };
    } else {
      let%Result pos = readDigits(source, position^);
      Ok(position := pos);
    };

  let%Result () =
    if (isChar(source, position^, '.')) {
      isFloat := true;
      position := position^ + 1;
      let%Result pos = readDigits(source, position^);
      Ok(position := pos);
    } else {
      Ok();
    };

  let%Result () =
    if (isChar(source, position^, 'E') || isChar(source, position^, 'e')) {
      isFloat := true;
      position := position^ + 1;

      if (isChar(source, position^, '+') || isChar(source, position^, '-')) {
        position := position^ + 1;
      };

      let%Result pos = readDigits(source, position^);
      Ok(position := pos);
    } else {
      Ok();
    };

  let loc = {start, end_: position^, line, column};

  let tok =
    isFloat^
      ? Float(String.sub(source, start, position^ - start))
      : Int(String.sub(source, start, position^ - start));

  Ok({token: tok, location: loc});
};

/**
 * Converts a hex character to its integer value.
 * '0' becomes 0, '9' becomes 9
 * 'A' becomes 10, 'F' becomes 15
 * 'a' becomes 10, 'f' becomes 15
 *
 * Returns -1 on error.
 */
let char2hex = c =>
  c >= 48 && c <= 57
    ? c - 48  // 0-9
    : c >= 65 && c <= 70
        ? c - 55  // c-F
        : c >= 97 && c <= 102
            ? c - 87  // a-f
            : (-1);

/**
 * Converts four hexadecimal chars to the integer that the
 * string represents. For example, uniCharCode('0','0','0','f')
 * will return 15, and uniCharCode('0','0','f','f') returns 255.
 *
 * Returns a negative number on error, if a char was invalid.
 *
 * This is implemented by noting that char2hex() returns -1 on error,
 * which means the result of ORing the char2hex() will also be negative.
 */
let uniCharCode = (a, b, c, d) =>
  char2hex(a)
  lsl 12
  lor char2hex(b)
  lsl 8
  lor char2hex(c)
  lsl 4
  lor char2hex(d);

/**
 * Reads a string token from the source file.
 *
 * "([^"\\\u000A\u000D]|(\\(u[0-9a-fA-F]{4}|["\\/bfnrt])))*"
 */
let readString = (source, ~start, ~line, ~column): result(tokenResult) => {
  let rec aux = (value, position, chunkStart) => {
    let%Result () =
      if (position >= String.length(source)) {
        syntaxError("Unterminated string");
      } else {
        Ok();
      };

    switch (source.[position]) {
    | '\n' => syntaxError("Unterminated string")
    | '"' =>
      Ok({
        token:
          String(
            value ++ String.sub(source, chunkStart, position - chunkStart),
          ),
        location: {
          line,
          column,
          start,
          end_: position + 1,
        },
      })
    | c when Char.code(c) == 92 =>
      let newPosition = ref(position + 1);
      let code = source.[newPosition^]->Char.code;
      let%Result rest =
        switch (code) {
        | 34 => Ok("\"")
        | 47 => Ok("/")
        | 92 => Ok("\\")
        | 98 => Ok("\b")
        | 102 => Ok("\\f")
        | 110 => Ok("\n")
        | 114 => Ok("\r")
        | 116 => Ok("\t")
        // u
        | 117 =>
          let charCode =
            uniCharCode(
              source.[position + 1]->Char.code,
              source.[position + 2]->Char.code,
              source.[position + 3]->Char.code,
              source.[position + 4]->Char.code,
            );

          if (charCode < 0) {
            syntaxError(
              "Invalid character escape sequence: "
              ++ "\\u"
              ++ String.sub(source, position + 1, position + 5)
              ++ ".",
            );
          } else {
            newPosition := newPosition^ + 4;
            Ok(Char.chr(charCode) |> String.make(1));
          };

        | _ =>
          syntaxError(
            "Invalid character escape sequence: \\"
            ++ (Char.chr(code) |> String.make(1)),
          )
        };
      let value =
        value
        ++ String.sub(source, chunkStart, newPosition^ - chunkStart - 1)
        ++ rest;

      let nextPosition = newPosition^ + 1;
      aux(value, nextPosition, nextPosition);
    | _ => aux(value, position + 1, chunkStart)
    };
  };

  aux("", start + 1, start + 1);
};

/**
 * Gets the next token from the source starting at the given position.
 *
 * This skips over whitespace until it finds the next lexable token, then lexes
 * punctuators immediately or calls the appropriate helper function for more
 * complicated tokens.
 */
let readToken = (lexer, from): result(tokenResult) => {
  let {source} = lexer;
  let position = positionAfterWhitespace(lexer, from);
  let line = lexer.line;
  let column = 1 + position - lexer.lineStart;
  let sourceLength = String.length(source);
  let singleWidth = {start: position, end_: position + 1, line, column};

  if (position >= String.length(source)) {
    Ok({
      token: EndOfFile,
      location: {
        start: sourceLength,
        end_: sourceLength,
        line,
        column,
      },
    });
  } else {
    switch (source.[position]) {
    | '!' => Ok({token: Bang, location: singleWidth})
    | '$' => Ok({token: Dollar, location: singleWidth})
    | '&' => Ok({token: Amp, location: singleWidth})
    | '(' => Ok({token: ParenOpen, location: singleWidth})
    | ')' => Ok({token: ParenClose, location: singleWidth})
    | ':' => Ok({token: Colon, location: singleWidth})
    | '=' => Ok({token: Equals, location: singleWidth})
    | '@' => Ok({token: At, location: singleWidth})
    | '[' => Ok({token: BracketOpen, location: singleWidth})
    | ']' => Ok({token: BracketClose, location: singleWidth})
    | '{' => Ok({token: BraceOpen, location: singleWidth})
    | '|' => Ok({token: Pipe, location: singleWidth})
    | '}' => Ok({token: BraceClose, location: singleWidth})
    | '.' =>
      if (isChar(source, position + 1, '.')
          && isChar(source, position + 2, '.')) {
        Ok({
          token: Spread,
          location: {
            start: position,
            end_: position + 3,
            line,
            column,
          },
        });
      } else if (position + 1 >= String.length(source)) {
        syntaxError("Unexpected End of File");
      } else {
        syntaxError(
          "Unexpected Character" ++ String.make(1, source.[position + 1]),
        );
      }
    | 'A'..'Z'
    | 'a'..'z'
    | '_' => Ok(readName(source, ~start=position, ~line, ~column))
    | '0'..'9'
    | '-' => readNumber(source, ~start=position, ~line, ~column)
    | '"' => readString(source, ~start=position, ~line, ~column)
    | '#' => Ok(readComment(source, ~start=position, ~line, ~column))
    | char => syntaxError("Unexpected Character" ++ String.make(1, char))
    };
  };
};

let make = source => {
  source,
  curr: {
    token: StartOfFile,
    location: {
      start: 0,
      end_: 0,
      column: 0,
      line: 1,
    },
  },
  line: 1,
  lineStart: 0,
};

let lookahead =
  fun
  | {curr: {token: EndOfFile} as tokenResult} => Result.Ok(tokenResult)
  | lexer => {
      let rec skipComment = prevToken => {
        let%Result token = readToken(lexer, prevToken.location.end_);
        switch (token) {
        | {token: Comment(_)} => skipComment(token)
        | token => Ok(token)
        };
      };

      skipComment(lexer.curr);
    };

let advance = lexer => {
  let%Result curr = lookahead(lexer);
  lexer.curr = curr;
  Ok(curr);
};

================================================
FILE: reason-graphql/src/language/Graphql_Language_Parser.re
================================================
open Graphql_Language_Ast;
module Lexer = Graphql_Language_Lexer;

module Result = {
  include Belt.Result;
  let let_ = flatMap;
};

type result('a) = Result.t('a, Graphql_Language_Error.t);

let syntaxError = a => Result.Error(Graphql_Language_Error.SyntaxError(a));

let expectedError = (lexer: Lexer.t, token: Lexer.token) => {
  syntaxError(
    "Expected" ++ Lexer.tokenKind(token) ++ ", found " ++ Lexer.tokenDesc(lexer.curr),
  );
};

let expect = (lexer: Lexer.t, token: Lexer.token) =>
  switch (lexer.curr.token) {
  | currToken when currToken == token => Lexer.advance(lexer)
  | _ => expectedError(lexer, token)
  };

let skip = (lexer: Lexer.t, skipToken: Lexer.token): result(bool) =>
  switch (lexer.curr.token) {
  | token when token == skipToken => Lexer.advance(lexer)->Result.map(_ => true)
  | _ => Ok(false)
  };

let skipKeyword = (lexer: Lexer.t, value: string): result(bool) =>
  switch (lexer.curr.token) {
  | Name(name) when name == value => Lexer.advance(lexer)->Result.map(_ => true)
  | _ => Ok(false)
  };

let expectKeyword = (lexer: Lexer.t, value: string): result(unit) => {
  let%Result skipped = skipKeyword(lexer, value);
  if (!skipped) {
    syntaxError("Expected " ++ value ++ ", found " ++ Lexer.tokenDesc(lexer.curr));
  } else {
    Ok();
  };
};

let unexpected = (lexer: Lexer.t) => {
  syntaxError("Unexpected " ++ Lexer.tokenDesc(lexer.curr));
};

let any =
    (
      lexer: Lexer.t,
      openKind: Lexer.token,
      parseFn: Lexer.t => result('a),
      closeKind: Lexer.token,
    ) => {
  let%Result _ = expect(lexer, openKind);

  let rec collect = nodes => {
    let%Result skipped = skip(lexer, closeKind);
    if (!skipped) {
      let%Result node = parseFn(lexer);
      collect([node, ...nodes]);
    } else {
      Ok(Belt.List.reverse(nodes));
    };
  };

  collect([]);
};

let many =
    (
      lexer: Lexer.t,
      openKind: Lexer.token,
      parseFn: Lexer.t => result('a),
      closeKind: Lexer.token,
    )
    : result(list('a)) => {
  let%Result _ = expect(lexer, openKind);
  let%Result node = parseFn(lexer);

  let rec collect = nodes => {
    let%Result skipped = skip(lexer, closeKind);
    if (!skipped) {
      let%Result node = parseFn(lexer);
      collect([node, ...nodes]);
    } else {
      Ok(Belt.List.reverse(nodes));
    };
  };

  collect([node]);
};

let parseName =
  fun
  | ({curr: {token: Name(value)}} as lexer: Lexer.t) =>
    Lexer.advance(lexer)->Result.map(_ => value)

  | lexer => expectedError(lexer, Name(""));

let parseNamedType = (lexer: Lexer.t) => {
  let%Result name = parseName(lexer);
  Ok(NamedType(name));
};

let parseVariable = (lexer: Lexer.t) => {
  let%Result _ = expect(lexer, Dollar);
  let%Result name = parseName(lexer);
  Ok(`Variable(name));
};

let rec parseValueLiteral = (lexer: Lexer.t, ~isConst: bool): result(value) =>
  Result.(
    switch (lexer.curr.token) {
    | BracketOpen =>
      any(lexer, BracketOpen, parseValueLiteral(~isConst), BracketClose)
      ->map(list => `List(list))
    | BraceOpen => parseObject(lexer, ~isConst)
    | Int(value) => Lexer.advance(lexer)->map(_ => `Int(int_of_string(value)))
    | Float(value) => Lexer.advance(lexer)->map(_ => `Float(float_of_string(value)))
    | String(value) => Lexer.advance(lexer)->map(_ => `String(value))
    | Name("true") => Lexer.advance(lexer)->map(_ => `Boolean(true))
    | Name("false") => Lexer.advance(lexer)->map(_ => `Boolean(false))
    | Name("null") => Lexer.advance(lexer)->map(_ => `Null)
    | Name(value) => Lexer.advance(lexer)->map(_ => `Enum(value))
    | Dollar when !isConst => parseVariable(lexer)
    | _ => unexpected(lexer)
    }
  )

and parseObject = (lexer: Lexer.t, ~isConst: bool) => {
  let%Result _ = expect(lexer, BraceOpen);

  let rec parseFields = fields => {
    let%Result skipped = skip(lexer, BraceClose);
    if (!skipped) {
      let%Result field = parseObjectField(lexer, ~isConst);
      parseFields([field, ...fields]);
    } else {
      Ok(fields);
    };
  };

  let%Result fields = parseFields([]);
  Ok(`Object(Belt.List.reverse(fields)));
}

and parseObjectField = (lexer: Lexer.t, ~isConst: bool): result((string, value)) => {
  let%Result name = parseName(lexer);
  let%Result _ = expect(lexer, Colon);
  let%Result value = parseValueLiteral(lexer, ~isConst);
  Ok((name, value));
};

let rec parseTypeReference = (lexer: Lexer.t) => {
  let%Result typ = {
    let%Result skipped = skip(lexer, BracketOpen);
    if (skipped) {
      let%Result t = parseTypeReference(lexer);
      let%Result _ = expect(lexer, BracketClose);
      Ok(ListType(t));
    } else {
      let%Result typ = parseNamedType(lexer);
      Ok(typ);
    };
  };

  let%Result skipped = skip(lexer, Bang);

  skipped ? Ok(NonNullType(typ)) : Ok(typ);
};

let parseArgument = (lexer: Lexer.t, ~isConst): result((string, value)) => {
  let%Result name = parseName(lexer);
  let%Result _ = expect(lexer, Colon);
  let%Result valueLiteral = parseValueLiteral(lexer, ~isConst);
  Ok((name, valueLiteral));
};

let parseArguments = (lexer: Lexer.t, ~isConst: bool) =>
  switch (lexer.curr.token) {
  | ParenOpen => many(lexer, ParenOpen, parseArgument(~isConst), ParenClose)
  | _ => Ok([])
  };

let parseDirective = (lexer: Lexer.t, ~isConst: bool): result(directive) => {
  let%Result _ = expect(lexer, At);
  let%Result name = parseName(lexer);
  let%Result arguments = parseArguments(lexer, ~isConst);
  Ok({name, arguments}: directive);
};

let parseDirectives = (lexer: Lexer.t, ~isConst: bool) => {
  let rec collect = directives =>
    switch (lexer.curr.token) {
    | At =>
      let%Result directive = parseDirective(lexer, ~isConst);
      collect([directive, ...directives]);
    | _ => Ok(Belt.List.reverse(directives))
    };

  collect([]);
};

/* Operation Definitions */

let parseOperationType = (lexer: Lexer.t): result(Graphql_Language_Ast.operationType) => {
  switch (lexer.curr.token) {
  | Name("query") => Result.Ok(Query)
  | Name("mutation") => Ok(Mutation)
  | Name("subscription") => Ok(Subscription)
  | _ => unexpected(lexer)
  };
};

let parseVariableDefinition = (lexer: Lexer.t): result(variableDefinition) => {
  let%Result variable = parseVariable(lexer);
  let%Result _ = expect(lexer, Colon);
  let%Result typ = parseTypeReference(lexer);
  let%Result directives = parseDirectives(lexer, ~isConst=true);
  Ok({typ, variable, defaultValue: None, directives});
};

let parseVariableDefinitions = (lexer: Lexer.t) =>
  switch (lexer.curr.token) {
  | ParenOpen => many(lexer, ParenOpen, parseVariableDefinition, ParenClose)
  | _ => Ok([])
  };

let rec parseSelectionSet = (lexer: Lexer.t): result(list(selection)) => {
  many(lexer, BraceOpen, parseSelection, BraceClose);
}

and parseSelection = (lexer: Lexer.t): result(selection) =>
  switch (lexer.curr.token) {
  | Spread => parseFragment(lexer)
  | _ => parseField(lexer)
  }

and parseFragmentName = (lexer: Lexer.t) =>
  switch (lexer.curr.token) {
  | Name("on") => unexpected(lexer)
  | _ => parseName(lexer)
  }

and parseFragment = (lexer: Lexer.t) => {
  let%Result _ = expect(lexer, Spread);
  let%Result hasTypeCondition = skipKeyword(lexer, "on");

  switch (lexer.curr.token) {
  | Name(_) when !hasTypeCondition =>
    let%Result name = parseFragmentName(lexer);
    let%Result directives = parseDirectives(lexer, ~isConst=false);
    Ok(FragmentSpread({name, directives}));
  | _ =>
    let typeCondition =
      hasTypeCondition
        ? switch (parseName(lexer)) {
          | Ok(name) => Some(name)
          | _ => None
          }
        : None;
    let%Result directives = parseDirectives(lexer, ~isConst=false);
    let%Result selectionSet = parseSelectionSet(lexer);
    Ok(InlineFragment({typeCondition, directives, selectionSet}));
  };
}

and parseField = (lexer: Lexer.t) => {
  let%Result name = parseName(lexer);

  let%Result (alias, name) = {
    let%Result skipped = skip(lexer, Colon);
    if (skipped) {
      let%Result name2 = parseName(lexer);
      Ok((Some(name), name2));
    } else {
      Ok((None, name));
    };
  };

  let%Result arguments = parseArguments(lexer, ~isConst=false);
  let%Result directives = parseDirectives(lexer, ~isConst=false);
  let%Result selectionSet =
    switch (lexer.curr.token) {
    | BraceOpen => parseSelectionSet(lexer)
    | _ => Ok([])
    };

  Ok(Field({name, alias, arguments, directives, selectionSet}));
};

let parseOperationDefinition = (lexer: Lexer.t) =>
  switch (lexer.curr.token) {
  | BraceOpen =>
    let%Result selectionSet = parseSelectionSet(lexer);
    Ok(
      Operation({
        operationType: Query,
        name: None,
        variableDefinition: [],
        directives: [],
        selectionSet,
      }),
    );
  | _ =>
    let%Result operationType = parseOperationType(lexer);
    let%Result _ = Lexer.advance(lexer);
    let%Result name =
      switch (lexer.curr.token) {
      | Name(name) => Lexer.advance(lexer)->Result.map(_ => Some(name))
      | _ => Ok(None)
      };

    let%Result variableDefinition = parseVariableDefinitions(lexer);
    let%Result directives = parseDirectives(lexer, ~isConst=false);
    let%Result selectionSet = parseSelectionSet(lexer);

    Ok(Operation({operationType, name, variableDefinition, directives, selectionSet}));
  };

let parseFragmentDefinition = (lexer: Lexer.t) => {
  let%Result () = expectKeyword(lexer, "fragment");
  let%Result name = parseFragmentName(lexer);
  let%Result () = expectKeyword(lexer, "on");

  let%Result typeCondition = parseName(lexer);
  let%Result selectionSet = parseSelectionSet(lexer);
  let%Result directives = parseDirectives(lexer, ~isConst=false);

  Ok(Fragment({typeCondition, name, selectionSet, directives}));
};

let parseExecutableDefinition = (lexer: Lexer.t) =>
  switch (lexer.curr.token) {
  | Name("query" | "mutation" | "subscription")
  | BraceOpen => parseOperationDefinition(lexer)
  | Name("fragment") => parseFragmentDefinition(lexer)
  | _ => unexpected(lexer)
  };

let parseDocument = (lexer: Lexer.t): result(document) => {
  let%Result definitions = many(lexer, StartOfFile, parseExecutableDefinition, EndOfFile);
  Ok({definitions: definitions});
};

let parse = (body: string) => {
  let lexer = Lexer.make(body);
  parseDocument(lexer);
};

================================================
FILE: reason-graphql/src/language/Graphql_Language_Printer.re
================================================
open Graphql_Language_Ast;

let join = (list, seperator) => Belt.List.keep(list, x => x !== "") |> String.concat(seperator);

let wrap = (left, str, right) => str === "" ? "" : left ++ str ++ right;

let indent =
  fun
  | "" => ""
  | str => "  " ++ (str |> Js.String.replaceByRe([%bs.re "/\\n/g"], "\n  "));

let block =
  fun
  | [] => ""
  | list => "{\n" ++ indent(join(list, "\n")) ++ "\n}";

let rec printValue: value => string =
  fun
  | `Int(int) => Js.Int.toString(int)
  | `Float(float) => Js.Float.toString(float)
  | `String(string) => string->Js.Json.string->Js.Json.stringify
  | `Boolean(bool) => string_of_bool(bool)
  | `Null => "null"
  | `Variable(string) => "$" ++ string
  | `Enum(enum) => enum
  | `List(values) => "[" ++ join(values |> Belt.List.map(_, printValue), ", ") ++ "]"
  | `Object(fields) => "{" ++ printObjectFields(fields) ++ "}"

and printObjectFields = fields => fields |> Belt.List.map(_, printObjectField) |> join(_, ", ")

and printObjectField = ((k, v)) => k ++ ": " ++ printValue(v);

let rec printType =
  fun
  | NamedType(string) => string
  | ListType(typ) => "[" ++ printType(typ) ++ "]"
  | NonNullType(typ) => printType(typ) ++ "!";

let printVariableDef = ({variable, typ}: variableDefinition) =>
  printValue((variable :> value)) ++ ": " ++ printType(typ);
let printVariables = vars => vars->Belt.List.map(printVariableDef)->join(", ");

let printArgument = ((name, value)) => name ++ ": " ++ printValue(value);
let printArguments = (args: list((string, value))) =>
  args->Belt.List.map(printArgument)->join(", ");

let printDirective = ({name, arguments}: directive) =>
  "@" ++ name ++ wrap("(", printArguments(arguments), ")");

let printDirectives = directives => directives->Belt.List.map(printDirective)->join(" ");

let printOpt =
  fun
  | Some(v) => v
  | None => "";

let rec printSelectionSet = selectionSet =>
  selectionSet |> Belt.List.map(_, printSelection) |> block

and printSelection =
  fun
  | Field(field) => printField(field)
  | FragmentSpread(fragmentSpread) => printFragmentSpread(fragmentSpread)
  | InlineFragment(inlineFragmentDefinition) =>
    printInlineFragmentDefinition(inlineFragmentDefinition)

and printField = ({alias, name, arguments, directives, selectionSet}) =>
  join(
    [
      printAlias(alias) ++ name ++ wrap("(", printArguments(arguments), ")"),
      printDirectives(directives),
      printSelectionSet(selectionSet),
    ],
    " ",
  )

and printAlias =
  fun
  | Some(alias) => alias ++ ": "
  | None => ""

and printFragmentSpread = ({name, directives}) =>
  "..." ++ name ++ wrap(" ", printDirectives(directives), "")

and printInlineFragmentDefinition = ({typeCondition, selectionSet, directives}) =>
  join(
    [
      "...",
      switch (typeCondition) {
      | Some(condition) => wrap("on ", condition, "")
      | None => ""
      },
      printDirectives(directives),
      printSelectionSet(selectionSet),
    ],
    " ",
  );

let printOperationDef =
    ({operationType, variableDefinition, directives, selectionSet} as operationDef) => {
  let operationTypeStr =
    switch (operationType) {
    | Query => "query"
    | Subscription => "subscription"
    | Mutation => "mutation"
    };
  let varDefs = "(" ++ printVariables(variableDefinition) ++ ")";
  let directives = printDirectives(directives);
  let selectionSet = printSelectionSet(selectionSet);

  switch (operationDef) {
  | {operationType: Query, name: None, directives: [], variableDefinition: []} => selectionSet
  | {name} =>
    join(
      [operationTypeStr, join([printOpt(name), varDefs], ""), directives, selectionSet],
      " ",
    )
  };
};

let printFragmentDef = ({name, typeCondition, directives, selectionSet}) =>
  "fragment "
  ++ name
  ++ " on "
  ++ typeCondition
  ++ " "
  ++ wrap("", printDirectives(directives), " ")
  ++ printSelectionSet(selectionSet);

let printDefinition = definition =>
  switch (definition) {
  | Operation(operationDef) => printOperationDef(operationDef)
  | Fragment(fragmentDef) => printFragmentDef(fragmentDef)
  };

let print = ({definitions}: document) =>
  definitions |> Belt.List.map(_, printDefinition) |> join(_, "\n\n");


================================================
FILE: reason-graphql-bs-express/bsconfig.json
================================================
{
  "name": "reason-graphql-bs-express",
  "version": "0.6.0",
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    },
    {
      "dir": "examples",
      "type" : "dev"
    }
  ],
  "package-specs": {
    "module": "commonjs",
    "in-source": true
  },
  "suffix": ".bs.js",
  "warnings": {
    "number": "-45-44",
    "error": "+101"
  },
  "bs-dependencies": ["reason-graphql", "bs-express", "reason-future"],
  "namespace": false,
  "refmt": 3
}


================================================
FILE: reason-graphql-bs-express/examples/server.re
================================================
type ctx = {userIP: string};

module HelloWorldSchema = {
  open GraphqlJsPromise;

  let rootQuery =
    Schema.(
      query([
        field(
          "hello",
          nonnull(string),
          ~args=Arg.[defaultArg("name", string, ~default="world")],
          ~resolve=(ctx, (), name) =>
          name ++ " (" ++ ctx.userIP ++ ")"
        ),
        async_field(
          "helloAsync",
          nonnull(string),
          ~args=Arg.[defaultArg("name", string, ~default="world")],
          ~resolve=(ctx, (), name) =>
          Js.Promise.resolve(
            Belt.Result.Ok(name ++ " (" ++ ctx.userIP ++ ")"),
          )
        ),
      ])
    );

  let schema = Schema.create(rootQuery);
};

open Express;

let app = express();

App.use(app, Middleware.json(~limit=ByteLimit.mb(5.0), ()));

App.useOnPath(app, ~path="/graphql") @@
GraphqlExpress.middleware(
  HelloWorldSchema.schema,
  ~provideCtx=(req, _res) => {userIP: Request.ip(req)},
  ~graphiql=true,
);

let onListen = e =>
  switch (e) {
  | exception (Js.Exn.Error(e)) =>
    Js.log(e);
    Node.Process.exit(1);
  | _ => Js.log("Listening at http://127.0.0.1:3000")
  };

App.listen(app, ~port=3000, ~onListen, ());

================================================
FILE: reason-graphql-bs-express/package.json
================================================
{
  "name": "reason-graphql-bs-express",
  "version": "0.6.0",
  "author": "Sikan He",
  "license": "MIT",
  "repository": "https://github.com/sikanhe/reason-graphql/reason-graphql-bs-express",
  "homepage": "https://github.com/sikanhe/reason-graphql/reason-graphql-bs-express#readme",
  "keywords": [
    "BuckleScript",
    "GraphQL",
    "Express",
    "Reason",
    "ReasonML"
  ],
  "scripts": {
    "build": "bsb -make-world",
    "start": "bsb -make-world -w",
    "clean": "bsb -clean-world",
    "test": "yarn build && jest"
  },
  "files": [
    "src/",
    "bsconfig.json"
  ],
  "devDependencies": {
    "bs-platform": "^7.0.0",
    "reason-graphql": "^0.6.0",
    "bs-express": "^0.12.0",
    "reason-future": "^2.4.0"
  },
  "peerDependencies": {
    "reason-graphql": "^0.6.0",
    "bs-express": "^0.12.0",
    "reason-future": "^2.4.0",
    "bs-platform": ">= 7.0.0"
  }
}


================================================
FILE: reason-graphql-bs-express/src/Graphiql.re
================================================
let html = {j|
<!--
The request to this GraphQL server provided the header "Accept: text/html"
and as a result has been presented GraphiQL - an in-browser IDE for
exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser.
-->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>GraphiQL</title>
  <meta name="robots" content="noindex" />
  <meta name="referrer" content="origin" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }
    #graphiql {
      height: 100vh;
    }
  </style>
  <link href="//cdn.jsdelivr.net/npm/graphiql@0.12.0/graphiql.css" rel="stylesheet" />
  <script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script>
  <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
  <script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script>
  <script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/graphiql@0.12.0/graphiql.min.js"></script>
</head>
<body>
  <div id="graphiql">Loading...</div>
  <script>
    // Collect the URL parameters
    var parameters = {};
    window.location.search.substr(1).split('&').forEach(function (entry) {
      var eq = entry.indexOf('=');
      if (eq >= 0) {
        parameters[decodeURIComponent(entry.slice(0, eq))] =
          decodeURIComponent(entry.slice(eq + 1));
      }
    });
    // Produce a Location query string from a parameter object.
    function locationQuery(params) {
      return '?' + Object.keys(params).filter(function (key) {
        return Boolean(params[key]);
      }).map(function (key) {
        return encodeURIComponent(key) + '=' +
          encodeURIComponent(params[key]);
      }).join('&');
    }
    // Derive a fetch URL from the current URL, sans the GraphQL parameters.
    var graphqlParamNames = {
      query: true,
      variables: true,
      operationName: true
    };
    var otherParams = {};
    for (var k in parameters) {
      if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
        otherParams[k] = parameters[k];
      }
    }
    var fetchURL = locationQuery(otherParams);
    // Defines a GraphQL fetcher using the fetch API.
    function graphQLFetcher(graphQLParams) {
      return fetch(fetchURL, {
        method: 'post',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(graphQLParams),
        credentials: 'include',
      }).then(function (response) {
        return response.json();
      });
    }
    // When the query and variables string is edited, update the URL bar so
    // that it can be easily shared.
    function onEditQuery(newQuery) {
      parameters.query = newQuery;
      updateURL();
    }
    function onEditVariables(newVariables) {
      parameters.variables = newVariables;
      updateURL();
    }
    function onEditOperationName(newOperationName) {
      parameters.operationName = newOperationName;
      updateURL();
    }
    function updateURL() {
      history.replaceState(null, null, locationQuery(parameters));
    }
    // Render <GraphiQL /> into the body.
    ReactDOM.render(
      React.createElement(GraphiQL, {
        fetcher: graphQLFetcher,
        onEditQuery: onEditQuery,
        onEditVariables: onEditVariables,
        onEditOperationName: onEditOperationName
      }),
      document.getElementById('graphiql')
    );
  </script>
</body>
</html>
|j};

================================================
FILE: reason-graphql-bs-express/src/GraphqlExpress.re
================================================
open Express;

let parseBodyIntoDocumentAndVariables = req => {
  switch (Request.bodyJSON(req)) {
  | Some(body) =>
    switch (Js.Json.decodeObject(body)) {
    | Some(body) =>
      switch (Js.Dict.get(body, "query")) {
      | Some(query) =>
        switch (Js.Json.decodeString(query)) {
        | Some(queryString) =>
          switch (Graphql.Language.Parser.parse(queryString)) {
          | Ok(document) =>
            switch (Js.Dict.get(body, "variables")) {
            | Some(variablesJson) =>
              switch (Graphql.Json.toVariables(variablesJson)) {
              | Ok(variables) => Belt.Result.Ok((document, Some(variables)))
              | Error(_) => Ok((document, None))
              }
            | None => Ok((document, None))
            }
          | Error(SyntaxError(s)) => Error("GraphQL Syntax Error: " ++ s)
          }
        | None => Error("Query must be a string")
        }
      | None => Error("Must provide Query string")
      }
    | None => Error("body must be an JSON object")
    }
  | None => Error("no body found")
  };
};

let middleware = (~provideCtx, ~graphiql=false, schema) =>
  PromiseMiddleware.from((next, req, res) =>
    switch (Request.methodRaw(req)) {
    | "GET" when graphiql =>
      res
      |> Response.status(Response.StatusCode.Ok)
      |> Response.sendString(Graphiql.html)
      |> Js.Promise.resolve
    | "POST" =>
      switch (parseBodyIntoDocumentAndVariables(req)) {
      | Ok((document, variables)) =>
        GraphqlJsPromise.Schema.execute(
          schema,
          ~document,
          ~variables?,
          ~ctx=provideCtx(req, res),
        )
        |> Js.Promise.(
             then_(const => resolve(Graphql.Json.fromConstValue(const)))
           )
        |> Js.Promise.(then_(json => resolve(Response.sendJson(json, res))))
      | Error(error) =>
        res
        |> Response.status(Response.StatusCode.BadRequest)
        |> Response.sendString(error)
        |> Js.Promise.resolve
      }
    | _ => Js.Promise.resolve(next(Next.route, res))
    }
  );
Download .txt
gitextract_17cd7pmq/

├── .circleci/
│   └── config.yml
├── .github/
│   └── workflows/
│       └── nodejs.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── examples/
│   └── starwars-api/
│       ├── .gitignore
│       ├── README.md
│       ├── bsconfig.json
│       ├── package.json
│       └── src/
│           ├── schema.re
│           ├── server.re
│           └── swapi.re
├── reason-graphql/
│   ├── __tests__/
│   │   ├── GraphqlJsPromise.re
│   │   ├── StarWarsData.re
│   │   ├── StarWarsSchema.re
│   │   ├── parser_test.re
│   │   └── schema_test.re
│   ├── bsconfig.json
│   ├── package.json
│   └── src/
│       ├── Graphql.re
│       ├── Graphql_Interface.re
│       ├── Graphql_Json.re
│       ├── Graphql_Schema.re
│       ├── Graphql_Schema.rei
│       └── language/
│           ├── Graphql_Language.re
│           ├── Graphql_Language_Ast.re
│           ├── Graphql_Language_Error.re
│           ├── Graphql_Language_Lexer.re
│           ├── Graphql_Language_Parser.re
│           └── Graphql_Language_Printer.re
└── reason-graphql-bs-express/
    ├── bsconfig.json
    ├── examples/
    │   └── server.re
    ├── package.json
    └── src/
        ├── Graphiql.re
        └── GraphqlExpress.re
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (175K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 670,
    "preview": "version: 2\njobs:\n  build:\n    docker:\n      - image: circleci/node:11.8.0\n\n    environment:\n      TEST_REPORTS: /tmp/tes"
  },
  {
    "path": ".github/workflows/nodejs.yml",
    "chars": 472,
    "preview": "name: Node CI\n\non: [push]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version:"
  },
  {
    "path": ".gitignore",
    "chars": 87,
    "preview": ".DS_Store\n.merlin\n.bsb.lock\nnpm-debug.log\nlib\nnode_modules\n.vscode\n*.bs.js\n.pnp\n.pnp.js"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1228,
    "preview": "## DEV\n### *reason-graphql*\n* (Breaking) Removed \"Variations\". Instead, we provide guidance on how to integrate this wit"
  },
  {
    "path": "LICENSE.md",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2019 Sikan He\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 3907,
    "preview": "[![CircleCI](https://circleci.com/gh/sikanhe/reason-graphql/tree/master.svg?style=svg)](https://circleci.com/gh/sikanhe/"
  },
  {
    "path": "examples/starwars-api/.gitignore",
    "chars": 66,
    "preview": ".DS_Store\n.merlin\n.bsb.lock\nnpm-debug.log\n/lib/bs/\n/node_modules/\n"
  },
  {
    "path": "examples/starwars-api/README.md",
    "chars": 98,
    "preview": "# StarWars API using reason-graphql \n\nLive demo: https://reason-graphql-swapi.onrender.com/graphql"
  },
  {
    "path": "examples/starwars-api/bsconfig.json",
    "chars": 494,
    "preview": "{\n  \"name\": \"starwars-api\",\n  \"version\": \"0.1.0\",\n  \"sources\": {\n    \"dir\" : \"src\",\n    \"subdirs\" : true\n  },\n  \"package"
  },
  {
    "path": "examples/starwars-api/package.json",
    "chars": 584,
    "preview": "{\n  \"name\": \"starwars-api\",\n  \"version\": \"0.1.0\",\n  \"scripts\": {\n    \"build\": \"bsb -make-world\",\n    \"start\": \"bsb -make"
  },
  {
    "path": "examples/starwars-api/src/schema.re",
    "chars": 13528,
    "preview": "open GraphqlFuture;\n\nlet starship =\n  Schema.(\n    obj(\"starship\", ~fields=_ =>\n      [\n        field(\n          \"name\","
  },
  {
    "path": "examples/starwars-api/src/server.re",
    "chars": 519,
    "preview": "open Express;\n\nlet app = express();\n\nApp.use(app, Middleware.json(~limit=ByteLimit.mb(5.0), ()));\n\nApp.useOnPath(app, ~p"
  },
  {
    "path": "examples/starwars-api/src/swapi.re",
    "chars": 9253,
    "preview": "type film = {\n  title: string,\n  episodeID: int,\n  openingCrawl: string,\n  director: string,\n  producers: list(string),\n"
  },
  {
    "path": "reason-graphql/__tests__/GraphqlJsPromise.re",
    "chars": 328,
    "preview": "module Schema =\n  Graphql.Schema.Make({\n    type t('a) = Js.Promise.t('a);\n\n    let return = Js.Promise.resolve;\n    let"
  },
  {
    "path": "reason-graphql/__tests__/StarWarsData.re",
    "chars": 2804,
    "preview": "type episode =\n  | NEWHOPE\n  | EMPIRE\n  | JEDI;\n\ntype human = {\n  id: int,\n  name: string,\n  friends: list(int),\n  appea"
  },
  {
    "path": "reason-graphql/__tests__/StarWarsSchema.re",
    "chars": 6353,
    "preview": "open Belt.Result;\nopen GraphqlJsPromise;\n\nmodule StarWars = StarWarsData;\n\nlet episodeEnum =\n  Schema.(\n    makeEnum(\n  "
  },
  {
    "path": "reason-graphql/__tests__/parser_test.re",
    "chars": 3662,
    "preview": "open Jest;\nopen Graphql_Language;\n\ndescribe(\"Parse and print a graphql query\", () => {\n  open Expect;\n\n  let query = {|\n"
  },
  {
    "path": "reason-graphql/__tests__/schema_test.re",
    "chars": 11648,
    "preview": "open Jest;\nopen Expect;\nopen Graphql.Language;\nopen GraphqlJsPromise;\n\nlet schema = StarWarsSchema.schema;\n\nlet okRespon"
  },
  {
    "path": "reason-graphql/bsconfig.json",
    "chars": 512,
    "preview": "{\n  \"name\": \"reason-graphql\",\n  \"version\": \"0.6.1\",\n  \"sources\": [\n    {\n      \"dir\": \"src\",\n      \"subdirs\": true\n    }"
  },
  {
    "path": "reason-graphql/package.json",
    "chars": 731,
    "preview": "{\n  \"name\": \"reason-graphql\",\n  \"version\": \"0.6.1\",\n  \"author\": \"Sikan He\",\n  \"license\": \"MIT\",\n  \"repository\": \"https:/"
  },
  {
    "path": "reason-graphql/src/Graphql.re",
    "chars": 134,
    "preview": "module Schema = Graphql_Schema;\nmodule Language = Graphql_Language;\nmodule Json = Graphql_Json;\nmodule Interface = Graph"
  },
  {
    "path": "reason-graphql/src/Graphql_Interface.re",
    "chars": 6993,
    "preview": "module type IO = {\n  type t(+'a);\n  let return: 'a => t('a);\n  let bind: (t('a), 'a => t('b)) => t('b);\n  let map: (t('a"
  },
  {
    "path": "reason-graphql/src/Graphql_Json.re",
    "chars": 1698,
    "preview": "let rec fromConstValue: Graphql_Language_Ast.constValue => Js.Json.t =\n  fun\n  | `String(string)\n  | `Enum(string) => Js"
  },
  {
    "path": "reason-graphql/src/Graphql_Schema.re",
    "chars": 52495,
    "preview": "open Graphql_Language;\n\nmodule Result = Belt.Result;\nmodule Option = Belt.Option;\n\nmodule List = {\n  include Belt.List;\n"
  },
  {
    "path": "reason-graphql/src/Graphql_Schema.rei",
    "chars": 89,
    "preview": "module Make: (Io: Graphql_Interface.IO) => Graphql_Interface.Schema with module Io = Io;\n"
  },
  {
    "path": "reason-graphql/src/language/Graphql_Language.re",
    "chars": 158,
    "preview": "module Ast = Graphql_Language_Ast;\nmodule Lexer = Graphql_Language_Lexer;\nmodule Parser = Graphql_Language_Parser;\nmodul"
  },
  {
    "path": "reason-graphql/src/language/Graphql_Language_Ast.re",
    "chars": 9832,
    "preview": "type primitiveValue = [\n  | `Int(int)\n  | `Float(float)\n  | `Boolean(bool)\n  | `String(string)\n  | `Enum(string)\n  | `Nu"
  },
  {
    "path": "reason-graphql/src/language/Graphql_Language_Error.re",
    "chars": 34,
    "preview": "type t =\n  | SyntaxError(string);\n"
  },
  {
    "path": "reason-graphql/src/language/Graphql_Language_Lexer.re",
    "chars": 12474,
    "preview": "module Result = {\n  include Belt.Result;\n  let let_ = flatMap;\n};\n\ntype result('a) = Result.t('a, Graphql_Language_Error"
  },
  {
    "path": "reason-graphql/src/language/Graphql_Language_Parser.re",
    "chars": 10268,
    "preview": "open Graphql_Language_Ast;\nmodule Lexer = Graphql_Language_Lexer;\n\nmodule Result = {\n  include Belt.Result;\n  let let_ ="
  },
  {
    "path": "reason-graphql/src/language/Graphql_Language_Printer.re",
    "chars": 4168,
    "preview": "open Graphql_Language_Ast;\n\nlet join = (list, seperator) => Belt.List.keep(list, x => x !== \"\") |> String.concat(seperat"
  },
  {
    "path": "reason-graphql-bs-express/bsconfig.json",
    "chars": 466,
    "preview": "{\n  \"name\": \"reason-graphql-bs-express\",\n  \"version\": \"0.6.0\",\n  \"sources\": [\n    {\n      \"dir\": \"src\",\n      \"subdirs\":"
  },
  {
    "path": "reason-graphql-bs-express/examples/server.re",
    "chars": 1192,
    "preview": "type ctx = {userIP: string};\n\nmodule HelloWorldSchema = {\n  open GraphqlJsPromise;\n\n  let rootQuery =\n    Schema.(\n     "
  },
  {
    "path": "reason-graphql-bs-express/package.json",
    "chars": 889,
    "preview": "{\n  \"name\": \"reason-graphql-bs-express\",\n  \"version\": \"0.6.0\",\n  \"author\": \"Sikan He\",\n  \"license\": \"MIT\",\n  \"repository"
  },
  {
    "path": "reason-graphql-bs-express/src/Graphiql.re",
    "chars": 3628,
    "preview": "let html = {j|\n<!--\nThe request to this GraphQL server provided the header \"Accept: text/html\"\nand as a result has been "
  },
  {
    "path": "reason-graphql-bs-express/src/GraphqlExpress.re",
    "chars": 2059,
    "preview": "open Express;\n\nlet parseBodyIntoDocumentAndVariables = req => {\n  switch (Request.bodyJSON(req)) {\n  | Some(body) =>\n   "
  }
]

About this extraction

This page contains the full source code of the sikanhe/reason-graphql GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (160.7 KB), approximately 42.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!