Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursive fields: add Arg.fix and Schema.fix #199

Merged
merged 5 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,40 +136,42 @@ type tweet = {
replies : tweet list;
}

let tweet = Schema.(obj "tweet"
~fields:(fun tweet -> [
field "id"
~typ:(non_null int)
~args:Arg.[]
~resolve:(fun info t -> t.id)
;
field "replies"
~typ:(non_null (list tweet))
~args:Arg.[]
~resolve:(fun info t -> t.replies)
])
)
let tweet = Schema.(fix (fun recursive ->
recursive.obj "tweet"
~fields:(fun tweet -> [
field "id"
~typ:(non_null int)
~args:Arg.[]
~resolve:(fun info t -> t.id)
;
field "replies"
~typ:(non_null (list (non_null tweet)))
~args:Arg.[]
~resolve:(fun info t -> t.replies)
])))
```

### Mutually Recursive Objects

Mutually recursive objects can be defined using `let rec` and `lazy`:

```ocaml
let rec foo = lazy Schema.(obj "foo"
~fields:(fun _ -> [
field "bar"
~typ:Lazy.(force bar)
~args.Arg.[]
~resolve:(fun info foo -> foo.bar)
])
and bar = lazy Schema.(obj "bar"
~fields:(fun _ -> [
let foo, bar = Schema.(fix (fun recursive ->
let foo = recursive.obj "foo" ~fields:(fun (_, bar) -> [
field "bar"
~typ:bar
~args:Arg.[]
~resolve:(fun info foo -> foo.bar)
])
in
let bar = recursive.obj "bar" ~fields:(fun (foo, _) -> [
field "foo"
~typ:Lazy.(force foo)
~args.Arg.[]
~typ:foo
~args:Arg.[]
~resolve:(fun info bar -> bar.foo)
])
])
in
foo, bar))
```

### Lwt Support
Expand Down
4 changes: 3 additions & 1 deletion dune-project
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
(lang dune 1.1)
(lang dune 1.11)

(using menhir 2.0)

(using fmt 1.2)
25 changes: 24 additions & 1 deletion graphql/src/graphql_intf.ml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module type Schema = sig
val obj :
?doc:string ->
string ->
fields:(('ctx, 'src option) typ -> ('ctx, 'src) field list) ->
fields:('ctx, 'src) field list ->
('ctx, 'src option) typ

module Arg : sig
Expand All @@ -103,6 +103,17 @@ module type Schema = sig
default:Graphql_parser.const_value ->
'a arg

type 'a fixpoint = {
obj: 'src 't 'args.
?doc:string
-> string
-> fields:('a -> ('t, 'args) arg_list)
-> coerce:'args
-> 't option arg_typ
}

val fix : ('a fixpoint -> 'a) -> 'a

val scalar :
?doc:string ->
string ->
Expand Down Expand Up @@ -214,6 +225,18 @@ module type Schema = sig
'src ->
('ctx, 'a) abstract_value

type 'a fixpoint = {
obj: 'ctx 'src 'typ 'b. ?doc:string -> string ->
fields:('a -> ('ctx, 'src) field list) ->
('ctx, 'src option) typ;

interface : 'ctx 'src. ?doc:string -> string ->
fields:('a -> abstract_field list) ->
('ctx, 'src) abstract_typ
}

val fix : ('a fixpoint -> 'a) -> 'a

(** {3 Built-in scalars} *)

val int : ('ctx, int option) typ
Expand Down
80 changes: 60 additions & 20 deletions graphql/src/graphql_schema.ml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ module Make (Io : IO) (Field_error : Field_error) = struct
| Object : {
name : string;
doc : string option;
fields : ('a, 'b) arg_list;
fields : ('a, 'b) arg_list Lazy.t;
coerce : 'b;
}
-> 'a option arg_typ
Expand Down Expand Up @@ -175,6 +175,26 @@ module Make (Io : IO) (Field_error : Field_error) = struct
| [] : ('a, 'a) arg_list
| ( :: ) : 'a arg * ('b, 'c) arg_list -> ('b, 'a -> 'c) arg_list

type 'a fixpoint = {
obj: 'src 't 'args.
?doc:string
-> string
-> fields:('a -> ('t, 'args) arg_list)
-> coerce:'args
-> 't option arg_typ
}

let obj ?doc name ~fields ~coerce =
Object { name; doc; fields; coerce }

let fix : ('a fixpoint -> 'a) -> 'a = fun f ->
let rec recursive = {
obj = fun ?doc name ~fields ->
obj ?doc name ~fields:(lazy (fields (Lazy.force r)))
}
and r = lazy (f recursive)
in Lazy.force r

let rec string_of_const_value : Graphql_parser.const_value -> string =
function
| `Null -> Yojson.Basic.to_string `Null
Expand Down Expand Up @@ -350,7 +370,7 @@ module Make (Io : IO) (Field_error : Field_error) = struct
match value with
| `Assoc props ->
let props' = (props :> (string * Graphql_parser.value) list) in
eval_arglist variable_map ?field_type ~field_name o.fields props'
eval_arglist variable_map ?field_type ~field_name (Lazy.force o.fields) props'
o.coerce
>>| fun coerced -> Some coerced
| _ ->
Expand Down Expand Up @@ -421,8 +441,8 @@ module Make (Io : IO) (Field_error : Field_error) = struct

let enum ?doc name ~values = Enum { name; doc; values }

let obj ?doc name ~fields ~coerce = Object { name; doc; fields; coerce }

let obj ?doc name ~fields ~coerce =
obj ?doc name ~fields:(lazy fields) ~coerce
end

(* Schema data types *)
Expand Down Expand Up @@ -528,6 +548,38 @@ module Make (Io : IO) (Field_error : Field_error) = struct
}
-> directive

type 'a fixpoint = {
obj: 'ctx 'src 'typ 'b. ?doc:string -> string ->
fields:('a -> ('ctx, 'src) field list) ->
('ctx, 'src option) typ;

interface : 'ctx 'src. ?doc:string -> string ->
fields:('a -> abstract_field list) ->
('ctx, 'src) abstract_typ
}

let obj ?doc name ~fields =
Object { name; doc; fields; abstracts = ref [] }

let union ?doc name = Abstract { name; doc; types = []; kind = `Union }

let interface ?doc name ~fields =
let rec i =
Abstract { name; doc; types = []; kind = `Interface (lazy (fields i)) }
in
i

let fix f =
let rec recursive = {
obj = (fun ?doc name ~fields ->
obj ?doc name ~fields:( lazy (fields (Lazy.force r))));

interface = fun ?doc name ~fields ->
Abstract { name; doc; types = []; kind = `Interface (lazy (fields (Lazy.force r))) }
}
and r = lazy (f recursive)
in Lazy.force r

type 'ctx schema = {
query : ('ctx, unit) obj;
mutation : ('ctx, unit) obj option;
Expand Down Expand Up @@ -559,11 +611,7 @@ module Make (Io : IO) (Field_error : Field_error) = struct
}

(* Constructor functions *)
let obj ?doc name ~fields =
let rec o =
Object { name; doc; fields = lazy (fields o); abstracts = ref [] }
in
o
let obj ?doc name ~fields = obj ?doc name ~fields:(lazy fields)

let field ?doc ?(deprecated = NotDeprecated) name ~typ ~args ~resolve =
Field { name; doc; deprecated; typ; args; resolve; lift = Io.ok }
Expand Down Expand Up @@ -596,14 +644,6 @@ module Make (Io : IO) (Field_error : Field_error) = struct

let non_null typ = NonNullable typ

let union ?doc name = Abstract { name; doc; types = []; kind = `Union }

let interface ?doc name ~fields =
let rec i =
Abstract { name; doc; types = []; kind = `Interface (lazy (fields i)) }
in
i

let add_type abstract_typ typ =
match (abstract_typ, typ) with
| Abstract a, Object o ->
Expand Down Expand Up @@ -745,7 +785,7 @@ module Make (Io : IO) (Field_error : Field_error) = struct
let memo' =
(AnyArgTyp obj :: result, StringSet.add o.name visited)
in
arg_list_types memo' o.fields)
arg_list_types memo' (Lazy.force o.fields))

and arg_list_types :
type a b.
Expand Down Expand Up @@ -1071,7 +1111,7 @@ module Make (Io : IO) (Field_error : Field_error) = struct
(fun (AbstractField f) -> AnyField f)
(Lazy.force fields))
| AnyArgTyp (Arg.Object o) ->
let arg_list = args_to_list o.fields in
let arg_list = args_to_list (Lazy.force o.fields) in
Some
(List.map
(fun (AnyArg f) -> AnyArgField f)
Expand Down Expand Up @@ -1147,7 +1187,7 @@ module Make (Io : IO) (Field_error : Field_error) = struct
(fun _ t ->
match t with
| AnyArgTyp (Arg.Object o) ->
Some (args_to_list o.fields)
Some (args_to_list (Lazy.force o.fields))
| _ -> None);
};
Field
Expand Down
36 changes: 17 additions & 19 deletions graphql/test/abstract_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,26 @@ let meow : cat = { name = "Meow"; kittens = 1 }
let fido : dog = { name = "Fido"; puppies = 2 }

let cat =
Schema.(
obj "Cat" ~fields:(fun _ ->
[
field "name" ~typ:(non_null string)
~args:Arg.[]
~resolve:(fun _ (cat : cat) -> cat.name);
field "kittens" ~typ:(non_null int)
~args:Arg.[]
~resolve:(fun _ (cat : cat) -> cat.kittens);
]))
Schema.(fix (fun fixpoint ->
fixpoint.obj "Cat" ~fields:(fun _self -> [
field "name" ~typ:(non_null string)
~args:Arg.[]
~resolve:(fun _ (cat : cat) -> cat.name);
field "kittens" ~typ:(non_null int)
~args:Arg.[]
~resolve:(fun _ (cat : cat) -> cat.kittens);
])))

let dog =
Schema.(
obj "Dog" ~fields:(fun _ ->
[
field "name" ~typ:(non_null string)
~args:Arg.[]
~resolve:(fun _ (dog : dog) -> dog.name);
field "puppies" ~typ:(non_null int)
~args:Arg.[]
~resolve:(fun _ (dog : dog) -> dog.puppies);
]))
obj "Dog" ~fields:[
field "name" ~typ:(non_null string)
~args:Arg.[]
~resolve:(fun _ (dog : dog) -> dog.name);
field "puppies" ~typ:(non_null int)
~args:Arg.[]
~resolve:(fun _ (dog : dog) -> dog.puppies);
])

let pet : (unit, [ `pet ]) Schema.abstract_typ = Schema.union "Pet"

Expand Down
3 changes: 1 addition & 2 deletions graphql/test/echo_schema.ml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ let color_enum_arg = Schema.Arg.enum "color" ~values:color_enum_values
let person_arg =
Schema.Arg.(
obj "person"
~fields:
[
~fields:[
arg "title" ~typ:string;
arg "first_name" ~typ:(non_null string);
arg "last_name" ~typ:(non_null string);
Expand Down
24 changes: 11 additions & 13 deletions graphql/test/error_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,11 @@ let suite =
fun () ->
let obj_with_non_nullable_field =
Schema.(
obj "obj" ~fields:(fun _ ->
[
io_field "non_nullable" ~typ:(non_null int)
~args:Arg.[]
~resolve:(fun _ () -> Error "boom");
]))
obj "obj" ~fields: [
io_field "non_nullable" ~typ:(non_null int)
~args:Arg.[]
~resolve:(fun _ () -> Error "boom");
])
in
let schema =
Schema.(
Expand Down Expand Up @@ -100,13 +99,12 @@ let suite =
fun () ->
let foo =
Schema.(
obj "Foo" ~fields:(fun _ ->
[
io_field "id" ~typ:int
~args:Arg.[]
~resolve:(fun _ (id, should_fail) ->
if should_fail then Error "boom" else Ok (Some id));
]))
obj "Foo" ~fields:[
io_field "id" ~typ:int
~args:Arg.[]
~resolve:(fun _ (id, should_fail) ->
if should_fail then Error "boom" else Ok (Some id));
])
in
let schema =
Schema.(
Expand Down
12 changes: 12 additions & 0 deletions graphql/test/schema_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ let suite =
match Graphql.Schema.execute Test_schema.schema () doc with
| Ok _ -> ()
| Error err -> failwith (Yojson.Basic.pretty_to_string err) ) );

( "recursive schema",
`Quick,
fun () ->
let query = "query a { tree { children { element } } }" in
test_query query
(`Assoc
[
( "data",
`Assoc [ ("tree", `Assoc [ "children", `List [
`Assoc ["element", `Int 1] ] ]) ]); ]));

( "subscription",
`Quick,
fun () ->
Expand Down
3 changes: 1 addition & 2 deletions graphql/test/test_common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ let yojson =
( module struct
type t = Yojson.Basic.json

let pp formatter t =
Format.pp_print_text formatter (Yojson.Basic.pretty_to_string t)
let pp = Yojson.Basic.pretty_print ?std:None
Copy link
Contributor Author

@anmonteiro anmonteiro Feb 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by change for better diffs when testing


let equal = ( = )
end : Alcotest.TESTABLE
Expand Down
Loading