Skip to content

Commit

Permalink
Move Object Type to DataDict (#2805)
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyMDev committed Feb 13, 2023
1 parent f9a452c commit e7bb548
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 220 deletions.
9 changes: 6 additions & 3 deletions Sources/Apollo/GraphQLExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,18 @@ final class GraphQLExecutor {

// MARK: - Execution

func execute<Accumulator: GraphQLResultAccumulator>(
selectionSet: RootSelectionSet.Type,
func execute<
Accumulator: GraphQLResultAccumulator,
SelectionSet: RootSelectionSet
>(
selectionSet: SelectionSet.Type,
on data: JSONObject,
withRootCacheReference root: CacheReference? = nil,
variables: GraphQLOperation.Variables? = nil,
accumulator: Accumulator
) throws -> Accumulator.FinalResult {
let info = ObjectExecutionInfo(variables: variables,
schema: selectionSet.__schema,
schema: SelectionSet.Schema.self,
withRootCacheReference: root)

let rootValue = execute(selections: selectionSet.__selections,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Apollo/GraphQLSelectionSetMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ApolloAPI
import Foundation

/// An accumulator that converts executed data to the correct values to create a `SelectionSet`.
final class GraphQLSelectionSetMapper<SelectionSet: AnySelectionSet>: GraphQLResultAccumulator {
final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator {

let stripNullValues: Bool
let allowMissingValuesForOptionalFields: Bool
Expand Down Expand Up @@ -71,7 +71,7 @@ final class GraphQLSelectionSetMapper<SelectionSet: AnySelectionSet>: GraphQLRes
return .init(fieldEntries, uniquingKeysWith: { (_, last) in last })
}

func finish(rootValue: JSONObject, info: ObjectExecutionInfo) -> SelectionSet {
return SelectionSet.init(data: DataDict(rootValue, variables: info.variables))
func finish(rootValue: JSONObject, info: ObjectExecutionInfo) -> T {
return T.init(unsafeData: rootValue, variables: info.variables)
}
}
40 changes: 26 additions & 14 deletions Sources/ApolloAPI/DataDict.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
public struct DataDict: Hashable {

public var _data: JSONObject
public let _objectType: Object?
public let _variables: GraphQLOperation.Variables?

public init(
@usableFromInline init(
_ data: JSONObject,
objectType: Object?,
variables: GraphQLOperation.Variables?
) {
self._data = data
self._objectType = objectType
self._variables = variables
}

Expand All @@ -23,10 +26,10 @@ public struct DataDict: Hashable {
}

@inlinable public subscript<T: SelectionSetEntityValue>(_ key: String) -> T {
get { T.init(fieldData: _data[key], variables: _variables) }
get { T.init(_fieldData: _data[key], variables: _variables) }
set { _data[key] = newValue._fieldData }
_modify {
var value = T.init(fieldData: _data[key], variables: _variables)
var value = T.init(_fieldData: _data[key], variables: _variables)
defer { _data[key] = value._fieldData }
yield &value
}
Expand All @@ -44,39 +47,48 @@ public struct DataDict: Hashable {
}

public protocol SelectionSetEntityValue {
init(fieldData: AnyHashable?, variables: GraphQLOperation.Variables?)
/// - Warning: This function is not supported for external use.
/// Unsupported usage may result in unintended consequences including crashes.
init(_fieldData: AnyHashable?, variables: GraphQLOperation.Variables?)
var _fieldData: AnyHashable { get }
}

extension AnySelectionSet {
@inlinable public init(fieldData: AnyHashable?, variables: GraphQLOperation.Variables?) {
guard let fieldData = fieldData as? JSONObject else {
extension SelectionSet {
/// - Warning: This function is not supported for external use.
/// Unsupported usage may result in unintended consequences including crashes.
@inlinable public init(_fieldData data: AnyHashable?, variables: GraphQLOperation.Variables?) {
guard let data = data as? JSONObject else {
fatalError("\(Self.self) expected data for entity.")
}
self.init(data: DataDict(fieldData, variables: variables))

self.init(unsafeData: data, variables: variables)
}

@inlinable public var _fieldData: AnyHashable { __data._data }
}

extension Optional: SelectionSetEntityValue where Wrapped: SelectionSetEntityValue {
@inlinable public init(fieldData: AnyHashable?, variables: GraphQLOperation.Variables?) {
guard case let .some(fieldData) = fieldData else {
/// - Warning: This function is not supported for external use.
/// Unsupported usage may result in unintended consequences including crashes.
@inlinable public init(_fieldData data: AnyHashable?, variables: GraphQLOperation.Variables?) {
guard case let .some(data) = data else {
self = .none
return
}
self = .some(Wrapped.init(fieldData: fieldData, variables: variables))
self = .some(Wrapped.init(_fieldData: data, variables: variables))
}

@inlinable public var _fieldData: AnyHashable { map(\._fieldData) }
}

extension Array: SelectionSetEntityValue where Element: SelectionSetEntityValue {
@inlinable public init(fieldData: AnyHashable?, variables: GraphQLOperation.Variables?) {
guard let fieldData = fieldData as? [AnyHashable?] else {
/// - Warning: This function is not supported for external use.
/// Unsupported usage may result in unintended consequences including crashes.
@inlinable public init(_fieldData data: AnyHashable?, variables: GraphQLOperation.Variables?) {
guard let data = data as? [AnyHashable?] else {
fatalError("\(Self.self) expected list of data for entity.")
}
self = fieldData.map { Element.init(fieldData:$0?.base as? AnyHashable, variables: variables) }
self = data.map { Element.init(_fieldData:$0?.base as? AnyHashable, variables: variables) }
}

@inlinable public var _fieldData: AnyHashable { map(\._fieldData) }
Expand Down
2 changes: 1 addition & 1 deletion Sources/ApolloAPI/FragmentProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
///
/// A ``SelectionSet`` can be converted to any ``Fragment`` included in it's
/// `Fragments` object via its ``SelectionSet/fragments-swift.property`` property.
public protocol Fragment: AnySelectionSet {
public protocol Fragment: SelectionSet {
/// The definition of the fragment in GraphQL syntax.
static var fragmentDefinition: StaticString { get }
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/ApolloAPI/GraphQLOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ public enum DocumentType {
/// - See: [GraphQLSpec - Document](https://spec.graphql.org/draft/#Document)
public struct OperationDefinition {
let operationDefinition: String
let fragments: [Fragment.Type]?
let fragments: [any Fragment.Type]?

public init(_ definition: String, fragments: [Fragment.Type]? = nil) {
public init(_ definition: String, fragments: [any Fragment.Type]? = nil) {
self.operationDefinition = definition
self.fragments = fragments
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/ApolloAPI/Selection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ public enum Selection {
/// A single field selection.
case field(Field)
/// A fragment spread of a named fragment definition.
case fragment(Fragment.Type)
case fragment(any Fragment.Type)
/// An inline fragment with a child selection set nested in a parent selection set.
case inlineFragment(InlineFragment.Type)
case inlineFragment(any InlineFragment.Type)
/// A group of selections that have `@include/@skip` directives.
case conditional(Conditions, [Selection])

Expand Down Expand Up @@ -35,7 +35,7 @@ public enum Selection {
public indirect enum OutputType {
case scalar(any ScalarType.Type)
case customScalar(any CustomScalarType.Type)
case object(RootSelectionSet.Type)
case object(any RootSelectionSet.Type)
case nonNull(OutputType)
case list(OutputType)

Expand Down
83 changes: 51 additions & 32 deletions Sources/ApolloAPI/SelectionSet.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,3 @@
// MARK: - Type Erased SelectionSets

public protocol AnySelectionSet: SelectionSetEntityValue {
static var __schema: SchemaMetadata.Type { get }

static var __selections: [Selection] { get }

/// The GraphQL type for the `SelectionSet`.
///
/// This may be a concrete type (`Object`) or an abstract type (`Interface`, or `Union`).
static var __parentType: ParentType { get }

/// The data of the underlying GraphQL object represented by generated selection set.
var __data: DataDict { get }

/// Designated Initializer
///
/// - Parameter data: The data of the underlying GraphQL object represented by generated
/// selection set.
init(data: DataDict)
}

public extension AnySelectionSet {
static var __selections: [Selection] { [] }
}

/// A selection set that represents the root selections on its `__parentType`. Nested selection
/// sets for type cases are not `RootSelectionSet`s.
///
Expand All @@ -38,7 +12,7 @@ public extension AnySelectionSet {
/// This is why only a `RootSelectionSet` can be executed by a `GraphQLExecutor`. Executing a
/// non-root selection set would result in fields from the root selection set not being collected
/// into the `ResponseDict` for the `SelectionSet`'s data.
public protocol RootSelectionSet: AnySelectionSet, OutputTypeConvertible { }
public protocol RootSelectionSet: SelectionSet, OutputTypeConvertible { }

/// A selection set that represents an inline fragment nested inside a `RootSelectionSet`.
///
Expand All @@ -51,23 +25,39 @@ public protocol RootSelectionSet: AnySelectionSet, OutputTypeConvertible { }
/// from the fragment's parent `RootSelectionSet` that will be selected. This includes fields from
/// the parent selection set, as well as any other child selections sets that are compatible with
/// the `InlineFragment`'s `__parentType` and the operation's inclusion condition.
public protocol InlineFragment: AnySelectionSet { }
public protocol InlineFragment: SelectionSet { }

// MARK: - SelectionSet
public protocol SelectionSet: AnySelectionSet, Hashable {
public protocol SelectionSet: SelectionSetEntityValue, Hashable {
associatedtype Schema: SchemaMetadata

/// A type representing all of the fragments the `SelectionSet` can be converted to.
/// Defaults to a stub type with no fragments.
/// A `SelectionSet` with fragments should provide a type that conforms to `FragmentContainer`
associatedtype Fragments = NoFragments

static var __selections: [Selection] { get }

/// The GraphQL type for the `SelectionSet`.
///
/// This may be a concrete type (`Object`) or an abstract type (`Interface`, or `Union`).
static var __parentType: ParentType { get }

/// The data of the underlying GraphQL object represented by generated selection set.
var __data: DataDict { get }

/// Designated Initializer
///
/// - Parameter data: The data of the underlying GraphQL object represented by generated
/// selection set.
init(data: DataDict)
}

extension SelectionSet {
extension SelectionSet {

@inlinable public static var __schema: SchemaMetadata.Type { Schema.self }
@inlinable public static var __selections: [Selection] { [] }

@usableFromInline var __objectType: Object? { Schema.objectType(forTypename: __typename) }
@inlinable public var __objectType: Object? { __data._objectType }

@inlinable public var __typename: String { __data["__typename"] }

Expand Down Expand Up @@ -106,6 +96,35 @@ extension SelectionSet {
_asInlineFragment(if: Selection.Conditions(condition))
}

/// Initializes the `SelectionSet` **unsafely** with an unsafe result data dictionary.
///
/// - Warning: This method is unsafe and improper use may result in unintended consequences
/// including crashes. The `unsafeData` should mirror the result data returned by a
/// `GraphQLSelectionSetMapper` after completion of GraphQL Execution.
///
/// This is not identical to the JSON response from a GraphQL network request. The data should be
/// normalized and custom scalars should be converted to their concrete types.
///
/// To create a `SelectionSet` from data representing a JSON format GraphQL network response
/// directly, create a `GraphQLResponse` object and call `parseResultFast()`.
@inlinable public init(
unsafeData data: [String: AnyHashable],
variables: GraphQLOperation.Variables? = nil
) {
let objectType: Object?
if let typename = data["__typename"] as? String {
objectType = Schema.objectType(forTypename: typename)
} else {
objectType = nil
}

self.init(data: DataDict(
data,
objectType: objectType,
variables: variables
))
}

@inlinable public func hash(into hasher: inout Hasher) {
hasher.combine(__data)
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/ApolloTestSupport/TestMock.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#if !COCOAPODS
@_exported import ApolloAPI
@_exported @testable import ApolloAPI
#endif
import Foundation

Expand Down Expand Up @@ -77,11 +77,15 @@ public class Mock<O: MockObject>: AnyMock, Hashable {
// MARK: - Selection Set Conversion

public extension SelectionSet {
static func from(
_ mock: AnyMock,
static func from<O: MockObject>(
_ mock: Mock<O>,
withVariables variables: GraphQLOperation.Variables? = nil
) -> Self {
Self.init(data: DataDict(mock._selectionSetMockData, variables: variables))
Self.init(data: DataDict(
mock._selectionSetMockData,
objectType: O.objectType,
variables: variables)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public extension MockMutableRootSelectionSet {
static var __parentType: ParentType { Object.mock }

init() {
self.init(data: DataDict([:], variables: nil))
self.init(data: .empty())
}
}

Expand Down
24 changes: 17 additions & 7 deletions Tests/ApolloInternalTestHelpers/MockOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ open class MockSubscription<SelectionSet: RootSelectionSet>: MockOperation<Selec
// MARK: - MockSelectionSets

@dynamicMemberLookup
open class AbstractMockSelectionSet: AnySelectionSet {
open class AbstractMockSelectionSet<F>: RootSelectionSet, Hashable {
public typealias Schema = MockSchemaMetadata
public typealias Fragments = F

open class var __schema: SchemaMetadata.Type { MockSchemaMetadata.self }
open class var __selections: [Selection] { [] }
open class var __parentType: ParentType { Object.mock }

public var __data: DataDict = DataDict([:], variables: nil)
public var __data: DataDict = .empty()

public required init(data: DataDict) {
self.__data = data
Expand All @@ -62,10 +65,7 @@ open class AbstractMockSelectionSet: AnySelectionSet {
public subscript<T: MockSelectionSet>(dynamicMember key: String) -> T? {
__data[key]
}

}

open class MockSelectionSet: AbstractMockSelectionSet, RootSelectionSet, Hashable {
public static func == (lhs: MockSelectionSet, rhs: MockSelectionSet) -> Bool {
lhs.__data == rhs.__data
}
Expand All @@ -75,8 +75,18 @@ open class MockSelectionSet: AbstractMockSelectionSet, RootSelectionSet, Hashabl
}
}

open class MockFragment: AbstractMockSelectionSet, RootSelectionSet, Fragment {
public typealias MockSelectionSet = AbstractMockSelectionSet<NoFragments>

open class MockFragment: MockSelectionSet, Fragment {
public typealias Schema = MockSchemaMetadata

open class var fragmentDefinition: StaticString { "" }
}

open class MockTypeCase: AbstractMockSelectionSet, InlineFragment { }
open class MockTypeCase: MockSelectionSet, InlineFragment { }

extension DataDict {
public static func empty() -> DataDict {
DataDict([:], objectType: nil, variables: nil)
}
}
Loading

0 comments on commit e7bb548

Please sign in to comment.