diff --git a/.gitignore b/.gitignore index c693cf61..761c9c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -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.* diff --git a/src/output_bucklescript_decoder.ml b/src/output_bucklescript_decoder.ml index d15b4bbd..cbc0f2d5 100644 --- a/src/output_bucklescript_decoder.ml +++ b/src/output_bucklescript_decoder.ml @@ -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 @@ -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 diff --git a/src/result_decoder.ml b/src/result_decoder.ml index 2cfcf593..60ab9297 100644 --- a/src/result_decoder.ml +++ b/src/result_decoder.ml @@ -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" diff --git a/src/result_structure.ml b/src/result_structure.ml index 3aaeac71..e3b98557 100644 --- a/src/result_structure.ml +++ b/src/result_structure.ml @@ -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 @@ -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 diff --git a/src/schema.ml b/src/schema.ml index a8d654ab..61546dd5 100644 --- a/src/schema.ml +++ b/src/schema.ml @@ -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 diff --git a/tests/__tests__/interface.re b/tests/__tests__/interface.re new file mode 100644 index 00000000..95e9d77f --- /dev/null +++ b/tests/__tests__/interface.re @@ -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"}), + |], + } + ); + }) +); \ No newline at end of file diff --git a/tests/schema.gql b/tests/schema.gql index 9e5daa19..cd3e9ae0 100644 --- a/tests/schema.gql +++ b/tests/schema.gql @@ -1,130 +1,160 @@ schema { - query: Query - mutation: Mutation - subscription: Subscription + query: Query + mutation: Mutation + subscription: Subscription +} + +interface User { + id: ID! +} + +interface Name { + name: String! +} + +interface Anonymous { + anonymousId: Int! +} + +type AdminUser implements User & Name { + id: ID! + name: String! +} + +type AnonymousUser implements User & Anonymous { + id: ID! + anonymousId: Int! +} + +type OtherUser implements User { + id: ID! } type Query { - stringField: String! - variousScalars: VariousScalars! - lists: Lists! + stringField: String! + variousScalars: VariousScalars! + lists: Lists! + users: [User!]! - scalarsInput(arg: VariousScalarsInput!): String! - listsInput(arg: ListsInput!): String! - recursiveInput(arg: RecursiveInput!): String! - nonrecursiveInput(arg: NonrecursiveInput!): String! - enumInput(arg: SampleField!): String! - argNamedQuery(query: Int!): Int! - customScalarField(argOptional: CustomScalar, argRequired: CustomScalar!): CustomScalarObject! + scalarsInput(arg: VariousScalarsInput!): String! + listsInput(arg: ListsInput!): String! + recursiveInput(arg: RecursiveInput!): String! + nonrecursiveInput(arg: NonrecursiveInput!): String! + enumInput(arg: SampleField!): String! + argNamedQuery(query: Int!): Int! + customScalarField( + argOptional: CustomScalar + argRequired: CustomScalar! + ): CustomScalarObject! - dogOrHuman: DogOrHuman! + dogOrHuman: DogOrHuman! - nestedObject: NestedObject! + nestedObject: NestedObject! } type Mutation { - mutationWithError: MutationWithErrorResult! + mutationWithError: MutationWithErrorResult! } type Subscription { - simpleSubscription: DogOrHuman! + simpleSubscription: DogOrHuman! } type MutationWithErrorResult { - value: SampleResult - errors: [SampleError!] + value: SampleResult + errors: [SampleError!] } type SampleResult { - stringField: String! + stringField: String! } type SampleError { - field: SampleField! - message: String! + field: SampleField! + message: String! } enum SampleField { - FIRST - SECOND - THIRD + FIRST + SECOND + THIRD } type VariousScalars { - nullableString: String - string: String! - nullableInt: Int - int: Int! - nullableFloat: Float - float: Float! - nullableBoolean: Boolean - boolean: Boolean! - nullableID: ID - id: ID! + nullableString: String + string: String! + nullableInt: Int + int: Int! + nullableFloat: Float + float: Float! + nullableBoolean: Boolean + boolean: Boolean! + nullableID: ID + id: ID! } type Lists { - nullableOfNullable: [String] - nullableOfNonNullable: [String!] - nonNullableOfNullable: [String]! - nonNullableOfNonNullable: [String!]! + nullableOfNullable: [String] + nullableOfNonNullable: [String!] + nonNullableOfNullable: [String]! + nonNullableOfNonNullable: [String!]! } input VariousScalarsInput { - nullableString: String - string: String! - nullableInt: Int - int: Int! - nullableFloat: Float - float: Float! - nullableBoolean: Boolean - boolean: Boolean! - nullableID: ID - id: ID! + nullableString: String + string: String! + nullableInt: Int + int: Int! + nullableFloat: Float + float: Float! + nullableBoolean: Boolean + boolean: Boolean! + nullableID: ID + id: ID! } input ListsInput { - nullableOfNullable: [String] - nullableOfNonNullable: [String!] - nonNullableOfNullable: [String]! - nonNullableOfNonNullable: [String!]! + nullableOfNullable: [String] + nullableOfNonNullable: [String!] + nonNullableOfNullable: [String]! + nonNullableOfNonNullable: [String!]! } input NonrecursiveInput { - field: String - enum: SampleField + field: String + enum: SampleField } input RecursiveInput { - otherField: String - inner: RecursiveInput - enum: SampleField + otherField: String + inner: RecursiveInput + enum: SampleField } type Dog { - name: String! - barkVolume: Float! + name: String! + barkVolume: Float! } type Human { - name: String! + name: String! } union DogOrHuman = Dog | Human type NestedObject { - inner: NestedObject + inner: NestedObject - field: String! + field: String! } type WithArgField { - argField(arg1: String, arg2: Int): NestedObject + argField(arg1: String, arg2: Int): NestedObject } scalar CustomScalar type CustomScalarObject { - nullable: CustomScalar - nonNullable: CustomScalar! + nullable: CustomScalar + nonNullable: CustomScalar! }