├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE.md ├── README.md ├── examples ├── github.ml ├── jbuild └── schema.json ├── pkg └── pkg.ml ├── ppx_graphql.descr ├── ppx_graphql.opam ├── src ├── introspection.ml ├── jbuild └── ppx_graphql.ml └── test ├── generate_schema.js ├── integration_test.ml ├── jbuild ├── parsing_test.ml ├── schema.graphql ├── schema.json ├── test.ml ├── test_helper.ml └── variables_test.ml /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _tests 3 | *.native 4 | *.byte 5 | *.install 6 | .merlin -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: false 3 | services: 4 | - docker 5 | install: wget https://raw.githubusercontent.com/ocaml/ocaml-travisci-skeleton/master/.travis-docker.sh 6 | script: bash -ex ./.travis-docker.sh 7 | env: 8 | global: 9 | - PINS="ppx_graphql:." 10 | matrix: 11 | - PACKAGE="ppx_graphql" DISTRO="debian-stable" OCAML_VERSION="4.03.0" 12 | - PACKAGE="ppx_graphql" DISTRO="fedora-25" OCAML_VERSION="4.04.1" 13 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.2.0 2018-04-01 2 | --------------------------------- 3 | 4 | - Basic union support (#2) 5 | 6 | 0.1.0 2017-08-22 7 | --------------------------------- 8 | 9 | Initial public release. 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Andreas Garnæs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Type-safe GraphQL queries in OCaml 2 | ----------------------------------------------- 3 | 4 | ![Build Status](https://travis-ci.org/andreas/ppx_graphql.svg?branch=master) 5 | 6 | Given a GraphQL schema (introspection query response) and a GraphQL query, `ppx_graphql` generates three values: (1) a GraphQL query (2) a function to construct the associated query variables, and (3) a function for parsing the GraphQL JSON response into a typed value (object type). 7 | 8 | Here's an example of using `ppx_graphql` and the generated values (the schema is shown at the top in [GraphQL Schema Language](https://raw.githubusercontent.com/sogko/graphql-shorthand-notation-cheat-sheet/master/graphql-shorthand-notation-cheat-sheet.png)): 9 | 10 | ```ocaml 11 | (* 12 | enum ROLE { 13 | USER 14 | ADMIN 15 | } 16 | 17 | type User { 18 | id: ID! 19 | role: ROLE 20 | contacts: [User!]! 21 | } 22 | 23 | type Query { 24 | user(id: ID!): User 25 | } 26 | 27 | schema { 28 | query: Query 29 | } 30 | *) 31 | 32 | let query, kvariables, parse = [%graphql {| 33 | query FindUser($id: ID!) { 34 | user(id: $id) { 35 | id 36 | role 37 | contacts { 38 | id 39 | } 40 | } 41 | } 42 | |}] in 43 | (* ... *) 44 | ``` 45 | 46 | In this example, the following values are generated: 47 | 48 | - `query` (type `string`) is the GraphQL query to be submitted. Currently it's an unmodified version of the string provided to `%graphql`, but it will likely be modified in the future, e.g. to inject `__typename` for interface disambiguation. 49 | - `kvariables` (type `(Yojson.Basic.json -> 'a) -> id:string -> unit -> 'a`) is a function to construct the JSON value to submit as query variables ([doc](http://graphql.org/learn/serving-over-http/#post-request)). Note that the first argument is a continuation to handle the resulting JSON value -- this makes it easier to write nice clients (see more below). The type is extracted from the query. Required variables appear as labeled arguments, optional variables appear as optional arguments. 50 | - `parse` is a function for parsing the JSON response from the server and has the type: 51 | 52 | ``` 53 | Yojson.Basic.json -> 54 | `USER | `ADMIN] option; 57 | contacts: list> 58 | > 59 | ``` 60 | This type captures the shape of the GraphQL response in a type-safe fashion based on the provided schema. Scalars are converted to their OCaml equivalent (e.g. a GraphQL `String` is an OCaml `string`), nullable types are converted to `option` types, enums to polymorphic variants, lists to list types and GraphQL objects to OCaml objects. Note that this function will likely return a `result` type in the future, as the GraphQL query can fail. 61 | 62 | With the above, it's possible to write quite executable queries quite easily: 63 | 64 | ```ocaml 65 | let executable_query (query, kvariables, parse) = 66 | kvariables (fun variables -> 67 | let response_body = (* construct HTTP body here and submit to GraphQL endpoint *) in 68 | Yojson.Basic.of_string response_body 69 | |> parse 70 | ) 71 | 72 | let find_user_role = executable_query [%graphql {| 73 | query FindUserRole($id: ID!) { 74 | user(id: $id) { 75 | role 76 | } 77 | } 78 | |}] 79 | ``` 80 | Here `find_user_role` has the type ```id:string -> unit -> option>```. See [`github.ml`](https://github.com/andreas/ocaml-graphql-server/blob/ppx/ppx_graphql/examples/github.ml) for a real example using `Lwt` and `Cohttp`. 81 | 82 | `[%graphql ...]` expects a file `schema.json` to be present in the same directory as the source file. This file should contain an introspection query response. 83 | 84 | For use with jbuilder, use the `preprocess`- and `preprocessor_deps`-stanza: 85 | 86 | ``` 87 | (executable 88 | (preprocess (pps (ppx_graphql))) 89 | (preprocessor_deps ((file schema.json))) 90 | ... 91 | ) 92 | ``` 93 | 94 | ### Unions 95 | 96 | When a field of type union is part of your GraphQL query, you must select `__typename` on that field, otherwise you will get a runtime error! This limitation is intended to be solved in the future. 97 | 98 | Example: 99 | 100 | ```ocaml 101 | let _ = [%graphql {| 102 | query SearchRepositories($query: String!) { 103 | search(query: $query, type: REPOSITORY, first: 5) { 104 | nodes { 105 | __typename 106 | ...on Repository { 107 | nameWithOwner 108 | } 109 | } 110 | } 111 | } 112 | |}] 113 | ``` 114 | 115 | ### Limitations and Future Work 116 | 117 | - No support for input objects 118 | - No support for interfaces 119 | - No support for custom scalar types 120 | - Poor error handling 121 | - Error reporting should be improved 122 | - Path to JSON introspection query result is hardcoded to "schema.json" 123 | - Assumes the query has already been validated 124 | -------------------------------------------------------------------------------- /examples/github.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Infix 2 | 3 | let executable_query (query, kvariables, parse) = 4 | fun ~token -> ( 5 | kvariables (fun variables -> 6 | let uri = Uri.of_string "https://api.github.com/graphql" in 7 | let headers = Cohttp.Header.of_list [ 8 | "Authorization", "bearer " ^ token; 9 | "User-Agent", "andreas/ppx_graphql"; 10 | ] in 11 | let body = `Assoc [ 12 | "query", `String query; 13 | "variables", variables; 14 | ] in 15 | let serialized_body = Yojson.Basic.to_string body in 16 | Cohttp_lwt_unix.Client.post ~headers ~body:(`String serialized_body) uri >>= fun (rsp, body) -> 17 | Cohttp_lwt.Body.to_string body >|= fun body' -> 18 | match Cohttp.Code.(code_of_status rsp.status |> is_success) with 19 | | false -> 20 | Error body' 21 | | true -> 22 | try 23 | Ok (Yojson.Basic.from_string body' |> parse) 24 | with Yojson.Json_error err -> 25 | Error err 26 | ) 27 | ) 28 | 29 | let find_repository = executable_query [%graphql {| 30 | query FindRepository($owner: String!, $name: String!) { 31 | repository(owner: $owner, name: $name) { 32 | id 33 | description 34 | } 35 | } 36 | |}] 37 | 38 | let search_repositories = executable_query [%graphql {| 39 | query SearchRepositories($query: String!) { 40 | search(query: $query, type: REPOSITORY, first: 5) { 41 | nodes { 42 | __typename 43 | ...on Repository { 44 | nameWithOwner 45 | } 46 | } 47 | } 48 | } 49 | |}] 50 | 51 | let main () = 52 | let token = Unix.getenv "GITHUB_TOKEN" in 53 | search_repositories ~token ~query:"topic:mirageos" () >|= function 54 | | Ok rsp -> 55 | begin match rsp#search#nodes with 56 | | Some nodes -> 57 | List.iter (function 58 | | Some (`Repository r) -> Format.printf "Repo: %s\n" r#nameWithOwner 59 | | _ -> () 60 | ) nodes 61 | | None -> 62 | Format.printf "Empty search result" 63 | end 64 | | Error err -> 65 | Format.printf "Error! %s" err 66 | 67 | let () = 68 | Lwt_main.run (main ()) 69 | -------------------------------------------------------------------------------- /examples/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (executable 4 | ((libraries (lwt cohttp.lwt uri)) 5 | (preprocess (pps (ppx_graphql))) 6 | (preprocessor_deps ((file schema.json))) 7 | (modules (github)) 8 | (name github)) 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/pkg.ml: -------------------------------------------------------------------------------- 1 | #use "topfind" 2 | #require "topkg-jbuilder.auto" 3 | -------------------------------------------------------------------------------- /ppx_graphql.descr: -------------------------------------------------------------------------------- 1 | Write type-safe GraphQL queries 2 | 3 | Given a introspection query response in `schema.json`, the expression `[%graphql {| query { ... } |} ]` is rewritten to a 3-tuple `(query, kvariables, parse)`: 4 | 5 | - `query` (type `string`) is the GraphQL query to be submitted. 6 | - `kvariables` (type `(Yojson.Basic.json -> 'a) -> arg1:_ -> ... -> argn:_ -> unit -> 'a`) is a function to construct the JSON value to submit as query variables. The labels and types of `argx` are extracted from the query. Required variables appear as labeled arguments, optional variables appear as optional arguments. 7 | - `parse` is a function for parsing the JSON response from the server and has the type `Yojson.Basic.json -> < ... >`. The shape of the object type corresponds to the returned response. 8 | -------------------------------------------------------------------------------- /ppx_graphql.opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.2" 2 | maintainer: "Andreas Garnaes " 3 | authors: "Andreas Garnaes " 4 | homepage: "https://github.com/andreas/ppx_graphql" 5 | doc: "https://andreas.github.io/ppx_graphql/" 6 | bug-reports: "https://github.com/andreas/ppx_graphql/issues" 7 | dev-repo: "https://github.com/andreas/ppx_graphql.git" 8 | build: [["jbuilder" "build" "-p" name "-j" jobs]] 9 | build-test: [["jbuilder" "runtest" "-p" name "-j" jobs]] 10 | depends: [ 11 | "jbuilder" {build} 12 | "graphql" 13 | "yojson" 14 | "ppx_metaquot" 15 | "alcotest" {test & >= "0.4.5"} 16 | ] 17 | -------------------------------------------------------------------------------- /src/introspection.ml: -------------------------------------------------------------------------------- 1 | type enum_value = { 2 | name : string; 3 | } 4 | 5 | type field = { 6 | name : string; 7 | args : input_value list; 8 | typ : type_ref; 9 | } 10 | 11 | and typ = 12 | | Object of { 13 | name : string; 14 | fields : field list; 15 | interfaces : string list; 16 | } 17 | | Scalar of { 18 | name : string; 19 | } 20 | | Interface of { 21 | name : string; 22 | fields : field list; 23 | possible_types : string list; 24 | } 25 | | Union of { 26 | name : string; 27 | possible_types : string list; 28 | } 29 | | Enum of { 30 | name : string; 31 | enum_values : enum_value list; 32 | } 33 | | InputObject of { 34 | name : string; 35 | input_fields : input_value list; 36 | } 37 | 38 | and input_value = { 39 | name : string; 40 | typ : type_ref; 41 | default_value : string option; 42 | } 43 | 44 | and type_ref = 45 | | List of type_ref 46 | | NonNull of type_ref 47 | | Type of string 48 | 49 | type schema = { 50 | query_type : string; 51 | mutation_type : string option; 52 | types : typ list; 53 | } 54 | 55 | let named_of_yojson json = 56 | Yojson.Basic.Util.(json |> member "name" |> to_string) 57 | 58 | let enum_value_of_yojson json = 59 | let open Yojson.Basic.Util in 60 | { 61 | name = json |> member "name" |> to_string; 62 | } 63 | 64 | let rec field_of_yojson json = 65 | let open Yojson.Basic.Util in 66 | { 67 | name = json |> member "name" |> to_string; 68 | args = json |> member "args" |> convert_each input_value_of_yojson; 69 | typ = json |> member "type" |> type_ref_of_yojson; 70 | } 71 | 72 | and typ_of_yojson json = 73 | let open Yojson.Basic.Util in 74 | let name = json |> member "name" |> to_string in 75 | let kind = json |> member "kind" |> to_string in 76 | match kind with 77 | | "SCALAR" -> 78 | Scalar { 79 | name; 80 | } 81 | | "OBJECT" -> 82 | Object { 83 | name; 84 | fields = json |> member "fields" |> convert_each field_of_yojson; 85 | interfaces = json |> member "interfaces" |> convert_each named_of_yojson; 86 | } 87 | | "INTERFACE" -> 88 | Interface { 89 | name; 90 | fields = json |> member "fields" |> convert_each field_of_yojson; 91 | possible_types = json |> member "possibleTypes" |> convert_each named_of_yojson; 92 | } 93 | | "UNION" -> 94 | Union { 95 | name; 96 | possible_types = json |> member "possibleTypes" |> convert_each named_of_yojson; 97 | } 98 | | "ENUM" -> 99 | Enum { 100 | name; 101 | enum_values = json |> member "enumValues" |> convert_each enum_value_of_yojson; 102 | } 103 | | "INPUT_OBJECT" -> 104 | InputObject { 105 | name; 106 | input_fields = json |> member "inputFields" |> convert_each input_value_of_yojson; 107 | } 108 | | _ -> failwith (Format.sprintf "Unknown kind `%s`" kind) 109 | 110 | and input_value_of_yojson json = 111 | let open Yojson.Basic.Util in 112 | { 113 | name = json |> member "name" |> to_string; 114 | typ = json |> member "type" |> type_ref_of_yojson; 115 | default_value = json |> member "defaultValue" |> to_string_option; 116 | } 117 | 118 | and type_ref_of_yojson json = 119 | let open Yojson.Basic.Util in 120 | let kind = json |> member "kind" |> to_string in 121 | match kind with 122 | | "SCALAR" 123 | | "OBJECT" 124 | | "INTERFACE" 125 | | "UNION" 126 | | "ENUM" 127 | | "INPUT_OBJECT" -> 128 | let name = json |> member "name" |> to_string in 129 | Type name 130 | | "LIST" -> 131 | let of_type = json |> member "ofType" |> type_ref_of_yojson in 132 | List of_type 133 | | "NON_NULL" -> 134 | let of_type = json |> member "ofType" |> type_ref_of_yojson in 135 | NonNull of_type 136 | | _ -> failwith (Format.sprintf "Unknown kind `%s`" kind) 137 | 138 | let schema_of_yojson json = 139 | let open Yojson.Basic.Util in 140 | { 141 | query_type = json |> member "queryType" |> named_of_yojson; 142 | mutation_type = json |> member "mutationType" |> to_option (named_of_yojson); 143 | types = json |> member "types" |> convert_each typ_of_yojson; 144 | } 145 | 146 | let of_file f = 147 | let chan = open_in f in 148 | let json = Yojson.Basic.from_channel chan in 149 | Yojson.Basic.Util.(json |> member "data" |> member "__schema" |> schema_of_yojson) 150 | -------------------------------------------------------------------------------- /src/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (library 4 | ((name ppx_graphql) 5 | (public_name ppx_graphql) 6 | (kind ppx_rewriter) 7 | (libraries (ocaml-migrate-parsetree graphql yojson)) 8 | (ppx_runtime_libraries (yojson)) 9 | (preprocess (pps (ppx_metaquot))))) 10 | -------------------------------------------------------------------------------- /src/ppx_graphql.ml: -------------------------------------------------------------------------------- 1 | open Migrate_parsetree 2 | open Ast_403 3 | 4 | module StringMap = Map.Make(String) 5 | 6 | type ctx = { 7 | fragments : Graphql_parser.fragment StringMap.t; 8 | types : Introspection.typ list; 9 | } 10 | 11 | let loc = !Ast_helper.default_loc 12 | 13 | let lid s : Ast_helper.lid = 14 | { txt = Longident.Lident s; loc = !Ast_helper.default_loc } 15 | 16 | let str txt : Ast_helper.str = { txt; loc = !Ast_helper.default_loc } 17 | 18 | let const_string s = 19 | Ast_helper.Exp.constant (Pconst_string (s, None)) 20 | 21 | let failwithf format = 22 | Format.ksprintf failwith format 23 | 24 | let rec exprs_to_list = function 25 | | [] -> 26 | Ast_helper.Exp.construct (lid "[]") None 27 | | expr::exprs -> 28 | Ast_helper.Exp.(construct (lid "::") (Some (tuple [expr; exprs_to_list exprs]))) 29 | 30 | let typename_field : Introspection.field = 31 | { 32 | name = "__typename"; 33 | args = []; 34 | typ = NonNull(Type("String")); 35 | } 36 | 37 | let select_field typ field_name = 38 | if field_name = "__typename" then 39 | typename_field 40 | else 41 | let find_field type_name fields = 42 | try 43 | List.find (fun (field : Introspection.field) -> field.name = field_name) fields 44 | with Not_found -> 45 | failwithf "Invalid field `%s` for type `%s`" field_name type_name 46 | in 47 | match typ with 48 | | Introspection.Object o -> 49 | find_field o.name o.fields 50 | | Introspection.Interface i -> 51 | find_field i.name i.fields 52 | | Introspection.Union u -> 53 | failwith "Cannot select field from union" 54 | | Introspection.Enum _ -> 55 | failwith "Cannot select field from enum" 56 | | Introspection.Scalar _ -> 57 | failwith "Cannot select field from scalar" 58 | | Introspection.InputObject _ -> 59 | failwith "Cannot select field from input object" 60 | 61 | let match_type_name type_name typ = 62 | match typ with 63 | | Introspection.Object o -> o.name = type_name 64 | | Introspection.Union u -> u.name = type_name 65 | | Introspection.Interface i -> i.name = type_name 66 | | Introspection.Enum e -> e.name = type_name 67 | | Introspection.Scalar s -> s.name = type_name 68 | | Introspection.InputObject io -> io.name = type_name 69 | 70 | let rec collect_fields ctx type_name fields = 71 | List.map (function 72 | | Graphql_parser.Field field -> 73 | [field] 74 | | Graphql_parser.FragmentSpread spread -> 75 | begin try 76 | let fragment = StringMap.find spread.name ctx.fragments in 77 | if fragment.type_condition = type_name then 78 | collect_fields ctx type_name fragment.selection_set 79 | else 80 | [] 81 | with Not_found -> 82 | failwithf "Invalid fragment `%s`" spread.name 83 | end 84 | | Graphql_parser.InlineFragment fragment -> 85 | match fragment.type_condition with 86 | | None -> 87 | collect_fields ctx type_name fragment.selection_set 88 | | Some condition when condition = type_name -> 89 | collect_fields ctx type_name fragment.selection_set 90 | | _ -> [] 91 | ) fields 92 | |> List.concat 93 | 94 | let alias_or_name : Graphql_parser.field -> string = fun field -> 95 | match field.alias with 96 | | Some alias -> alias 97 | | None -> field.name 98 | 99 | let convert_scalar : string -> Parsetree.expression = 100 | function 101 | | "Int" -> [%expr to_int_option] 102 | | "Boolean" -> [%expr to_bool_option] 103 | | "URI" 104 | | "String" -> [%expr to_string_option] 105 | | "Float" -> [%expr to_float_option] 106 | | "ID" -> [%expr function 107 | | `Null -> None 108 | | `String s -> Some s 109 | | `Int n -> Some (string_of_int n) 110 | | json -> raise (Yojson.Basic.Util.Type_error ("Invalid type for ID", json)) 111 | ] 112 | | typ -> failwithf "Unknown scalar type `%s`" typ 113 | 114 | let convert_enum enum_values = 115 | let default_case = Ast_helper.(Exp.case Pat.(any ()) [%expr failwith "Invalid enum value"]) in 116 | let cases = List.fold_left (fun memo (value : Introspection.enum_value) -> 117 | let pattern = Ast_helper.Pat.constant (Pconst_string (value.name, None)) in 118 | let expr = Ast_helper.Exp.variant value.name None in 119 | (Ast_helper.Exp.case pattern expr)::memo 120 | ) [default_case] enum_values in 121 | Ast_helper.Exp.function_ cases 122 | 123 | let parse_json_method method_name expr = 124 | let val_ = Ast_helper.Cf.(val_ (str method_name) Asttypes.Immutable (concrete Asttypes.Fresh expr)) in 125 | let method_ = Ast_helper.(Cf.method_ (str method_name) Asttypes.Public (Cf.concrete Asttypes.Fresh (Exp.ident (lid method_name)))) in 126 | [val_; method_] 127 | 128 | let rec resolve_type_ref : ctx -> Introspection.typ -> Graphql_parser.field -> Introspection.type_ref -> Parsetree.expression = 129 | fun ctx obj query_field type_ref -> 130 | match type_ref with 131 | | Introspection.Type type_name -> 132 | begin match List.find (match_type_name type_name) ctx.types with 133 | | Introspection.Scalar s -> 134 | let convert_expr = convert_scalar s.name in 135 | [%expr json |> [%e convert_expr]] 136 | | Introspection.Enum e -> 137 | let convert_expr = convert_enum e.enum_values in 138 | [%expr json |> to_option (fun json -> json |> to_string |> [%e convert_expr])] 139 | | Introspection.Object o as obj -> 140 | let fields = collect_fields ctx o.name query_field.selection_set in 141 | let methods = resolve_fields ctx obj fields in 142 | let convert_expr = Ast_helper.(Exp.object_ (Cstr.mk (Pat.any ()) methods)) in 143 | [%expr json |> to_option (fun json -> [%e convert_expr])] 144 | | Introspection.Union u -> 145 | convert_union ctx query_field u.possible_types 146 | | Introspection.Interface _ -> 147 | failwithf "Interface not supported yet" 148 | | Introspection.InputObject _ -> 149 | failwithf "Input object `%s` cannot be used in selection set" type_name 150 | | exception Not_found -> 151 | failwithf "Unknown type `%s`" type_name 152 | end 153 | | Introspection.NonNull t -> 154 | let expr = resolve_type_ref ctx obj query_field t in 155 | [%expr match [%e expr] with None -> failwith "NonNull field was null" | Some v -> v] 156 | | Introspection.List t -> 157 | let expr = resolve_type_ref ctx obj query_field t in 158 | [%expr json |> to_option (convert_each (fun json -> [%e expr]))] 159 | 160 | and resolve_field : ctx -> Introspection.typ -> Graphql_parser.field -> Parsetree.class_field list = 161 | fun ctx typ query_field -> 162 | let field = select_field typ query_field.name in 163 | let alias = alias_or_name query_field in 164 | let parse_expr = resolve_type_ref ctx typ query_field field.typ in 165 | parse_json_method alias [%expr let json = member [%e const_string alias] json in [%e parse_expr]] 166 | 167 | and resolve_fields ctx obj fields : Parsetree.class_field list = 168 | List.map (resolve_field ctx obj) fields 169 | |> List.concat 170 | 171 | and convert_union ctx query_field possible_types = 172 | let branches = List.map (fun type_name -> 173 | let obj = List.find (match_type_name type_name) ctx.types in 174 | let fields = collect_fields ctx type_name query_field.Graphql_parser.selection_set in 175 | let methods = resolve_fields ctx obj fields in 176 | let convert_expr = Ast_helper.(Exp.object_ (Cstr.mk (Pat.any ()) methods)) in 177 | type_name, convert_expr 178 | ) possible_types in 179 | let default_case = Ast_helper.(Exp.case Pat.(any ()) [%expr failwith "Unknown __typename for union"]) in 180 | let cases = List.fold_left (fun memo (type_name, convert_expr) -> 181 | let pattern = Ast_helper.Pat.constant (Pconst_string (type_name, None)) in 182 | let variant_expr = Ast_helper.Exp.variant type_name (Some convert_expr) in 183 | let expr : Parsetree.expression = [%expr json |> to_option (fun json -> [%e variant_expr])] in 184 | (Ast_helper.Exp.case pattern expr)::memo 185 | ) [default_case] branches in 186 | Ast_helper.Exp.match_ [%expr member "__typename" json |> to_string] cases 187 | 188 | let generate_parse_fn : Introspection.schema -> Graphql_parser.document -> Parsetree.expression = 189 | fun schema [Graphql_parser.Operation op] -> 190 | let typ = List.find (match_type_name schema.query_type) schema.types in 191 | let ctx = { fragments = StringMap.empty; types = schema.types } in 192 | let fields = collect_fields ctx schema.query_type op.selection_set in 193 | let methods = resolve_fields ctx typ fields in 194 | [%expr fun json -> 195 | let open Yojson.Basic.Util in 196 | let json = member "data" json in 197 | [%e Ast_helper.(Exp.object_ (Cstr.mk (Pat.any ()) methods))] 198 | ] 199 | 200 | let accept_none : Parsetree.expression -> (Parsetree.expression -> Parsetree.expression) -> Parsetree.expression = 201 | fun value expr_fn -> 202 | [%expr match [%e value] with 203 | | None -> `Null 204 | | Some x -> [%e expr_fn [%expr x]] 205 | ] 206 | 207 | let scalar_to_yojson name value : Parsetree.expression = 208 | match name with 209 | | "Int" -> [%expr `Int [%e value]] 210 | | "Boolean" -> [%expr `Bool [%e value]] 211 | | "ID" 212 | | "String" -> [%expr `String [%e value]] 213 | | "Float" -> [%expr `Float [%e value]] 214 | | typ -> failwithf "Unknown scalar type `%s`" typ 215 | 216 | let enum_to_yojson enum_values value : Parsetree.expression = 217 | let cases = List.map (fun (value : Introspection.enum_value) -> 218 | let pattern = Ast_helper.Pat.variant value.name None in 219 | let expr : Parsetree.expression = [%expr `String [%e const_string value.name]] in 220 | Ast_helper.Exp.case pattern expr 221 | ) enum_values 222 | in 223 | Ast_helper.Exp.match_ value cases 224 | 225 | let rec schema_typ_to_yojson ?(nullable=true) types typ value = 226 | let handle_none = if nullable then accept_none value else (fun expr_fn -> expr_fn value) in 227 | match typ with 228 | | Introspection.Type type_name -> 229 | handle_none begin match List.find (match_type_name type_name) types with 230 | | Introspection.Scalar s -> 231 | scalar_to_yojson s.name 232 | | Introspection.Enum e -> 233 | enum_to_yojson e.enum_values 234 | | Introspection.InputObject o -> 235 | failwith "Input objects are not supported yet" 236 | | Introspection.Object _ 237 | | Introspection.Interface _ 238 | | Introspection.Union _ -> 239 | failwithf "Invalid argument type `%s` (must be scalar, enum or input object)" type_name 240 | | exception Not_found -> 241 | failwithf "Unknown argument type `%s`" type_name 242 | end 243 | | Introspection.List typ' -> 244 | let expr = schema_typ_to_yojson types typ' [%expr x] in 245 | handle_none (fun value' -> [%expr `List (List.map (fun x -> [%e expr]) [%e value'])]) 246 | | Introspection.NonNull typ' -> 247 | schema_typ_to_yojson ~nullable:false types typ' value 248 | 249 | let rec input_typ_to_introspection_typ = function 250 | | Graphql_parser.NamedType type_name -> 251 | Introspection.Type type_name 252 | | Graphql_parser.ListType typ' -> 253 | Introspection.List (input_typ_to_introspection_typ typ') 254 | | Graphql_parser.NonNullType typ' -> 255 | Introspection.NonNull (input_typ_to_introspection_typ typ') 256 | 257 | let generate_variable_fn : Introspection.schema -> Graphql_parser.document -> Parsetree.expression = 258 | fun schema [Graphql_parser.Operation op] -> 259 | let properties = List.fold_right (fun (arg : Graphql_parser.variable_definition) memo -> 260 | let txt = Longident.Lident arg.name in 261 | let var = Ast_helper.Exp.ident {txt; loc} in 262 | let introspection_typ = input_typ_to_introspection_typ arg.typ in 263 | let expr : Parsetree.expression = [%expr [%e schema_typ_to_yojson schema.types introspection_typ var]] in 264 | let prop : Parsetree.expression = [%expr ([%e const_string arg.name], [%e expr])] in 265 | prop::memo 266 | ) op.variable_definitions [] in 267 | let prop_expr_list = exprs_to_list properties in 268 | let fn_with_cont : Parsetree.expression = [%expr fun () -> k ((`Assoc [%e prop_expr_list]) : Yojson.Basic.json)] in 269 | let fn_with_args = List.fold_right (fun (arg : Graphql_parser.variable_definition) memo -> 270 | let label = match arg.typ with 271 | | NonNullType _ -> 272 | Asttypes.Labelled arg.name 273 | | NamedType _ 274 | | ListType _ -> 275 | Asttypes.Optional arg.name 276 | in 277 | Ast_helper.(Exp.fun_ label None (Pat.var {txt=arg.name; loc}) memo) 278 | ) op.variable_definitions fn_with_cont 279 | in [%expr fun k -> [%e fn_with_args]] 280 | 281 | let generate (loc : Location.t) query = 282 | let schema_path = (Location.absolute_path loc.loc_start.pos_fname |> Filename.dirname) ^ "/schema.json" in 283 | let schema = Introspection.of_file schema_path in 284 | match Graphql_parser.parse query with 285 | | Error err -> 286 | let msg = Format.sprintf "Invalid GraphQL query: %s" err in 287 | raise (Location.Error (Location.error ~loc msg)) 288 | | Ok doc -> 289 | try 290 | Ast_helper.with_default_loc loc (fun () -> 291 | let variable_fn = generate_variable_fn schema doc in 292 | let parse_fn = generate_parse_fn schema doc in 293 | query, variable_fn, parse_fn 294 | ) 295 | with Failure msg -> raise (Location.Error (Location.error ~loc msg)) 296 | 297 | let mapper _config _cookies = 298 | let default_mapper = Ast_mapper.default_mapper in 299 | { default_mapper with 300 | expr = fun mapper expr -> 301 | match expr with 302 | | { pexp_desc = Pexp_extension ({ txt = "graphql"; loc}, pstr)} -> 303 | begin match pstr with 304 | | PStr [{ pstr_desc = 305 | Pstr_eval ({ pexp_loc = loc; 306 | pexp_desc = Pexp_constant (Pconst_string (query, _))}, _)}] -> 307 | let query, variable_fn, parse_fn = generate loc query in 308 | Ast_helper.Exp.tuple [const_string query; variable_fn; parse_fn] 309 | | _ -> 310 | raise (Location.Error ( 311 | Location.error ~loc "[%graphql] accepts a string, e.g. [%graphql \"query { id }\"]")) 312 | end 313 | | other -> default_mapper.expr mapper other 314 | } 315 | 316 | let () = 317 | Driver.register ~name:"ppx_graphql" 318 | Versions.ocaml_403 319 | mapper 320 | -------------------------------------------------------------------------------- /test/generate_schema.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var graphql = require('graphql'); 3 | var mockServer = require('graphql-tools').mockServer; 4 | 5 | var schema = graphql.buildSchema(fs.readFileSync('/dev/stdin', 'utf-8')); 6 | mockServer(schema).query(graphql.introspectionQuery).then(response => { 7 | fs.writeSync(process.stdout.fd, JSON.stringify(response)); 8 | }); -------------------------------------------------------------------------------- /test/integration_test.ml: -------------------------------------------------------------------------------- 1 | open Test_helper 2 | 3 | let executable (query, variables, of_json) = 4 | fun ~response -> 5 | variables (fun vars -> 6 | vars, of_json response 7 | ) 8 | 9 | let query_type = 10 | let query_formatter formatter t = Format.pp_print_text formatter (string_of_int t#non_nullable_int) in 11 | let query_eq a b = a#non_nullable_int = b#non_nullable_int in 12 | Alcotest.testable query_formatter query_eq 13 | 14 | let suite : (string * [>`Quick] * (unit -> unit)) list = [ 15 | ("simple query with argument", `Quick, fun () -> 16 | let exec_query = executable [%graphql {| 17 | query MyQuery($int: Int) { 18 | non_nullable_int 19 | } 20 | |}] in 21 | let expected = `Assoc ["int", `Int 1], object method non_nullable_int = 42 end in 22 | let response = `Assoc ["data", `Assoc [ 23 | "non_nullable_int", `Int 42 24 | ]] in 25 | Alcotest.(check (pair yojson query_type)) "response" expected (exec_query ~response ~int:1 ()); 26 | ); 27 | ] -------------------------------------------------------------------------------- /test/jbuild: -------------------------------------------------------------------------------- 1 | (jbuild_version 1) 2 | 3 | (executable 4 | ((libraries (alcotest)) 5 | (preprocess (pps (ppx_graphql))) 6 | (preprocessor_deps ((file schema.json))) 7 | (modules (test test_helper variables_test parsing_test integration_test)) 8 | (name test)) 9 | ) 10 | 11 | (alias 12 | ((name runtest) 13 | (package ppx_graphql) 14 | (deps (test.exe)) 15 | (action (run ${<})))) 16 | -------------------------------------------------------------------------------- /test/parsing_test.ml: -------------------------------------------------------------------------------- 1 | let color_enum = Alcotest.of_pp (fun formatter t -> 2 | let txt = match t with 3 | | `RED -> "RED" 4 | | `GREEN -> "GREEN" 5 | | `BLUE -> "BLUE" 6 | in Format.pp_print_text formatter txt 7 | ) 8 | 9 | let suite : (string * [>`Quick] * (unit -> unit)) list = [ 10 | ("nullable primitives", `Quick, fun () -> 11 | let _, _, of_json = [%graphql {| 12 | query { 13 | nullable_int 14 | nullable_string 15 | nullable_float 16 | nullable_bool 17 | nullable_id 18 | nullable_enum 19 | } 20 | |}] in 21 | let response = `Assoc ["data", `Assoc [ 22 | "nullable_int", `Int 42; 23 | "nullable_string", `String "42"; 24 | "nullable_float", `Float 42.0; 25 | "nullable_bool", `Bool true; 26 | "nullable_id", `String "42"; 27 | "nullable_enum", `String "RED"; 28 | ]] in 29 | let parsed = of_json response in 30 | Alcotest.(check (option int)) "nullable int" (Some 42) parsed#nullable_int; 31 | Alcotest.(check (option string)) "nullable string" (Some "42") parsed#nullable_string; 32 | Alcotest.(check (option (float epsilon_float))) "nullable float" (Some 42.0) parsed#nullable_float; 33 | Alcotest.(check (option bool)) "nullable bool" (Some true) parsed#nullable_bool; 34 | Alcotest.(check (option string)) "nullable ID" (Some "42") parsed#nullable_id; 35 | Alcotest.(check (option color_enum)) "nullable enum" (Some `RED) parsed#nullable_enum; 36 | ); 37 | ("non-nullable primitives", `Quick, fun () -> 38 | let _, _, of_json = [%graphql {| 39 | query { 40 | non_nullable_int 41 | non_nullable_string 42 | non_nullable_float 43 | non_nullable_bool 44 | non_nullable_id 45 | non_nullable_enum 46 | } 47 | |}] in 48 | let response = `Assoc ["data", `Assoc [ 49 | "non_nullable_int", `Int 42; 50 | "non_nullable_string", `String "42"; 51 | "non_nullable_float", `Float 42.0; 52 | "non_nullable_bool", `Bool true; 53 | "non_nullable_id", `String "42"; 54 | "non_nullable_enum", `String "GREEN"; 55 | ]] in 56 | let parsed = of_json response in 57 | Alcotest.(check int) "non_nullable int" 42 parsed#non_nullable_int; 58 | Alcotest.(check string) "non_nullable string" "42" parsed#non_nullable_string; 59 | Alcotest.(check (float epsilon_float)) "non_nullable float" 42.0 parsed#non_nullable_float; 60 | Alcotest.(check bool) "non_nullable bool" true parsed#non_nullable_bool; 61 | Alcotest.(check string) "non_nullable ID" "42" parsed#non_nullable_id; 62 | Alcotest.(check color_enum) "nullable enum" `GREEN parsed#non_nullable_enum; 63 | ); 64 | ("list of primitives", `Quick, fun () -> 65 | let _, _, of_json = [%graphql {| 66 | query { 67 | list_int 68 | list_string 69 | list_float 70 | list_bool 71 | list_id 72 | list_enum 73 | } 74 | |}] in 75 | let response = `Assoc ["data", `Assoc [ 76 | "list_int", `List [`Int 1; `Int 2]; 77 | "list_string", `List [`String "1"; `String "2"]; 78 | "list_float", `List [`Float 1.1; `Float 2.2]; 79 | "list_bool", `List [`Bool false; `Bool true]; 80 | "list_id", `List [`String "1"; `Int 2]; 81 | "list_enum", `List [`String "BLUE"; `String "RED"]; 82 | ]] in 83 | let parsed = of_json response in 84 | Alcotest.(check (option (list (option int)))) "list int" (Some [Some 1; Some 2]) parsed#list_int; 85 | Alcotest.(check (option (list (option string)))) "list string" (Some [Some "1"; Some "2"]) parsed#list_string; 86 | Alcotest.(check (option (list (option (float epsilon_float))))) "list float" (Some [Some 1.1; Some 2.2]) parsed#list_float; 87 | Alcotest.(check (option (list (option bool)))) "list bool" (Some [Some false; Some true]) parsed#list_bool; 88 | Alcotest.(check (option (list (option string)))) "list ID" (Some [Some "1"; Some "2"]) parsed#list_id; 89 | Alcotest.(check (option (list (option color_enum)))) "list enum" (Some [Some `BLUE; Some `RED]) parsed#list_enum; 90 | ); 91 | ("object", `Quick, fun () -> 92 | let _, _, of_json = [%graphql {| 93 | query { 94 | person { 95 | id 96 | age 97 | name 98 | nickname 99 | net_worth 100 | favorite_color 101 | friends { 102 | id 103 | favorite_color 104 | } 105 | } 106 | } 107 | |}] in 108 | let response = `Assoc ["data", `Assoc [ 109 | "person", `Assoc [ 110 | "id", `Int 42; 111 | "age", `Int 30; 112 | "name", `String "John Doe"; 113 | "nickname", `Null; 114 | "net_worth", `Float 1_000.0; 115 | "favorite_color", `String "BLUE"; 116 | "friends", `List [ 117 | `Assoc [ 118 | "id", `String "9"; 119 | "favorite_color", `Null; 120 | ]; 121 | `Null 122 | ] 123 | ] 124 | ]] in 125 | let parsed = of_json response in 126 | Alcotest.(check string) "id" "42" parsed#person#id; 127 | Alcotest.(check int) "age" 30 parsed#person#age; 128 | Alcotest.(check string) "name" "John Doe" parsed#person#name; 129 | Alcotest.(check (option string)) "nickname" None parsed#person#nickname; 130 | Alcotest.(check (option (float epsilon_float))) "net_worth" (Some 1_000.0) parsed#person#net_worth; 131 | Alcotest.(check (option color_enum)) "favorite_color" (Some `BLUE) parsed#person#favorite_color; 132 | let Some friend0 = List.nth parsed#person#friends 0 in 133 | Alcotest.(check string) "friend id" "9" friend0#id; 134 | Alcotest.(check (option color_enum)) "friend color" None friend0#favorite_color; 135 | let friend1 = List.nth parsed#person#friends 1 in 136 | Alcotest.(check (option pass)) "null friend" None friend1; 137 | ); 138 | ("aliasing", `Quick, fun () -> 139 | let _, _, of_json = [%graphql {| 140 | query { 141 | alias: non_nullable_int 142 | } 143 | |}] in 144 | let response = `Assoc ["data", `Assoc [ 145 | "alias", `Int 42; 146 | ]] in 147 | let parsed = of_json response in 148 | Alcotest.(check int) "alias" 42 parsed#alias; 149 | ); 150 | ("union", `Quick, fun () -> 151 | let _, _, of_json = [%graphql {| 152 | query { 153 | search(query: "foo") { 154 | __typename 155 | 156 | ... on Pet { 157 | name 158 | } 159 | 160 | ... on Person { 161 | id 162 | } 163 | } 164 | } 165 | |}] in 166 | let response = `Assoc ["data", `Assoc [ 167 | "search", `List [ 168 | `Assoc [ 169 | "__typename", `String "Pet"; 170 | "name", `String "Fido"; 171 | ]; 172 | `Assoc [ 173 | "__typename", `String "Person"; 174 | "id", `Int 42; 175 | ] 176 | ] 177 | ]] in 178 | let parsed = of_json response in 179 | let [`Pet pet; `Person person] = parsed#search in 180 | Alcotest.(check string) "pet name" "Fido" pet#name; 181 | Alcotest.(check string) "person id" "42" person#id 182 | ); 183 | ] 184 | -------------------------------------------------------------------------------- /test/schema.graphql: -------------------------------------------------------------------------------- 1 | enum COLOR { 2 | RED 3 | GREEN 4 | BLUE 5 | } 6 | 7 | input IntervalInput { 8 | min: Float 9 | max: Float 10 | } 11 | 12 | input PersonFilter { 13 | favorite_color: COLOR 14 | age: IntervalInput 15 | } 16 | 17 | type Person { 18 | id: ID! 19 | age: Int! 20 | name: String! 21 | nickname: String 22 | net_worth: Float 23 | favorite_color: COLOR 24 | friends: [Person]! 25 | } 26 | 27 | type Pet { 28 | name: String! 29 | } 30 | 31 | union SearchResult = Person | Pet 32 | 33 | type Query { 34 | # Nullable primitives 35 | nullable_int: Int 36 | nullable_string: String 37 | nullable_float: Float 38 | nullable_bool: Boolean 39 | nullable_id: ID 40 | nullable_enum: COLOR 41 | 42 | # Non-nullable primitives 43 | non_nullable_int: Int! 44 | non_nullable_string: String! 45 | non_nullable_float: Float! 46 | non_nullable_bool: Boolean! 47 | non_nullable_id: ID! 48 | non_nullable_enum: COLOR! 49 | 50 | # List of primitives 51 | list_int: [Int] 52 | list_string: [String] 53 | list_float: [Float] 54 | list_bool: [Boolean] 55 | list_id: [ID] 56 | list_enum: [COLOR] 57 | 58 | # Objects 59 | person: Person! 60 | 61 | filter_persons(filter: PersonFilter): [Person]! 62 | 63 | search(query: String!): [SearchResult!]! 64 | } 65 | 66 | schema { 67 | query: Query 68 | } 69 | -------------------------------------------------------------------------------- /test/schema.json: -------------------------------------------------------------------------------- 1 | {"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"OBJECT","name":"Query","description":null,"fields":[{"name":"nullable_int","description":null,"args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"nullable_string","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"nullable_float","description":null,"args":[],"type":{"kind":"SCALAR","name":"Float","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"nullable_bool","description":null,"args":[],"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"nullable_id","description":null,"args":[],"type":{"kind":"SCALAR","name":"ID","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"nullable_enum","description":null,"args":[],"type":{"kind":"ENUM","name":"COLOR","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"non_nullable_int","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"non_nullable_string","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"non_nullable_float","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Float","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"non_nullable_bool","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"non_nullable_id","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"non_nullable_enum","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"COLOR","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"list_int","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"list_string","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"list_float","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"Float","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"list_bool","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"list_id","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"list_enum","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"ENUM","name":"COLOR","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"person","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Person","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"filter_persons","description":null,"args":[{"name":"filter","description":null,"type":{"kind":"INPUT_OBJECT","name":"PersonFilter","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"Person","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"search","description":null,"args":[{"name":"query","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"UNION","name":"SearchResult","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","description":"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"String","description":"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","description":"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Boolean","description":"The `Boolean` scalar type represents `true` or `false`.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"ID","description":"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"COLOR","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"RED","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"GREEN","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"BLUE","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"Person","description":null,"fields":[{"name":"id","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"age","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nickname","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"net_worth","description":null,"args":[],"type":{"kind":"SCALAR","name":"Float","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"favorite_color","description":null,"args":[],"type":{"kind":"ENUM","name":"COLOR","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"friends","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"Person","ofType":null}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"PersonFilter","description":null,"fields":null,"inputFields":[{"name":"favorite_color","description":null,"type":{"kind":"ENUM","name":"COLOR","ofType":null},"defaultValue":null},{"name":"age","description":null,"type":{"kind":"INPUT_OBJECT","name":"IntervalInput","ofType":null},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"INPUT_OBJECT","name":"IntervalInput","description":null,"fields":null,"inputFields":[{"name":"min","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null},{"name":"max","description":null,"type":{"kind":"SCALAR","name":"Float","ofType":null},"defaultValue":null}],"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"SearchResult","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Person","ofType":null},{"kind":"OBJECT","name":"Pet","ofType":null}]},{"kind":"OBJECT","name":"Pet","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","description":"A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.","fields":[{"name":"types","description":"A list of all types supported by this server.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","description":"The type that query operations will be rooted at.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","description":"If this server supports mutation, the type that mutation operations will be rooted at.","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","description":"If this server support subscription, the type that subscription operations will be rooted at.","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"directives","description":"A list of all directives supported by this server.","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","description":"The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.","fields":[{"name":"kind","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","description":null,"args":[{"name":"includeDeprecated","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","description":null,"args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","description":null,"args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","description":"An enum describing what kind of type a given `__Type` is.","fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"SCALAR","description":"Indicates this type is a scalar.","isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":"Indicates this type is an object. `fields` and `interfaces` are valid fields.","isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":"Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.","isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":"Indicates this type is a union. `possibleTypes` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":"Indicates this type is an enum. `enumValues` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":"Indicates this type is an input object. `inputFields` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"LIST","description":"Indicates this type is a list. `ofType` is a valid field.","isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","description":"Indicates this type is a non-null. `ofType` is a valid field.","isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__Field","description":"Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","description":"Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","description":"A GraphQL-formatted string representing the default value for this input value.","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","description":"One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","description":"A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.","fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","description":null,"args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"locations","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__DirectiveLocation","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"args","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"onOperation","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use `locations`."},{"name":"onFragment","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use `locations`."},{"name":"onField","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":true,"deprecationReason":"Use `locations`."}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__DirectiveLocation","description":"A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.","fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"QUERY","description":"Location adjacent to a query operation.","isDeprecated":false,"deprecationReason":null},{"name":"MUTATION","description":"Location adjacent to a mutation operation.","isDeprecated":false,"deprecationReason":null},{"name":"SUBSCRIPTION","description":"Location adjacent to a subscription operation.","isDeprecated":false,"deprecationReason":null},{"name":"FIELD","description":"Location adjacent to a field.","isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_DEFINITION","description":"Location adjacent to a fragment definition.","isDeprecated":false,"deprecationReason":null},{"name":"FRAGMENT_SPREAD","description":"Location adjacent to a fragment spread.","isDeprecated":false,"deprecationReason":null},{"name":"INLINE_FRAGMENT","description":"Location adjacent to an inline fragment.","isDeprecated":false,"deprecationReason":null},{"name":"SCHEMA","description":"Location adjacent to a schema definition.","isDeprecated":false,"deprecationReason":null},{"name":"SCALAR","description":"Location adjacent to a scalar definition.","isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","description":"Location adjacent to an object type definition.","isDeprecated":false,"deprecationReason":null},{"name":"FIELD_DEFINITION","description":"Location adjacent to a field definition.","isDeprecated":false,"deprecationReason":null},{"name":"ARGUMENT_DEFINITION","description":"Location adjacent to an argument definition.","isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","description":"Location adjacent to an interface definition.","isDeprecated":false,"deprecationReason":null},{"name":"UNION","description":"Location adjacent to a union definition.","isDeprecated":false,"deprecationReason":null},{"name":"ENUM","description":"Location adjacent to an enum definition.","isDeprecated":false,"deprecationReason":null},{"name":"ENUM_VALUE","description":"Location adjacent to an enum value definition.","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","description":"Location adjacent to an input object type definition.","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_FIELD_DEFINITION","description":"Location adjacent to an input object field definition.","isDeprecated":false,"deprecationReason":null}],"possibleTypes":null}],"directives":[{"name":"skip","description":"Directs the executor to skip this field or fragment when the `if` argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Skipped when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"Directs the executor to include this field or fragment only when the `if` argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Included when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"deprecated","description":"Marks an element of a GraphQL schema as no longer supported.","locations":["FIELD_DEFINITION","ENUM_VALUE"],"args":[{"name":"reason","description":"Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":"\"No longer supported\""}]}]}}} -------------------------------------------------------------------------------- /test/test.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | Alcotest.run "ppx_graphql" [ 3 | "parsing", Parsing_test.suite; 4 | "variables", Variables_test.suite; 5 | "integration", Integration_test.suite; 6 | ] 7 | -------------------------------------------------------------------------------- /test/test_helper.ml: -------------------------------------------------------------------------------- 1 | let id : 'a. 'a -> 'a = fun x -> x 2 | 3 | let yojson = Alcotest.of_pp ( 4 | fun formatter t -> 5 | Format.pp_print_text formatter (Yojson.Basic.pretty_to_string t) 6 | ) -------------------------------------------------------------------------------- /test/variables_test.ml: -------------------------------------------------------------------------------- 1 | open Test_helper 2 | 3 | let suite : (string * [>`Quick] * (unit -> unit)) list = [ 4 | ("nullable", `Quick, fun () -> 5 | let _, mk_variables, _ = [%graphql {| 6 | query Foo($int: Int, $string: String, $bool: Boolean, $float: Float, $id: ID, $enum: COLOR) { 7 | non_nullable_int 8 | } 9 | |}] in 10 | let variables = mk_variables id ~int:1 ~string:"2" ~bool:true ~float:12.3 ~id:"42" ~enum:`RED () in 11 | let expected = `Assoc [ 12 | "int", `Int 1; 13 | "string", `String "2"; 14 | "bool", `Bool true; 15 | "float", `Float 12.3; 16 | "id", `String "42"; 17 | "enum", `String "RED" 18 | ] in 19 | Alcotest.(check yojson) "nullable" expected variables; 20 | ); 21 | ("non-nullable", `Quick, fun () -> 22 | let _, mk_variables, _ = [%graphql {| 23 | query Foo($int: Int!, $string: String!, $bool: Boolean!, $float: Float!, $id: ID!, $enum: COLOR!) { 24 | non_nullable_int 25 | } 26 | |}] in 27 | let variables = mk_variables id ~int:1 ~string:"2" ~bool:true ~float:12.3 ~id:"42" ~enum:`RED () in 28 | let expected = `Assoc [ 29 | "int", `Int 1; 30 | "string", `String "2"; 31 | "bool", `Bool true; 32 | "float", `Float 12.3; 33 | "id", `String "42"; 34 | "enum", `String "RED" 35 | ] in 36 | Alcotest.(check yojson) "non_nullable" expected variables; 37 | ); 38 | ("list", `Quick, fun () -> 39 | let _, mk_variables, _ = [%graphql {| 40 | query Foo($list: [Int]) { 41 | non_nullable_int 42 | } 43 | |}] in 44 | let variables = mk_variables id ~list:[Some 1; None] () in 45 | let expected = `Assoc [ 46 | "list", `List [`Int 1; `Null] 47 | ] in 48 | Alcotest.(check yojson) "list" expected variables; 49 | ); 50 | ] --------------------------------------------------------------------------------