Skip to content

Commit 0b563c9

Browse files
BobaFetterscalvincestari
authored andcommittedSep 13, 2023
Migrating defer branch to new project structure
1 parent c8aba64 commit 0b563c9

12 files changed

+798
-196
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ xcuserdata/
2121
*.xcscmblueprint
2222
.DS_Store
2323

24+
## Visual Studio Code
25+
.vscode/launch.json
26+
2427
## Obj-C/Swift specific
2528
*.hmap
2629
*.ipa

‎Design/3093-graphql-defer.md

+459
Large diffs are not rendered by default.

‎Sources/Apollo/FieldSelectionCollector.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,16 @@ struct DefaultFieldSelectionCollector: FieldSelectionCollector {
7777
info: info)
7878
}
7979

80-
case let .fragment(fragment):
80+
// TODO: _ is fine for now but will need to be handled in #3145
81+
case let .fragment(fragment, _):
8182
groupedFields.addFulfilledFragment(fragment)
8283
try collectFields(from: fragment.__selections,
8384
into: &groupedFields,
8485
for: object,
8586
info: info)
8687

87-
case let .inlineFragment(typeCase):
88+
// TODO: _ is fine for now but will need to be handled in #3145
89+
case let .inlineFragment(typeCase, _):
8890
if let runtimeType = info.runtimeObjectType(for: object),
8991
typeCase.__parentType.canBeConverted(from: runtimeType) {
9092
groupedFields.addFulfilledFragment(typeCase)
@@ -146,7 +148,8 @@ struct CustomCacheDataWritingFieldSelectionCollector: FieldSelectionCollector {
146148
info: info,
147149
asConditionalFields: true)
148150

149-
case let .fragment(fragment):
151+
// TODO: _ is fine for now but will need to be handled in #3145
152+
case let .fragment(fragment, _):
150153
if groupedFields.fulfilledFragments.contains(type: fragment) {
151154
try collectFields(from: fragment.__selections,
152155
into: &groupedFields,
@@ -155,7 +158,8 @@ struct CustomCacheDataWritingFieldSelectionCollector: FieldSelectionCollector {
155158
asConditionalFields: false)
156159
}
157160

158-
case let .inlineFragment(typeCase):
161+
// TODO: _ is fine for now but will need to be handled in #3145
162+
case let .inlineFragment(typeCase, _):
159163
if groupedFields.fulfilledFragments.contains(type: typeCase) {
160164
try collectFields(from: typeCase.__selections,
161165
into: &groupedFields,
+40-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,50 @@
11
import Foundation
22

3+
// MARK: Status extensions
34
extension HTTPURLResponse {
45
var isSuccessful: Bool {
56
return (200..<300).contains(statusCode)
67
}
8+
}
79

10+
// MARK: Multipart extensions
11+
extension HTTPURLResponse {
12+
/// Returns true if the `Content-Type` HTTP header contains the `multipart/mixed` MIME type.
813
var isMultipart: Bool {
914
return (allHeaderFields["Content-Type"] as? String)?.contains("multipart/mixed") ?? false
1015
}
1116

12-
var multipartBoundary: String? {
13-
guard let contentType = allHeaderFields["Content-Type"] as? String else { return nil }
17+
struct MultipartHeaderComponents {
18+
let media: String?
19+
let boundary: String?
20+
let `protocol`: String?
21+
22+
init(media: String? = nil, boundary: String? = nil, protocol: String? = nil) {
23+
self.media = media
24+
self.boundary = boundary
25+
self.protocol = `protocol`
26+
}
27+
}
28+
29+
/// Components of the `Content-Type` header specifically related to the `multipart` media type.
30+
var multipartHeaderComponents: MultipartHeaderComponents {
31+
guard let contentType = allHeaderFields["Content-Type"] as? String else {
32+
return MultipartHeaderComponents()
33+
}
1434

15-
let marker = "boundary="
16-
let markerLength = marker.count
35+
var media: String? = nil
36+
var boundary: String? = nil
37+
var `protocol`: String? = nil
1738

1839
for component in contentType.components(separatedBy: ";") {
1940
let directive = component.trimmingCharacters(in: .whitespaces)
20-
if directive.prefix(markerLength) == marker {
41+
42+
if directive.starts(with: "multipart/") {
43+
media = directive.components(separatedBy: "/").last
44+
continue
45+
}
46+
47+
if directive.starts(with: "boundary=") {
2148
if let markerEndIndex = directive.firstIndex(of: "=") {
2249
var startIndex = directive.index(markerEndIndex, offsetBy: 1)
2350
if directive[startIndex] == "\"" {
@@ -28,11 +55,17 @@ extension HTTPURLResponse {
2855
endIndex = directive.index(before: endIndex)
2956
}
3057

31-
return String(directive[startIndex...endIndex])
58+
boundary = String(directive[startIndex...endIndex])
3259
}
60+
continue
61+
}
62+
63+
if directive.contains("Spec=") {
64+
`protocol` = directive
65+
continue
3366
}
3467
}
3568

36-
return nil
69+
return MultipartHeaderComponents(media: media, boundary: boundary, protocol: `protocol`)
3770
}
3871
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
#if !COCOAPODS
3+
import ApolloAPI
4+
#endif
5+
6+
struct MultipartResponseDeferParser: MultipartResponseSpecificationParser {
7+
static let protocolSpec: String = "deferSpec=20220824"
8+
9+
static func parse(
10+
data: Data,
11+
boundary: String,
12+
dataHandler: ((Data) -> Void),
13+
errorHandler: ((Error) -> Void)
14+
) {
15+
// TODO: Will be implemented in #3146
16+
}
17+
}

‎Sources/Apollo/MultipartResponseParsingInterceptor.swift

+51-132
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,23 @@ import ApolloAPI
66
/// Parses multipart response data into chunks and forwards each on to the next interceptor.
77
public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
88

9-
public enum MultipartResponseParsingError: Error, LocalizedError, Equatable {
9+
public enum ParsingError: Error, LocalizedError, Equatable {
1010
case noResponseToParse
11-
case cannotParseResponseData
12-
case unsupportedContentType(type: String)
13-
case cannotParseChunkData
14-
case irrecoverableError(message: String?)
15-
case cannotParsePayloadData
11+
case cannotParseResponse
1612

1713
public var errorDescription: String? {
1814
switch self {
1915
case .noResponseToParse:
2016
return "There is no response to parse. Check the order of your interceptors."
21-
case .cannotParseResponseData:
17+
case .cannotParseResponse:
2218
return "The response data could not be parsed."
23-
case let .unsupportedContentType(type):
24-
return "Unsupported content type: application/json is required but got \(type)."
25-
case .cannotParseChunkData:
26-
return "The chunk data could not be parsed."
27-
case let .irrecoverableError(message):
28-
return "An irrecoverable error occured: \(message ?? "unknown")."
29-
case .cannotParsePayloadData:
30-
return "The payload data could not be parsed."
3119
}
3220
}
3321
}
3422

35-
private enum ChunkedDataLine {
36-
case heartbeat
37-
case contentHeader(type: String)
38-
case json(object: JSONObject)
39-
case unknown
40-
}
41-
42-
private static let dataLineSeparator: StaticString = "\r\n\r\n"
43-
private static let contentTypeHeader: StaticString = "content-type:"
44-
private static let heartbeat: StaticString = "{}"
23+
private static let responseParsers: [String: MultipartResponseSpecificationParser.Type] = [
24+
MultipartResponseSubscriptionParser.protocolSpec: MultipartResponseSubscriptionParser.self
25+
]
4526

4627
public var id: String = UUID().uuidString
4728

@@ -56,7 +37,7 @@ public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
5637

5738
guard let response else {
5839
chain.handleErrorAsync(
59-
MultipartResponseParsingError.noResponseToParse,
40+
ParsingError.noResponseToParse,
6041
request: request,
6142
response: response,
6243
completion: completion
@@ -74,129 +55,67 @@ public struct MultipartResponseParsingInterceptor: ApolloInterceptor {
7455
return
7556
}
7657

58+
let multipartComponents = response.httpResponse.multipartHeaderComponents
59+
7760
guard
78-
let boundaryString = response.httpResponse.multipartBoundary,
79-
let dataString = String(data: response.rawData, encoding: .utf8)
61+
let boundary = multipartComponents.boundary,
62+
let `protocol` = multipartComponents.protocol,
63+
let parser = Self.responseParsers[`protocol`]
8064
else {
8165
chain.handleErrorAsync(
82-
MultipartResponseParsingError.cannotParseResponseData,
66+
ParsingError.cannotParseResponse,
8367
request: request,
8468
response: response,
8569
completion: completion
8670
)
8771
return
8872
}
8973

90-
for chunk in dataString.components(separatedBy: "--\(boundaryString)") {
91-
if chunk.isEmpty || chunk.isBoundaryPrefix { continue }
92-
93-
for dataLine in chunk.components(separatedBy: Self.dataLineSeparator.description) {
94-
switch (parse(dataLine: dataLine.trimmingCharacters(in: .newlines))) {
95-
case .heartbeat:
96-
// Periodically sent by the router - noop
97-
continue
98-
99-
case let .contentHeader(type):
100-
guard type == "application/json" else {
101-
chain.handleErrorAsync(
102-
MultipartResponseParsingError.unsupportedContentType(type: type),
103-
request: request,
104-
response: response,
105-
completion: completion
106-
)
107-
return
108-
}
109-
110-
case let .json(object):
111-
if let errors = object["errors"] as? [JSONObject] {
112-
let message = errors.first?["message"] as? String
113-
114-
chain.handleErrorAsync(
115-
MultipartResponseParsingError.irrecoverableError(message: message),
116-
request: request,
117-
response: response,
118-
completion: completion
119-
)
120-
121-
// These are fatal-level transport errors, don't process anything else.
122-
return
123-
}
124-
125-
guard let payload = object["payload"] else {
126-
chain.handleErrorAsync(
127-
MultipartResponseParsingError.cannotParsePayloadData,
128-
request: request,
129-
response: response,
130-
completion: completion
131-
)
132-
return
133-
}
134-
135-
if payload is NSNull {
136-
// `payload` can be null such as in the case of a transport error
137-
continue
138-
}
139-
140-
guard
141-
let payload = payload as? JSONObject,
142-
let data: Data = try? JSONSerializationFormat.serialize(value: payload)
143-
else {
144-
chain.handleErrorAsync(
145-
MultipartResponseParsingError.cannotParsePayloadData,
146-
request: request,
147-
response: response,
148-
completion: completion
149-
)
150-
return
151-
}
152-
153-
let response = HTTPResponse<Operation>(
154-
response: response.httpResponse,
155-
rawData: data,
156-
parsedResponse: nil
157-
)
158-
chain.proceedAsync(
159-
request: request,
160-
response: response,
161-
interceptor: self,
162-
completion: completion
163-
)
164-
165-
case .unknown:
166-
chain.handleErrorAsync(
167-
MultipartResponseParsingError.cannotParseChunkData,
168-
request: request,
169-
response: response,
170-
completion: completion
171-
)
172-
}
173-
}
174-
}
175-
}
176-
177-
/// Parses the data line of a multipart response chunk
178-
private func parse(dataLine: String) -> ChunkedDataLine {
179-
if dataLine == Self.heartbeat.description {
180-
return .heartbeat
181-
}
74+
let dataHandler: ((Data) -> Void) = { data in
75+
let response = HTTPResponse<Operation>(
76+
response: response.httpResponse,
77+
rawData: data,
78+
parsedResponse: nil
79+
)
18280

183-
if dataLine.starts(with: Self.contentTypeHeader.description) {
184-
return .contentHeader(type: (dataLine.components(separatedBy: ":").last ?? dataLine)
185-
.trimmingCharacters(in: .whitespaces)
81+
chain.proceedAsync(
82+
request: request,
83+
response: response,
84+
interceptor: self,
85+
completion: completion
18686
)
18787
}
18888

189-
if
190-
let data = dataLine.data(using: .utf8),
191-
let jsonObject = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject
192-
{
193-
return .json(object: jsonObject)
89+
let errorHandler: ((Error) -> Void) = { parserError in
90+
chain.handleErrorAsync(
91+
parserError,
92+
request: request,
93+
response: response,
94+
completion: completion
95+
)
19496
}
19597

196-
return .unknown
98+
parser.parse(
99+
data: response.rawData,
100+
boundary: boundary,
101+
dataHandler: dataHandler,
102+
errorHandler: errorHandler
103+
)
197104
}
198105
}
199106

200-
fileprivate extension String {
201-
var isBoundaryPrefix: Bool { self == "--" }
107+
/// A protocol that multipart response parsers must conform to in order to be added to the list of
108+
/// available response specification parsers.
109+
protocol MultipartResponseSpecificationParser {
110+
/// The specification string matching what is expected to be received in the `Content-Type` header
111+
/// in an HTTP response.
112+
static var protocolSpec: String { get }
113+
114+
/// Function that will be called to process the response data.
115+
static func parse(
116+
data: Data,
117+
boundary: String,
118+
dataHandler: ((Data) -> Void),
119+
errorHandler: ((Error) -> Void)
120+
)
202121
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import Foundation
2+
#if !COCOAPODS
3+
import ApolloAPI
4+
#endif
5+
6+
struct MultipartResponseSubscriptionParser: MultipartResponseSpecificationParser {
7+
public enum ParsingError: Swift.Error, LocalizedError, Equatable {
8+
case cannotParseResponseData
9+
case unsupportedContentType(type: String)
10+
case cannotParseChunkData
11+
case irrecoverableError(message: String?)
12+
case cannotParsePayloadData
13+
14+
public var errorDescription: String? {
15+
switch self {
16+
case .cannotParseResponseData:
17+
return "The response data could not be parsed."
18+
case let .unsupportedContentType(type):
19+
return "Unsupported content type: application/json is required but got \(type)."
20+
case .cannotParseChunkData:
21+
return "The chunk data could not be parsed."
22+
case let .irrecoverableError(message):
23+
return "An irrecoverable error occured: \(message ?? "unknown")."
24+
case .cannotParsePayloadData:
25+
return "The payload data could not be parsed."
26+
}
27+
}
28+
}
29+
30+
private enum ChunkedDataLine {
31+
case heartbeat
32+
case contentHeader(type: String)
33+
case json(object: JSONObject)
34+
case unknown
35+
}
36+
37+
static let protocolSpec: String = "subscriptionSpec=1.0"
38+
39+
private static let dataLineSeparator: StaticString = "\r\n\r\n"
40+
private static let contentTypeHeader: StaticString = "content-type:"
41+
private static let heartbeat: StaticString = "{}"
42+
43+
static func parse(
44+
data: Data,
45+
boundary: String,
46+
dataHandler: ((Data) -> Void),
47+
errorHandler: ((Error) -> Void)
48+
) {
49+
guard let dataString = String(data: data, encoding: .utf8) else {
50+
errorHandler(ParsingError.cannotParseResponseData)
51+
return
52+
}
53+
54+
for chunk in dataString.components(separatedBy: "--\(boundary)") {
55+
if chunk.isEmpty || chunk.isBoundaryPrefix { continue }
56+
57+
for dataLine in chunk.components(separatedBy: Self.dataLineSeparator.description) {
58+
switch (parse(dataLine: dataLine.trimmingCharacters(in: .newlines))) {
59+
case .heartbeat:
60+
// Periodically sent by the router - noop
61+
continue
62+
63+
case let .contentHeader(type):
64+
guard type == "application/json" else {
65+
errorHandler(ParsingError.unsupportedContentType(type: type))
66+
return
67+
}
68+
69+
case let .json(object):
70+
if let errors = object["errors"] as? [JSONObject] {
71+
let message = errors.first?["message"] as? String
72+
73+
errorHandler(ParsingError.irrecoverableError(message: message))
74+
return
75+
}
76+
77+
guard let payload = object["payload"] else {
78+
errorHandler(ParsingError.cannotParsePayloadData)
79+
return
80+
}
81+
82+
if payload is NSNull {
83+
// `payload` can be null such as in the case of a transport error
84+
continue
85+
}
86+
87+
guard
88+
let payload = payload as? JSONObject,
89+
let data: Data = try? JSONSerializationFormat.serialize(value: payload)
90+
else {
91+
errorHandler(ParsingError.cannotParsePayloadData)
92+
return
93+
}
94+
95+
dataHandler(data)
96+
97+
case .unknown:
98+
errorHandler(ParsingError.cannotParseChunkData)
99+
}
100+
}
101+
}
102+
}
103+
104+
/// Parses the data line of a multipart response chunk
105+
private static func parse(dataLine: String) -> ChunkedDataLine {
106+
if dataLine == Self.heartbeat.description {
107+
return .heartbeat
108+
}
109+
110+
if dataLine.starts(with: Self.contentTypeHeader.description) {
111+
return .contentHeader(type: (dataLine.components(separatedBy: ":").last ?? dataLine)
112+
.trimmingCharacters(in: .whitespaces)
113+
)
114+
}
115+
116+
if
117+
let data = dataLine.data(using: .utf8),
118+
let jsonObject = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject
119+
{
120+
return .json(object: jsonObject)
121+
}
122+
123+
return .unknown
124+
}
125+
}
126+
127+
fileprivate extension String {
128+
var isBoundaryPrefix: Bool { self == "--" }
129+
}

‎Sources/Apollo/RequestChainNetworkTransport.swift

+36-23
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ open class RequestChainNetworkTransport: NetworkTransport {
6969
self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry
7070
}
7171

72-
/// Constructs a default (ie, non-multipart) GraphQL request.
72+
/// Constructs a GraphQL request for the given operation.
7373
///
7474
/// Override this method if you need to use a custom subclass of `HTTPRequest`.
7575
///
@@ -81,18 +81,37 @@ open class RequestChainNetworkTransport: NetworkTransport {
8181
open func constructRequest<Operation: GraphQLOperation>(
8282
for operation: Operation,
8383
cachePolicy: CachePolicy,
84-
contextIdentifier: UUID? = nil) -> HTTPRequest<Operation> {
85-
JSONRequest(operation: operation,
86-
graphQLEndpoint: self.endpointURL,
87-
contextIdentifier: contextIdentifier,
88-
clientName: self.clientName,
89-
clientVersion: self.clientVersion,
90-
additionalHeaders: self.additionalHeaders,
91-
cachePolicy: cachePolicy,
92-
autoPersistQueries: self.autoPersistQueries,
93-
useGETForQueries: self.useGETForQueries,
94-
useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry,
95-
requestBodyCreator: self.requestBodyCreator)
84+
contextIdentifier: UUID? = nil
85+
) -> HTTPRequest<Operation> {
86+
let request = JSONRequest(
87+
operation: operation,
88+
graphQLEndpoint: self.endpointURL,
89+
contextIdentifier: contextIdentifier,
90+
clientName: self.clientName,
91+
clientVersion: self.clientVersion,
92+
additionalHeaders: self.additionalHeaders,
93+
cachePolicy: cachePolicy,
94+
autoPersistQueries: self.autoPersistQueries,
95+
useGETForQueries: self.useGETForQueries,
96+
useGETForPersistedQueryRetry: self.useGETForPersistedQueryRetry,
97+
requestBodyCreator: self.requestBodyCreator
98+
)
99+
100+
if Operation.operationType == .subscription {
101+
request.addHeader(
102+
name: "Accept",
103+
value: "multipart/mixed;boundary=\"graphql\";\(MultipartResponseSubscriptionParser.protocolSpec),application/json"
104+
)
105+
}
106+
107+
if Operation.hasDeferredFragments {
108+
request.addHeader(
109+
name: "Accept",
110+
value: "multipart/mixed;boundary=\"graphql\";\(MultipartResponseDeferParser.protocolSpec),application/json"
111+
)
112+
}
113+
114+
return request
96115
}
97116

98117
// MARK: - NetworkTransport Conformance
@@ -108,16 +127,10 @@ open class RequestChainNetworkTransport: NetworkTransport {
108127
completionHandler: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) -> Cancellable {
109128

110129
let chain = makeChain(operation: operation, callbackQueue: callbackQueue)
111-
let request = self.constructRequest(for: operation,
112-
cachePolicy: cachePolicy,
113-
contextIdentifier: contextIdentifier)
114-
115-
if Operation.operationType == .subscription {
116-
request.addHeader(
117-
name: "Accept",
118-
value: "multipart/mixed;boundary=\"graphql\";subscriptionSpec=1.0,application/json"
119-
)
120-
}
130+
let request = self.constructRequest(
131+
for: operation,
132+
cachePolicy: cachePolicy,
133+
contextIdentifier: contextIdentifier)
121134

122135
chain.kickoff(request: request, completion: completionHandler)
123136
return chain

‎Sources/Apollo/URLSessionClient.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,8 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
271271
taskData.append(additionalData: data)
272272

273273
if let httpResponse = dataTask.response as? HTTPURLResponse, httpResponse.isMultipart {
274-
guard let boundaryString = httpResponse.multipartBoundary else {
274+
let multipartHeaderComponents = httpResponse.multipartHeaderComponents
275+
guard let boundaryString = multipartHeaderComponents.boundary else {
275276
taskData.completionBlock(.failure(URLSessionClientError.missingMultipartBoundary))
276277
return
277278
}

‎Sources/ApolloAPI/GraphQLOperation.swift

+5
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public protocol GraphQLOperation: AnyObject, Hashable {
5959
static var operationName: String { get }
6060
static var operationType: GraphQLOperationType { get }
6161
static var operationDocument: OperationDocument { get }
62+
static var hasDeferredFragments: Bool { get }
6263

6364
var __variables: Variables? { get }
6465

@@ -70,6 +71,10 @@ public extension GraphQLOperation {
7071
return nil
7172
}
7273

74+
static var hasDeferredFragments: Bool {
75+
false
76+
}
77+
7378
static var definition: OperationDefinition? {
7479
operationDocument.definition
7580
}

‎Sources/ApolloAPI/Selection+Conditions.swift

+41-21
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,40 @@ public extension Selection {
3030
}
3131
}
3232

33-
struct Condition: ExpressibleByStringLiteral, Hashable {
34-
public let variableName: String
35-
public let inverted: Bool
33+
enum Condition: ExpressibleByStringLiteral, ExpressibleByBooleanLiteral, Hashable {
34+
case value(Bool)
35+
case variable(name: String, inverted: Bool)
3636

3737
public init(
3838
variableName: String,
3939
inverted: Bool
4040
) {
41-
self.variableName = variableName
42-
self.inverted = inverted;
41+
self = .variable(name: variableName, inverted: inverted)
4342
}
4443

4544
public init(stringLiteral value: StringLiteralType) {
46-
self.variableName = value
47-
self.inverted = false
45+
self = .variable(name: value, inverted: false)
4846
}
4947

50-
@inlinable public static prefix func !(value: Condition) -> Condition {
51-
.init(variableName: value.variableName, inverted: !value.inverted)
48+
public init(booleanLiteral value: BooleanLiteralType) {
49+
self = .value(value)
50+
}
51+
52+
@inlinable public static func `if`(_ condition: StringLiteralType) -> Condition {
53+
.variable(name: condition, inverted: false)
54+
}
55+
56+
@inlinable public static func `if`(_ condition: Condition) -> Condition {
57+
condition
58+
}
59+
60+
@inlinable public static prefix func !(condition: Condition) -> Condition {
61+
switch condition {
62+
case let .value(value):
63+
return .value(!value)
64+
case let .variable(name, inverted):
65+
return .init(variableName: name, inverted: !inverted)
66+
}
5267
}
5368

5469
@inlinable public static func &&(_ lhs: Condition, rhs: Condition) -> [Condition] {
@@ -101,19 +116,24 @@ fileprivate extension Array where Element == Selection.Condition {
101116
// MARK: Conditions - Individual
102117
fileprivate extension Selection.Condition {
103118
func evaluate(with variables: GraphQLOperation.Variables?) -> Bool {
104-
switch variables?[variableName] {
105-
case let boolValue as Bool:
106-
return inverted ? !boolValue : boolValue
107-
108-
case let nullable as GraphQLNullable<Bool>:
109-
let evaluated = nullable.unwrapped ?? false
110-
return inverted ? !evaluated : evaluated
111-
112-
case .none:
113-
return false
119+
switch self {
120+
case let .value(value):
121+
return value
122+
case let .variable(variableName, inverted):
123+
switch variables?[variableName] {
124+
case let boolValue as Bool:
125+
return inverted ? !boolValue : boolValue
126+
127+
case let nullable as GraphQLNullable<Bool>:
128+
let evaluated = nullable.unwrapped ?? false
129+
return inverted ? !evaluated : evaluated
130+
131+
case .none:
132+
return false
114133

115-
case let .some(wrapped):
116-
fatalError("Expected Bool for \(variableName), got \(wrapped)")
134+
case let .some(wrapped):
135+
fatalError("Expected Bool for \(variableName), got \(wrapped)")
136+
}
117137
}
118138
}
119139
}

‎Sources/ApolloAPI/Selection.swift

+7-8
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ public enum Selection {
22
/// A single field selection.
33
case field(Field)
44
/// A fragment spread of a named fragment definition.
5-
case fragment(any Fragment.Type)
5+
case fragment(any Fragment.Type, deferred: Condition? = nil)
66
/// An inline fragment with a child selection set nested in a parent selection set.
7-
case inlineFragment(any InlineFragment.Type)
7+
case inlineFragment(any InlineFragment.Type, deferred: Condition? = nil)
88
/// A group of selections that have `@include/@skip` directives.
99
case conditional(Conditions, [Selection])
1010

@@ -129,14 +129,13 @@ extension Selection: Hashable {
129129
switch (lhs, rhs) {
130130
case let (.field(lhs), .field(rhs)):
131131
return lhs == rhs
132-
case let (.fragment(lhs), .fragment(rhs)):
133-
return lhs == rhs
134-
case let (.inlineFragment(lhs), .inlineFragment(rhs)):
135-
return lhs == rhs
132+
case let (.fragment(lhsFragment, lhsDeferred), .fragment(rhsFragment, rhsDeferred)):
133+
return lhsFragment == rhsFragment && lhsDeferred == rhsDeferred
134+
case let (.inlineFragment(lhsFragment, lhsDeferred), .inlineFragment(rhsFragment, rhsDeferred)):
135+
return lhsFragment == rhsFragment && lhsDeferred == rhsDeferred
136136
case let (.conditional(lhsConditions, lhsSelections),
137137
.conditional(rhsConditions, rhsSelections)):
138-
return lhsConditions == rhsConditions &&
139-
lhsSelections == rhsSelections
138+
return lhsConditions == rhsConditions && lhsSelections == rhsSelections
140139
default: return false
141140
}
142141
}

0 commit comments

Comments
 (0)
Please sign in to comment.