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

Support SelectionSet to receive [String: Any] for initialization when create mock object #102

Merged
merged 16 commits into from
Nov 8, 2023
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
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
Loading