Skip to content

Commit

Permalink
Executor Refactor - Recursive execution
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyMDev committed Sep 21, 2021
1 parent 10c8066 commit 086e9b4
Showing 1 changed file with 118 additions and 57 deletions.
175 changes: 118 additions & 57 deletions Sources/Apollo/GraphQLExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,18 @@ struct GraphQLResolveInfo {

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

init(
variables: [String: JSONEncodable]?,
responsePath: ResponsePath,
cachePath: ResponsePath
) {
self.variables = variables
self.responsePath = responsePath
self.cachePath = cachePath
}

init(variables: [String: JSONEncodable]?, rootCacheKey: CacheKey? = nil) {
self.variables = variables
Expand All @@ -45,7 +55,9 @@ fileprivate struct ExecutionInfo { // TODO: Rename?
}
}

// func appending(responsePath: String, cachePath: String) -> // TODO?
mutating func resetCachePath(toCacheKey key: String) {
cachePath = [key]
}
}

fileprivate struct FieldSelectionGrouping: Sequence {
Expand Down Expand Up @@ -78,10 +90,10 @@ struct FieldExecutionInfo {

var mergedFields: [Selection.Field]

let responsePath: ResponsePath
var responsePath: ResponsePath
let responseKeyForField: String

private(set) var cachePath: ResponsePath = []
var cachePath: ResponsePath = []
private(set) var cacheKeyForField: String = ""

fileprivate init(
Expand All @@ -99,18 +111,30 @@ struct FieldExecutionInfo {

fileprivate mutating func computeCacheKeyAndPath() throws {
let cacheKey = try field.cacheKey(with: info.variables)
cachePath.append(cacheKey)
cachePath = info.cachePath.appending(cacheKey)
cacheKeyForField = cacheKey
}

fileprivate func computeMergedSelections() -> [Selection] {
/// The selections for all child selections of the merged fields.
///
/// There will only be child selections if the fields for this field info are
/// object type fields (objects; lists of objects; or non-null wrapped objects).
/// For scalar fields, the child selections will be an empty array.
fileprivate func computeChildSelections() -> [Selection] {
return mergedFields.flatMap { field -> [Selection] in
guard case let .object(selectionSet) = field.type.namedType else {
return []
}
return selectionSet.selections
}
}

/// Returns the `ExecutionInfo` that should be used for executing the child selections.
fileprivate func executionInfoForChildSelections() -> ExecutionInfo {
return ExecutionInfo(variables: info.variables,
responsePath: responsePath,
cachePath: cachePath)
}
}

/// An error which has occurred in processing a GraphQLResult
Expand Down Expand Up @@ -543,7 +567,6 @@ final class GraphQLExecutor {
groupedFields.append(field: field, withInfo: info)

case let .booleanCondition(booleanCondition):
let value = info.variables?[booleanCondition.variableName]
guard let value = info.variables?[booleanCondition.variableName] else {
throw GraphQLError("Variable \(booleanCondition.variableName) was not provided.")
}
Expand All @@ -565,19 +588,20 @@ final class GraphQLExecutor {
}

case let .typeCase(typeCase):
let selections: [Selection]
// TODO: Selections from parent types should be inherited automatically. Reduces amount of generated code.
// Also need to be able to select fields for multiple "variants" as a variant shouldn't be generated for
// every single concrete type.
if let runtimeType = runtimeType {
selections = typeCase.variants[runtimeType] ?? typeCase.default
} else {
selections = typeCase.default
}
try groupFields(selections,
forRuntimeType: runtimeType,
into: &groupedFields,
info: info)
fatalError("TODO: Implement this!")
// let selections: [Selection]
// // TODO: Selections from parent types should be inherited automatically. Reduces amount of generated code.
// // Also need to be able to select fields for multiple "variants" as a variant shouldn't be generated for
// // every single concrete type.
// if let runtimeType = runtimeType {
// selections = typeCase.variants[runtimeType] ?? typeCase.default
// } else {
// selections = typeCase.default
// }
// try groupFields(selections,
// forRuntimeType: runtimeType,
// into: &groupedFields,
// info: info)

// default:
// preconditionFailure()
Expand Down Expand Up @@ -610,8 +634,8 @@ final class GraphQLExecutor {
}
return value
}.flatMap {
return self.complete(value: $0,
for: fieldInfo,
return self.complete(fields: fieldInfo,
withValue: $0,
accumulator: accumulator)
}.map {
try accumulator.accept(fieldEntry: $0, info: fieldInfo)
Expand All @@ -625,37 +649,37 @@ final class GraphQLExecutor {
}

private func complete<Accumulator: GraphQLResultAccumulator>(
value: JSONValue,
for fieldInfo: FieldExecutionInfo,
fields fieldInfo: FieldExecutionInfo,
withValue value: JSONValue,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.PartialResult> {
complete(value: value,
complete(fields: fieldInfo,
withValue: value,
asType: fieldInfo.field.type,
fieldInfo: fieldInfo,
accumulator: accumulator)
}

/// After resolving the value for a field, it is completed by ensuring it adheres to the expected
/// return type. If the return type is another Object type, then the field execution process
/// continues recursively.
private func complete<Accumulator: GraphQLResultAccumulator>(
value: JSONValue,
fields fieldInfo: FieldExecutionInfo,
withValue value: JSONValue,
asType returnType: Selection.Field.OutputType,
fieldInfo: FieldExecutionInfo,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.PartialResult> {
if value is NSNull && returnType.isNullable {
return PossiblyDeferred { try accumulator.acceptNullValue(info: fieldInfo) }
}

switch returnType {
case let .nonNull(innerType) where value is NSNull:
case .nonNull where value is NSNull:
return .immediate(.failure(JSONDecodingError.nullValue))

case let .nonNull(innerType):
return complete(value: value,
return complete(fields: fieldInfo,
withValue: value,
asType: innerType,
fieldInfo: fieldInfo,
accumulator: accumulator)

case .scalar:
Expand All @@ -669,53 +693,90 @@ final class GraphQLExecutor {
let completedArray = array
.enumerated()
.map { index, element -> PossiblyDeferred<Accumulator.PartialResult> in
var info = info
var elementFieldInfo = fieldInfo

let indexSegment = String(index)
info.responsePath.append(indexSegment)
elementFieldInfo.responsePath.append(indexSegment)

if shouldComputeCachePath {
info.cachePath.append(indexSegment)
elementFieldInfo.cachePath.append(indexSegment)
}

return self.complete(value: element,
ofType: innerType,
info: info,
return self.complete(fields: elementFieldInfo,
withValue: element,
asType: innerType,
accumulator: accumulator)
}

return lazilyEvaluateAll(completedArray).map {
try accumulator.accept(list: $0, info: fieldInfo)
}
case .object:
if let reference = value as? CacheReference, let resolveReference = resolveReference {
switch value {
case let reference as CacheReference:
guard let resolveReference = resolveReference else {
return .immediate(.failure(JSONDecodingError.wrongType))
}

return resolveReference(reference).flatMap {
self.complete(value: $0,
ofType: returnType,
info: info,
self.complete(fields: fieldInfo,
withValue: $0,
asType: returnType,
accumulator: accumulator)
}
}

guard let object = value as? JSONObject else {
return .immediate(.failure(JSONDecodingError.wrongType))
}

// The merged selection set is a list of fields from all sub‐selection sets of the original fields.
let selections = mergeSelectionSets(for: info.fields)
case let object as JSONObject:
return executeChildSelections(forObjectTypeFields: fieldInfo,
onChildObject: object,
accumulator: accumulator)

var info = info
if shouldComputeCachePath, let cacheKeyForObject = self.cacheKey(for: object) {
info.cachePath = [cacheKeyForObject]
default:
return .immediate(.failure(JSONDecodingError.wrongType))
}

// We execute the merged selection set on the object to complete the value. This is the recursive step in the GraphQL execution model.
return self.execute(selections: selections,
on: object,
info: info,
accumulator: accumulator).map { $0 as! Accumulator.PartialResult }
default:
preconditionFailure()
}
}

private func executeChildSelections<Accumulator: GraphQLResultAccumulator>(
forObjectTypeFields fieldInfo: FieldExecutionInfo,
onChildObject object: JSONObject,
accumulator: Accumulator) -> PossiblyDeferred<Accumulator.PartialResult> {

let selections = fieldInfo.computeChildSelections()
var childExecutionInfo = fieldInfo.executionInfoForChildSelections()

// If the object has it's own cache key, reset the cache path to the key,
// rather than using the inherited cache path from the parent field.
if shouldComputeCachePath, let cacheKeyForObject = self.cacheKey(for: object) {
childExecutionInfo.resetCachePath(toCacheKey: cacheKeyForObject)
}

return self.execute(selections: selections,
on: object,
info: childExecutionInfo,
accumulator: accumulator).map { $0 as! Accumulator.PartialResult }
}
}

// TODO: can we replace OutputType with Any.Type?
// let type: Any.Type = type(of: returnType)
// switch type {
// case let optionalType as AnyOptional.Type:
// switch optionalType.wrappedType {
//
// }
//
// default:
//
// }
// public protocol AnyOptional {
// static var wrappedType: Any.Type { get }
// }
//
// extension Optional: AnyOptional {
// public static var wrappedType: Any.Type {
// Self.WrappedType.self
// }
// }

0 comments on commit 086e9b4

Please sign in to comment.