Skip to content

Commit

Permalink
Merge pull request #56 from baransu/master
Browse files Browse the repository at this point in the history
Interface support
  • Loading branch information
mhallin committed Oct 28, 2018
2 parents 14e7119 + d96812d commit af0e558
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 68 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
/node_modules
/tests/lib
/tests/graphql_schema.json
/tests/node_modules
/tests_apollo/lib
/**/.graphql_ppx_cache
/tests_apollo/graphql_schema.json
/tests_apollo/node_modules
/lib
/ppx
/graphql_ppx.*
Expand Down
40 changes: 40 additions & 0 deletions src/output_bucklescript_decoder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ let rec generate_decoder config = function
| Res_object (loc, name, fields) -> generate_object_decoder config loc name fields
| Res_poly_variant_selection_set (loc, name, fields) -> generate_poly_variant_selection_set config loc name fields
| Res_poly_variant_union (loc, name, fragments, exhaustive) -> generate_poly_variant_union config loc name fragments exhaustive
| Res_poly_variant_interface (loc, name, base, fragments) -> generate_poly_variant_interface config loc name base fragments
| Res_solo_fragment_spread (loc, name) -> generate_solo_fragment_spread loc name
| Res_error (loc, message) -> generate_error loc message

Expand Down Expand Up @@ -240,6 +241,45 @@ and generate_poly_variant_selection_set config loc name fields =
| None -> [%e make_error_raiser config [%expr "Expected type " ^ [%e const_str_expr name] ^ " to be an object"]]
| Some value -> ([%e generator_loop fields]: [%t variant_type])] [@metaloc loc]

and generate_poly_variant_interface config loc name base fragments =
let map_fallback_case (type_name, inner) = Ast_helper.(
let name_pattern = Pat.any () in
let variant = Exp.variant type_name (Some (generate_decoder config inner)) in
Exp.case name_pattern variant
) in

let map_case (type_name, inner) = Ast_helper.(
let name_pattern = Pat.constant (Const_string (type_name, None)) in
let variant = Exp.variant type_name (Some (generate_decoder config inner)) in
Exp.case name_pattern variant
) in
let map_case_ty (name, _) =
Rtag (name, [], false, [{ ptyp_desc = Ptyp_any; ptyp_attributes = []; ptyp_loc = Location.none }])
in

let fragment_cases = List.map map_case fragments in
let fallback_case = map_fallback_case base in
let fallback_case_ty = map_case_ty base in

let fragment_case_tys = List.map map_case_ty fragments in
let interface_ty = Ast_helper.(Typ.variant (fallback_case_ty :: fragment_case_tys) Closed None) in
let typename_matcher = Ast_helper.(Exp.match_
[%expr typename]
(List.concat [ fragment_cases; [ fallback_case ]])) in
[%expr
match Js.Json.decodeObject value with
| None -> [%e make_error_raiser config
[%expr "Expected Interface implementation " ^ [%e const_str_expr name] ^ " to be an object, got " ^ (Js.Json.stringify value)]]
| Some typename_obj -> match Js.Dict.get typename_obj "__typename" with
| None -> [%e make_error_raiser config [%expr
"Interface implementation" ^ [%e const_str_expr name] ^
" is missing the __typename field"]]
| Some typename -> match Js.Json.decodeString typename with
| None -> [%e make_error_raiser config [%expr
"Interface implementation " ^ [%e const_str_expr name] ^
" has a __typename field that is not a string"]]
| Some typename -> ([%e typename_matcher]: [%t interface_ty])] [@metaloc loc]

and generate_poly_variant_union config loc name fragments exhaustive_flag =
let fragment_cases = Ast_helper.(
fragments
Expand Down
35 changes: 33 additions & 2 deletions src/result_decoder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,42 @@ let rec unify_type error_marker as_record config span ty (selection_set: selecti
| Some ((Object o) as ty) ->
unify_selection_set error_marker as_record config span ty selection_set
| Some Enum enum_meta -> Res_poly_enum (config.map_loc span, enum_meta)
| Some ((Interface o) as ty) ->
unify_selection_set error_marker as_record config span ty selection_set
| Some ((Interface im) as ty) ->
unify_interface error_marker as_record config span im ty selection_set
| Some InputObject obj -> make_error error_marker config.map_loc span "Can't have fields on input objects"
| Some Union um -> unify_union error_marker config span um selection_set

and unify_interface error_marker as_record config span interface_meta ty selection_set =
match selection_set with
| None -> make_error error_marker config.map_loc span "Interface types must have subselections"
| Some selection_set ->
let unwrap_type_conds (selections, fragments) selection =
match selection with
| InlineFragment { item = { if_type_condition = None }; span } ->
raise_error config.map_loc span "Inline fragments must have a type condition"
| InlineFragment frag -> (selections, frag :: fragments)
| selection -> (selection :: selections, fragments)
in
let (base_selection_set, fragments) =
List.fold_left unwrap_type_conds ([], []) selection_set.item
in
let generate_case selection ty name = (
name,
Res_object (config.map_loc span, name, List.map (unify_selection error_marker config ty) selection)
) in
let generate_fragment_case { item = { if_type_condition = Some if_type_condition; if_selection_set } ; span } =
let { item } = if_selection_set in
let selection = List.append base_selection_set item in
let ty = match (lookup_type config.schema if_type_condition.item) with
| Some ty -> ty
| None -> ty
in
generate_case selection ty if_type_condition.item
in
let fragment_cases = (List.map generate_fragment_case fragments) in
let base_case = (generate_case base_selection_set ty interface_meta.im_name) in
Res_poly_variant_interface (config.map_loc span, interface_meta.im_name, base_case, fragment_cases)

and unify_union error_marker config span union_meta selection_set =
match selection_set with
| None -> make_error error_marker config.map_loc span "Union types must have subselections"
Expand Down
2 changes: 2 additions & 0 deletions src/result_structure.ml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and t =
| Res_object of loc * string * field_result list
| Res_poly_variant_selection_set of loc * string * (string * t) list
| Res_poly_variant_union of loc * string * (string * t) list * exhaustive_flag
| Res_poly_variant_interface of loc * string * (string * t) * (string * t) list
| Res_solo_fragment_spread of loc * string
| Res_error of loc * string

Expand All @@ -38,6 +39,7 @@ let res_loc = function
| Res_object (loc, _, _)
| Res_poly_variant_selection_set (loc, _, _)
| Res_poly_variant_union (loc, _, _, _)
| Res_poly_variant_interface (loc, _,_, _)
| Res_solo_fragment_spread (loc, _)
| Res_error (loc, _)
-> loc
Expand Down
7 changes: 7 additions & 0 deletions src/schema.ml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ exception Invalid_type of string
exception Inconsistent_schema of string


let lookup_implementations schema im =
let all_objects_implementing_interface _ value acc = match value with
| Object { om_interfaces } as o when List.exists (fun n -> n = im.im_name) om_interfaces -> o :: acc
| _ -> acc
in
Hashtbl.fold all_objects_implementing_interface schema.type_map []

let lookup_field ty name =
let find_field fs =
match List.find_all (fun f -> f.fm_name = name) fs with
Expand Down
61 changes: 61 additions & 0 deletions tests/__tests__/interface.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module QueryWithFragments = [%graphql
{|
query {
users {
id
... on AdminUser {
name
}
... on AnonymousUser {
anonymousId
}
}
}
|}
];

module QueryWithoutFragments = [%graphql
{|
query {
users {
id
}
}
|}
];

let json = {|{
"users": [
{ "__typename": "AdminUser", "id": "1", "name": "bob" },
{ "__typename": "AnonymousUser", "id": "2", "anonymousId": 1},
{ "__typename": "OtherUser", "id": "3"}
]}|};

Jest.(
describe("Interface definition", () => {
open Expect;
open! Expect.Operators;

test("Decodes the interface with fragments ", () =>
expect(QueryWithFragments.parse(Js.Json.parseExn(json)))
== {
"users": [|
`AdminUser({"id": "1", "name": "bob"}),
`AnonymousUser({"id": "2", "anonymousId": 1}),
`User({"id": "3"}),
|],
}
);

test("Decodes the interface without fragments ", () =>
expect(QueryWithoutFragments.parse(Js.Json.parseExn(json)))
== {
"users": [|
`User({"id": "1"}),
`User({"id": "2"}),
`User({"id": "3"}),
|],
}
);
})
);
Loading

0 comments on commit af0e558

Please sign in to comment.