Skip to content

Commit

Permalink
Refactor grouping of fields
Browse files Browse the repository at this point in the history
Merging selections now happens durring initial field grouping, rather than later.
  • Loading branch information
AnthonyMDev committed Sep 21, 2021
1 parent 0811558 commit 2c7a167
Show file tree
Hide file tree
Showing 14 changed files with 8,687 additions and 73 deletions.
8 changes: 8 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@
DE2FCF4326E809BB0057EA67 /* AllAnimalsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF3C26E809BB0057EA67 /* AllAnimalsQuery.swift */; };
DE2FCF4426E809BB0057EA67 /* ClassroomPetsWithSubtypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF3D26E809BB0057EA67 /* ClassroomPetsWithSubtypes.swift */; };
DE2FCF4526E80CF10057EA67 /* GraphQLOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC750601D2A59C300458D91 /* GraphQLOperation.swift */; };
DE2FCF4726E92EAB0057EA67 /* APIv1.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF4626E92EAB0057EA67 /* APIv1.swift */; };
DE2FCF4926E94D150057EA67 /* SelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF4826E94D150057EA67 /* SelectionTests.swift */; };
DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; };
DE3C7A94260A6C1000D2F4FF /* ApolloUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B68353E2463481A00337AE6 /* ApolloUtils.framework */; };
DE3C7A95260A6C1000D2F4FF /* ApolloUtils.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9B68353E2463481A00337AE6 /* ApolloUtils.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -825,6 +827,8 @@
DE2FCF3B26E809BB0057EA67 /* ClassroomPetsQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPetsQuery.swift; sourceTree = "<group>"; };
DE2FCF3C26E809BB0057EA67 /* AllAnimalsQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllAnimalsQuery.swift; sourceTree = "<group>"; };
DE2FCF3D26E809BB0057EA67 /* ClassroomPetsWithSubtypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPetsWithSubtypes.swift; sourceTree = "<group>"; };
DE2FCF4626E92EAB0057EA67 /* APIv1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIv1.swift; sourceTree = "<group>"; };
DE2FCF4826E94D150057EA67 /* SelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionTests.swift; sourceTree = "<group>"; };
DE3C7973260A646300D2F4FF /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = "<group>"; };
DE3C79A9260A6ACD00D2F4FF /* AnimalKingdomAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AnimalKingdomAPI.h; sourceTree = "<group>"; };
DE3C79AA260A6ACD00D2F4FF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1390,6 +1394,7 @@
isa = PBXGroup;
children = (
9BCF0CFC23FC9F060031D2A2 /* API.swift */,
DE2FCF4626E92EAB0057EA67 /* APIv1.swift */,
9BCF0CF123FC9F060031D2A2 /* StarWarsAPI.h */,
9B20614F2591B3860020D1E0 /* graphql */,
9BCF0CFF23FC9F060031D2A2 /* Info.plist */,
Expand Down Expand Up @@ -1600,6 +1605,7 @@
E61DD76426D60C1800C41614 /* SQLiteDotSwiftDatabaseBehaviorTests.swift */,
9B9BBB1A24DB75E60021C30F /* UploadRequestTests.swift */,
5BB2C0222380836100774170 /* VersionNumberTests.swift */,
DE2FCF4826E94D150057EA67 /* SelectionTests.swift */,
);
path = ApolloTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -2713,6 +2719,7 @@
9BAEEC10234BB95B00808306 /* FileManagerExtensionsTests.swift in Sources */,
9BAEEC17234C275600808306 /* ApolloSchemaTests.swift in Sources */,
DE2FCF4126E809BB0057EA67 /* WarmBloodedDetails.swift in Sources */,
DE2FCF4726E92EAB0057EA67 /* APIv1.swift in Sources */,
9F62DFAE2590557F00E6E808 /* DocumentParsingAndValidationTests.swift in Sources */,
DE2FCF4426E809BB0057EA67 /* ClassroomPetsWithSubtypes.swift in Sources */,
DE2FCF4326E809BB0057EA67 /* AllAnimalsQuery.swift in Sources */,
Expand Down Expand Up @@ -2871,6 +2878,7 @@
DED45E30261B972C0086EF63 /* CachePersistenceTests.swift in Sources */,
DED45EC4261BA0ED0086EF63 /* SplitNetworkTransportTests.swift in Sources */,
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */,
DE2FCF4926E94D150057EA67 /* SelectionTests.swift in Sources */,
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
DED45C2A2615319E0086EF63 /* DefaultInterceptorProviderTests.swift in Sources */,
Expand Down
225 changes: 175 additions & 50 deletions Sources/Apollo/GraphQLExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,71 @@ struct GraphQLResolveInfo {
}
}

fileprivate struct ExecutionInfo { // TODO: Rename?
let variables: [String: JSONEncodable]?
var responsePath: ResponsePath = []
var cachePath: ResponsePath = []

init(variables: [String: JSONEncodable]?, rootCacheKey: CacheKey? = nil) {
self.variables = variables
if let key = rootCacheKey {
cachePath = [key]
}
}
}

fileprivate struct FieldSelectionGrouping {
private var fieldInfoList: [String: FieldExecutionInfo] = [:]

mutating func append(field: Selection.Field, withInfo info: ExecutionInfo) {
let fieldKey = field.responseKey
if var fieldInfo = fieldInfoList[fieldKey] {
fieldInfo.mergeInSelections(of: field)
fieldInfoList[fieldKey] = fieldInfo
} else {
fieldInfoList[fieldKey] = FieldExecutionInfo(field: field, info: info)
}
}
}

struct FieldExecutionInfo {
let variables: [String: InputValue]?
let field: Selection.Field
private let info: ExecutionInfo

var responsePath: ResponsePath = []
var responseKeyForField: String = ""
private(set) var selections: [Selection] = []

var cachePath: ResponsePath = []
var cacheKeyForField: String = ""
let responsePath: ResponsePath
let responseKeyForField: String

var fields: [Selection.Field] = []
private(set) var cachePath: ResponsePath = []
private(set) var cacheKeyForField: String = ""

init(rootKey: CacheKey?, variables: [String: InputValue]?) {
self.variables = variables
fileprivate init(
field: Selection.Field,
info: ExecutionInfo
) {
self.field = field
self.info = info

if let rootKey = rootKey {
cachePath = [rootKey]
let responseKey = field.responseKey
responsePath = info.responsePath.appending(responseKey)
responseKeyForField = responseKey

mergeInSelections(of: field)
}

fileprivate mutating func computeCacheKeyAndPath(with variables: [String: JSONEncodable]) throws {
let cacheKey = try field.cacheKey(with: variables)
cachePath.append(cacheKey)
cacheKeyForField = cacheKey
}

fileprivate mutating func mergeInSelections(of field: Selection.Field) {
// TODO: Can we do this only for fields of object type?
// Hold on to a list of "otherFields" instead of a list of selections, then call a
// `mergeSelections()` function during `complete`, which only has to be called on object fields.
if case let .object(selectionSet) = field.type.namedType {
selections += selectionSet.selections
}
}
}
Expand Down Expand Up @@ -165,7 +214,7 @@ final class GraphQLExecutor {
info: info,
accumulator: accumulator)

return try accumulator.finish(rootValue: try rootValue.get(), info: info)
return try accumulator.finish(rootValue: try rootValue.get())
}

// TODO: Delete
Expand Down Expand Up @@ -219,15 +268,15 @@ final class GraphQLExecutor {
into: &groupedFields,
info: info)
}
case let fragmentSpread as GraphQLFragmentSpread:
let fragment = fragmentSpread.fragment

if let runtimeType = runtimeType, fragment.possibleTypes.contains(runtimeType) {
try collectFields(selections: fragment.selections,
forRuntimeType: runtimeType,
into: &groupedFields,
info: info)
}
// case let fragmentSpread as GraphQLFragmentSpread:
// let fragment = fragmentSpread.fragment

// if let runtimeType = runtimeType, fragment.possibleTypes.contains(runtimeType) {
// try collectFields(selections: fragment.selections,
// forRuntimeType: runtimeType,
// into: &groupedFields,
// info: info)
// }
case let typeCase as GraphQLTypeCase:
let selections: [GraphQLSelection]
if let runtimeType = runtimeType {
Expand Down Expand Up @@ -383,35 +432,50 @@ final class GraphQLExecutor {
return selections
}


// MARK: - New

func execute<Accumulator: GraphQLResultAccumulator>(
selections: [Selection],
selectionSet: AnySelectionSet.Type,
onData data: JSONObject,
withKey key: CacheKey? = nil,
variables: [String: JSONEncodable]? = nil,
accumulator: Accumulator
) throws -> Accumulator.FinalResult {
return try execute(selections: selectionSet.selections,
on: data,
withKey: key,
variables: variables,
accumulator: accumulator)
}

func execute<Accumulator: GraphQLResultAccumulator>(
selections: [Selection], // TODO: Pass in AnySelectionSet?
on object: JSONObject,
withKey key: CacheKey? = nil,
variables: [String: InputValue]? = nil,
variables: [String: JSONEncodable]? = nil,
accumulator: Accumulator
) throws -> Accumulator.FinalResult {
let info = FieldExecutionInfo(rootKey: key, variables: variables)
let info = ExecutionInfo(variables: variables, rootCacheKey: key)

let rootValue = execute(selections: selections,
on: object,
info: info,
accumulator: accumulator)
let rootValue = execute(
selections: selections,
on: object,
info: info,
accumulator: accumulator
)

return try accumulator.finish(rootValue: try rootValue.get(), info: info)
return try accumulator.finish(rootValue: try rootValue.get())
}

private func execute<Accumulator: GraphQLResultAccumulator>(
selections: [Selection],
on object: JSONObject,
info: FieldExecutionInfo,
info: ExecutionInfo,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.ObjectResult> {
do {
var groupedFields = GroupedSequence<String, Selection.Field>()
try groupFields(selections,
forRuntimeType: runtimeObjectType(for: object),
into: &groupedFields,
info: info)
let groupedFields = try groupFields(selections, on: object, info: info)

var fieldEntries: [PossiblyDeferred<Accumulator.FieldEntry>] = []
fieldEntries.reserveCapacity(groupedFields.keys.count)
Expand Down Expand Up @@ -440,6 +504,21 @@ final class GraphQLExecutor {
return schemaTypeFactory.objectType(forTypename: __typename)
}

private func groupFields(
_ selections: [Selection],
on object: JSONObject,
info: ExecutionInfo
) throws -> FieldSelectionGrouping {
var grouping = FieldSelectionGrouping()
try groupFields(
selections,
forRuntimeType: runtimeObjectType(for: object),
into: &grouping,
info: info
)
return grouping
}

/// Groups fields that share the same response key for simultaneous resolution.
///
/// Before execution, the selection set is converted to a grouped field set.
Expand All @@ -449,30 +528,24 @@ final class GraphQLExecutor {
private func groupFields(
_ selections: [Selection],
forRuntimeType runtimeType: Object.Type?,
into groupedFields: inout GroupedSequence<String, Selection.Field>,
info: FieldExecutionInfo
into groupedFields: inout FieldSelectionGrouping,
info: ExecutionInfo
) throws {
for selection in selections {
switch selection {
case let .field(field):
groupedFields.append(value: field, forKey: field.responseKey)
groupedFields.append(field: field, withInfo: info)

case let .booleanCondition(booleanCondition):
let value = info.variables?[booleanCondition.variableName]
switch value {
case Optional.none, .some(InputValue.none):
guard let value = info.variables?[booleanCondition.variableName] else {
throw GraphQLError("Variable \(booleanCondition.variableName) was not provided.")

case let .scalar(boolValue as Bool):
if boolValue == !booleanCondition.inverted {
try groupFields(booleanCondition.selections,
forRuntimeType: runtimeType,
into: &groupedFields,
info: info)
}

default:
throw GraphQLError("Variable \(booleanCondition.variableName) expected to be a Bool, got \(value ?? "nil").")
}
if value as? Bool == !booleanCondition.inverted {
try groupFields(booleanCondition.selections,
forRuntimeType: runtimeType,
into: &groupedFields,
info: info)
}

case let .fragmentSpread(fragmentSpread):
Expand Down Expand Up @@ -505,4 +578,56 @@ final class GraphQLExecutor {
}
}
}

// /// Each field requested in the grouped field set that is defined on the selected objectType will result in an entry in the response map. Field execution first coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value.
// ///
// /// - Note: GraphQL validation makes sure all fields sharing the same response key have the same
// /// arguments and are of the same type, so we only need to resolve one field.
// private func execute<Accumulator: GraphQLResultAccumulator>(field: Selection.Field,
// on object: JSONObject,
// info: GraphQLResolveInfo,
// accumulator: Accumulator) -> PossiblyDeferred<Accumulator.FieldEntry> {
//
// let firstField = fields[0]
//
// var info = info
//
// let responseKey = firstField.responseKey
// info.responseKeyForField = responseKey
// info.responsePath.append(responseKey)
//
// if shouldComputeCachePath {
// do {
// let cacheKey = try firstField.cacheKey(with: info.variables)
// info.cacheKeyForField = cacheKey
// info.cachePath.append(cacheKey)
// } catch {
// return .immediate(.failure(error))
// }
// }
//
// // We still need all fields to complete the value, because they may have different selection sets.
// info.fields = fields
//
// return PossiblyDeferred {
// guard let value = fieldResolver(object, info) else {
// throw JSONDecodingError.missingValue
// }
// return value
// }.flatMap {
// return self.complete(value: $0,
// ofType: firstField.type,
// info: info,
// accumulator: accumulator)
// }.map {
// try accumulator.accept(fieldEntry: $0, info: info)
// }.mapError { error in
// if !(error is GraphQLResultError) {
// return GraphQLResultError(path: info.responsePath, underlying: error)
// } else {
// return error
// }
// }
// }

}
10 changes: 5 additions & 5 deletions Sources/Apollo/GraphQLResultAccumulator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protocol GraphQLResultAccumulator: AnyObject {
func accept(fieldEntry: PartialResult, info: GraphQLResolveInfo) throws -> FieldEntry
func accept(fieldEntries: [FieldEntry], info: GraphQLResolveInfo) throws -> ObjectResult

func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult
func finish(rootValue: ObjectResult) throws -> FinalResult
}

func zip<Accumulator1: GraphQLResultAccumulator, Accumulator2: GraphQLResultAccumulator>(_ accumulator1: Accumulator1, _ accumulator2: Accumulator2) -> Zip2Accumulator<Accumulator1, Accumulator2> {
Expand Down Expand Up @@ -58,8 +58,8 @@ final class Zip2Accumulator<Accumulator1: GraphQLResultAccumulator, Accumulator2
return (try accumulator1.accept(fieldEntries: fieldEntries1, info: info), try accumulator2.accept(fieldEntries: fieldEntries2, info: info))
}

func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult {
return (try accumulator1.finish(rootValue: rootValue.0, info: info), try accumulator2.finish(rootValue: rootValue.1, info: info))
func finish(rootValue: ObjectResult) throws -> FinalResult {
return (try accumulator1.finish(rootValue: rootValue.0), try accumulator2.finish(rootValue: rootValue.1))
}
}

Expand Down Expand Up @@ -104,7 +104,7 @@ final class Zip3Accumulator<Accumulator1: GraphQLResultAccumulator, Accumulator2
return (try accumulator1.accept(fieldEntries: fieldEntries1, info: info), try accumulator2.accept(fieldEntries: fieldEntries2, info: info), try accumulator3.accept(fieldEntries: fieldEntries3, info: info))
}

func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult {
return (try accumulator1.finish(rootValue: rootValue.0, info: info), try accumulator2.finish(rootValue: rootValue.1, info: info), try accumulator3.finish(rootValue: rootValue.2, info: info))
func finish(rootValue: ObjectResult) throws -> FinalResult {
return (try accumulator1.finish(rootValue: rootValue.0), try accumulator2.finish(rootValue: rootValue.1), try accumulator3.finish(rootValue: rootValue.2))
}
}
8 changes: 6 additions & 2 deletions Sources/Apollo/GraphQLSelectionSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,15 @@ public struct GraphQLTypeCondition: GraphQLSelection {
}

public struct GraphQLFragmentSpread: GraphQLSelection {
let fragment: GraphQLFragment.Type
let fragment: Any.Type
// let fragment: GraphQLFragment.Type

public init(_ fragment: GraphQLFragment.Type) {
public init(_ fragment: Any.Type) {
self.fragment = fragment
}
// public init(_ fragment: GraphQLFragment.Type) {
// self.fragment = fragment
// }
}

public struct GraphQLTypeCase: GraphQLSelection {
Expand Down
Loading

0 comments on commit 2c7a167

Please sign in to comment.