Skip to content

Commit

Permalink
Merge pull request #28 from andreas/recursive-objects
Browse files Browse the repository at this point in the history
Allow recursive object types
  • Loading branch information
andreas authored Feb 12, 2017
2 parents b26952e + 257cf79 commit 8d4d8ab
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 31 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ let role = Schema.enum
let user = Schema.(obj
~name:"user"
~fields:[
~fields:(fun _ -> [
field "id"
~typ:(non_null int)
~args:Arg.[]
Expand All @@ -73,7 +73,7 @@ let user = Schema.(obj
~typ:(non_null role)
~args:Arg.[]
~resolve:(fun () p -> p.role)
]
])
)
let schema = Schema.(schema
Expand Down Expand Up @@ -104,6 +104,32 @@ let variables = (json_variables :> (string * Graphql_parser.const_value) list)
Graphql.Schema.execute schema ctx ~variables query
```

### Recursive Objects

To allow defining an object that refers to itself, the type itself is provided as argument to the `~fields` function. Example:

```ocaml
type tweet = {
id : int;
replies : tweet list;
}
let tweet = Schema.(obj
~name:"tweet"
~fields:(fun tweet -> [
field "id"
~typ:(non_null int)
~args:Arg.[]
~resolver:(fun ctx t -> t.id)
;
field "replies"
~typ:(non_null (list tweet))
~args:Arg.[]
~resolver:(fun ctx t -> t.replies)
])
)
```

### Lwt Support

```ocaml
Expand Down Expand Up @@ -155,7 +181,7 @@ The following types ensure this:
```ocaml
type ('ctx, 'src) obj = {
name : string;
fields : ('ctx, 'src) field list;
fields : ('ctx, 'src) field list Lazy.t;
}
and ('ctx, 'src) field =
Field : {
Expand Down
2 changes: 1 addition & 1 deletion src/graphql_intf.ml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module type Schema = sig
'ctx schema

val obj : name:string ->
fields:('ctx, 'src) field list ->
fields:(('ctx, 'src option) typ -> ('ctx, 'src) field list) ->
('ctx, 'src option) typ

module Arg : sig
Expand Down
70 changes: 45 additions & 25 deletions src/graphql_schema.ml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ module Make(Io : IO) = struct
let find k t = try Some (find_exn k t) with Missing_key _ -> None
end

module StringSet = Set.Make(String)

type variable_map = Graphql_parser.const_value StringMap.t

module Arg = struct
Expand Down Expand Up @@ -240,7 +242,7 @@ module Make(Io : IO) = struct

type ('ctx, 'src) obj = {
name : string;
fields : ('ctx, 'src) field list;
fields : ('ctx, 'src) field list Lazy.t;
}
and (_, _) field =
Field : {
Expand All @@ -264,15 +266,16 @@ module Make(Io : IO) = struct
let schema ~fields = {
query = {
name = "root";
fields;
fields = lazy fields;
}
}

let id : 'a. 'a -> 'a = fun x -> x

(* Constructor functions *)
let obj ~name ~fields =
Object { name; fields }
let rec o = Object { name; fields = lazy (fields o)} in
o

let field name ~typ ~args ~resolve =
Field { lift = Io.return; name; typ; args; resolve }
Expand Down Expand Up @@ -328,19 +331,35 @@ module Introspection = struct
| AnyArgField : (_, _) Arg.arg -> any_field
type any_arg = AnyArg : (_, _) Arg.arg -> any_arg

let unless_visited (result, visited) name f =
if StringSet.mem name visited then
result, visited
else
f (result, visited)

(* Extracts all types contained in a single type *)
let rec types : type src. any_typ list -> ('ctx, src) typ -> any_typ list = fun memo typ -> match typ with
| List typ -> types memo typ
| NonNullable typ -> types memo typ
| Scalar _ as scalar -> (AnyTyp scalar)::memo
| Enum _ as enum -> (AnyTyp enum)::memo
let rec types : type src. ?memo:(any_typ list * StringSet.t) -> ('ctx, src) typ -> (any_typ list * StringSet.t) = fun ?(memo=([], StringSet.empty)) typ ->
match typ with
| List typ -> types ~memo typ
| NonNullable typ -> types ~memo typ
| Scalar s as scalar ->
unless_visited memo s.name (fun (result, visited) ->
(AnyTyp scalar)::result, StringSet.add s.name visited
)
| Enum e as enum ->
unless_visited memo e.name (fun (result, visited) ->
(AnyTyp enum)::result, StringSet.add e.name visited
)
| Object o as obj ->
let memo' = (AnyTyp obj)::memo in
let reducer = fun memo (Field f) ->
let memo' = types memo f.typ in
arg_list_types memo' f.args
in
List.fold_left reducer memo' o.fields
unless_visited memo o.name (fun (result, visited) ->
let result' = (AnyTyp obj)::result in
let visited' = StringSet.add o.name visited in
let reducer = fun memo (Field f) ->
let result', visited' = types ~memo f.typ in
arg_list_types result' f.args, visited'
in
List.fold_left reducer (result', visited') (Lazy.force o.fields)
)
and arg_types : type a b. any_typ list -> (a, b) Arg.arg_typ -> any_typ list = fun memo argtyp ->
match argtyp with
| Arg.Scalar _ as scalar -> (AnyArgTyp scalar)::memo
Expand Down Expand Up @@ -383,7 +402,7 @@ module Introspection = struct

let __enum_value = Object {
name = "__EnumValue";
fields = [
fields = lazy [
Field {
name = "name";
typ = NonNullable string;
Expand Down Expand Up @@ -417,7 +436,7 @@ module Introspection = struct

let rec __input_value : 'ctx. ('ctx, any_arg option) typ = Object {
name = "__InputValue";
fields = [
fields = lazy [
Field {
name = "name";
typ = NonNullable string;
Expand Down Expand Up @@ -451,7 +470,7 @@ module Introspection = struct

and __type : 'ctx. ('ctx, any_typ option) typ = Object {
name = "__Type";
fields = [
fields = lazy [
Field {
name = "kind";
typ = NonNullable __type_kind;
Expand Down Expand Up @@ -497,7 +516,7 @@ module Introspection = struct
lift = Io.return;
resolve = fun _ t -> match t with
| AnyTyp (Object o) ->
Some (List.map (fun f -> AnyField f) o.fields)
Some (List.map (fun f -> AnyField f) (Lazy.force o.fields))
| AnyArgTyp (Arg.Object o) ->
let arg_list = args_to_list o.fields in
Some (List.map (fun (AnyArg f) -> AnyArgField f) arg_list)
Expand Down Expand Up @@ -556,7 +575,7 @@ module Introspection = struct

and __field : 'ctx. ('ctx, any_field option) typ = Object {
name = "__Field";
fields = [
fields = lazy [
Field {
name = "name";
typ = NonNullable string;
Expand Down Expand Up @@ -610,7 +629,7 @@ module Introspection = struct

let __directive = Object {
name = "__Directive";
fields = [
fields = lazy [
Field {
name = "name";
typ = NonNullable string;
Expand All @@ -623,20 +642,20 @@ module Introspection = struct

let __schema : 'ctx. ('ctx, 'ctx schema option) typ = Object {
name = "__Schema";
fields = [
fields = lazy [
Field {
name = "types";
typ = NonNullable (List (NonNullable __type));
args = Arg.[];
lift = Io.return;
resolve = fun _ s -> types [] (Object s.query);
resolve = fun _ s -> fst @@ types (Object s.query)
};
Field {
name = "queryType";
typ = NonNullable __type;
args = Arg.[];
lift = Io.return;
resolve = fun _ s -> AnyTyp (Object s.query);
resolve = fun _ s -> AnyTyp (Object s.query)
};
Field {
name = "mutationType";
Expand All @@ -663,7 +682,8 @@ module Introspection = struct
lift = Io.return;
resolve = fun _ _ -> s
} in
{ query = { s.query with fields = schema_field::s.query.fields } }
let fields = lazy (schema_field::(Lazy.force s.query.fields)) in
{ query = { s.query with fields } }
end

(* Execution *)
Expand Down Expand Up @@ -703,7 +723,7 @@ end
| None -> field.name

let field_from_object : ('ctx, 'src) obj -> string -> ('ctx, 'src) field option = fun obj field_name ->
List.find (fun (Field field) -> field.name = field_name) obj.fields
List.find (fun (Field field) -> field.name = field_name) (Lazy.force obj.fields)

let coerce_or_null : 'a option -> ('a -> (Yojson.Basic.json, string) result Io.t) -> (Yojson.Basic.json, string) result Io.t = fun src f ->
match src with
Expand Down
4 changes: 2 additions & 2 deletions test/test_schema.ml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ let role = Schema.enum

let user = Schema.(obj
~name:"user"
~fields:[
~fields:(fun _ -> [
field "id"
~typ:(non_null int)
~args:Arg.[]
Expand All @@ -34,7 +34,7 @@ let user = Schema.(obj
~typ:(non_null role)
~args:Arg.[]
~resolve:(fun () p -> p.role)
]
])
)

let schema = Schema.(schema
Expand Down

0 comments on commit 8d4d8ab

Please sign in to comment.