Skip to content

Commit

Permalink
Support SelectionSet to receive [String: Any] for initialization when…
Browse files Browse the repository at this point in the history
… create mock object (#102)
  • Loading branch information
Cookiezby authored and gh-action-runner committed Nov 8, 2023
1 parent 039872f commit e1bb21b
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 2 deletions.
277 changes: 277 additions & 0 deletions Tests/ApolloTests/SelectionSetTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
69 changes: 69 additions & 0 deletions apollo-ios/Sources/Apollo/SelectionSet+DictionaryIntializer.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 2 additions & 2 deletions docs/source/testing/test-mocks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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")
```
Expand Down

0 comments on commit e1bb21b

Please sign in to comment.