diff --git a/Tests/ApolloTests/SelectionSetTests.swift b/Tests/ApolloTests/SelectionSetTests.swift index 058771472..c9e988c4b 100644 --- a/Tests/ApolloTests/SelectionSetTests.swift +++ b/Tests/ApolloTests/SelectionSetTests.swift @@ -1715,4 +1715,281 @@ class SelectionSetTests: XCTestCase { expect(condition).to(equal(expected)) } + // MARK Selection dict intializer + func test__selectionDictInitializer_givenNonOptionalEntityField_givenValue__setsFieldDataCorrectly() { + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String?.self) + ]} + + var name: String? { __data["name"] } + } + + let object: [String: Any] = [ + "__typename": "Human", + "name": "Johnny Tsunami" + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.name).to(equal("Johnny Tsunami")) + } + + func test__selectionDictInitializer_givenOptionalEntityField_givenNilValue__returnsNil() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("friend", Hero?.self) + ]} + + var friend: Hero? { __data["friend"] } + } + + let object: [String: Any] = [ + "__typename": "Human" + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.friend).to(beNil()) + } + + func test__selectionDictInitializer_giveDictionaryEntityFiled_givenNonOptionalValue__setsFieldDataCorrectly() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("friend", Friend.self) + ]} + + var friend: Friend { __data["friend"] } + + class Friend: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + ]} + } + } + + let object: [String: Any] = [ + "__typename": "Human", + "friend": ["__typename": "Human"] + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.friend.__typename).to(equal("Human")) + } + + func test__selectionDictInitializer_giveOptionalDictionaryEntityFiled_givenNilValue__returnsNil() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("friend", Friend?.self) + ]} + + var friend: Friend? { __data["friend"] } + + class Friend: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + ]} + } + } + + let object: [String: Any] = [ + "__typename": "Human", + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.friend).to(beNil()) + } + + func test__selectionDictInitializer_giveDictionaryArrayEntityField_givenNonOptionalValue__setsFieldDataCorrectly() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("friends", [Friend].self) + ]} + + var friends: [Friend] { __data["friends"] } + + class Friend: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + ]} + } + } + + let object: [String: Any] = [ + "__typename": "Human", + "friends": [ + ["__typename": "Human"], + ["__typename": "Human"], + ["__typename": "Human"] + ] + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.friends.count).to(equal(3)) + } + + func test__selectionDictInitializer_giveOptionalDictionaryArrayEntityField_givenNilValue__returnsNil() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("friends", [Friend]?.self) + ]} + + var friends: [Friend]? { __data["friends"] } + + class Friend: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + ]} + } + } + + let object: [String: Any] = [ + "__typename": "Human" + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.friends).to(beNil()) + } + + func test__selectionDictInitializer_giveDictionaryArrayEntityField_givenEmptyValue__returnsEmpty() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("friends", [Friend].self) + ]} + + var friends: [Friend] { __data["friends"] } + + class Friend: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + ]} + } + } + + let object: [String: Any] = [ + "__typename": "Human", + "friends": [] + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.friends).to(beEmpty()) + } + + func test__selectionDictInitializer_giveNestedListEntityField_givenNonOptionalValue__setsFieldDataCorrectly() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("nestedList", [[Hero]].self) + ]} + + var nestedList: [[Hero]] { __data["nestedList"] } + } + + let object: [String: Any] = [ + "__typename": "Human", + "nestedList": [[ + [ + "__typename": "Human", + "nestedList": [[]] + ] + ]] + ] + + let expected = try! Hero( + data: [ + "__typename": "Human", + "nestedList": [[]] + ], + variables: nil + ) + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.nestedList).to(equal([[expected]])) + } + + func test__selectionDictInitializer_giveOptionalNestedListEntityField_givenNilValue__returnsNil() { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("nestedList", [[Hero]]?.self) + ]} + + var nestedList: [[Hero]]? { __data["nestedList"] } + } + + let object: [String: Any] = [ + "__typename": "Human", + ] + + // when + let actual = try! Hero(data: object) + + // then + expect(actual.nestedList).to(beNil()) + } } diff --git a/apollo-ios/Sources/Apollo/SelectionSet+DictionaryIntializer.swift b/apollo-ios/Sources/Apollo/SelectionSet+DictionaryIntializer.swift new file mode 100644 index 000000000..dd9b90c0a --- /dev/null +++ b/apollo-ios/Sources/Apollo/SelectionSet+DictionaryIntializer.swift @@ -0,0 +1,69 @@ +#if !COCOAPODS +import ApolloAPI +#endif + +public enum RootSelectionSetInitializeError: Error { + case hasNonHashableValue +} + +extension RootSelectionSet { + /// Initializes a `SelectionSet` with a raw JSON response object. + /// + /// The process of converting a JSON response into `SelectionSetData` is done by using a + /// `GraphQLExecutor` with a`GraphQLSelectionSetMapper` to parse, validate, and transform + /// the JSON response data into the format expected by `SelectionSet`. + /// + /// - Parameters: + /// - data: A dictionary representing a JSON response object for a GraphQL object. + /// - variables: [Optional] The operation variables that would be used to obtain + /// the given JSON response data. + @_disfavoredOverload + public init( + data: [String: Any], + variables: GraphQLOperation.Variables? = nil + ) throws { + let jsonObject = try Self.convertToAnyHashableValueDict(dict: data) + try self.init(data: jsonObject, variables: variables) + } + + /// Convert dictionary type [String: Any] to [String: AnyHashable] + /// - Parameter dict: [String: Any] type dictionary + /// - Returns: converted [String: AnyHashable] type dictionary + private static func convertToAnyHashableValueDict(dict: [String: Any]) throws -> [String: AnyHashable] { + var result = [String: AnyHashable]() + + for (key, value) in dict { + if let arrayValue = value as? [Any] { + result[key] = try convertToAnyHashableArray(array: arrayValue) + } else { + if let dictValue = value as? [String: Any] { + result[key] = try convertToAnyHashableValueDict(dict: dictValue) + } else if let hashableValue = value as? AnyHashable { + result[key] = hashableValue + } else { + throw RootSelectionSetInitializeError.hasNonHashableValue + } + } + } + return result + } + + /// Convert Any type Array type to AnyHashable type Array + /// - Parameter array: Any type Array + /// - Returns: AnyHashable type Array + private static func convertToAnyHashableArray(array: [Any]) throws -> [AnyHashable] { + var result: [AnyHashable] = [] + for value in array { + if let array = value as? [Any] { + result.append(try convertToAnyHashableArray(array: array)) + } else if let dict = value as? [String: Any] { + result.append(try convertToAnyHashableValueDict(dict: dict)) + } else if let hashable = value as? AnyHashable { + result.append(hashable) + } else { + throw RootSelectionSetInitializeError.hasNonHashableValue + } + } + return result + } +} diff --git a/docs/source/testing/test-mocks.mdx b/docs/source/testing/test-mocks.mdx index fff612336..500dae743 100644 --- a/docs/source/testing/test-mocks.mdx +++ b/docs/source/testing/test-mocks.mdx @@ -13,7 +13,7 @@ Generated test mocks provide a type-safe way to mock your response models. Rathe Because the generated response models are backed by a JSON dictionary, initializing them with mock data without generated test mocks is verbose and error-prone. You need to create stringly-typed JSON dictionaries that are structured exactly like the expected network response to ensure the your models parse properly. ```swift -let data: [String: AnyHashable] = [ +let data: [String: Any] = [ "data": [ "__typename": "Query", "hero": [ @@ -36,7 +36,7 @@ let data: [String: AnyHashable] = [ ] ] -let model = HeroAndFriendsQuery.Data(data: DataDict(data)) +let model = try HeroAndFriendsQuery.Data(data: data) XCTAssertEqual(model.hero.friends[1].name, "C-3PO") ```