Skip to content

Commit 83d2c88

Browse files
Preserves result order
Fixes GraphQLSwift#60
1 parent 58788b8 commit 83d2c88

File tree

9 files changed

+210
-45
lines changed

9 files changed

+210
-45
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
22
"object": {
33
"pins": [
4+
{
5+
"package": "swift-collections",
6+
"repositoryURL": "https://github.com/apple/swift-collections",
7+
"state": {
8+
"branch": null,
9+
"revision": "d45e63421d3dff834949ac69d3c37691e994bd69",
10+
"version": "0.0.3"
11+
}
12+
},
413
{
514
"package": "swift-nio",
615
"repositoryURL": "https://github.com/apple/swift-nio.git",

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ let package = Package(
88
],
99
dependencies: [
1010
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")),
11+
.package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "0.0.3")),
1112
],
1213
targets: [
1314
.target(
1415
name: "GraphQL",
1516
dependencies: [
1617
.product(name: "NIO", package: "swift-nio"),
18+
.product(name: "OrderedCollections", package: "swift-collections"),
1719
]
1820
),
1921
.testTarget(name: "GraphQLTests", dependencies: ["GraphQL"]),

Sources/GraphQL/Execution/Execute.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Dispatch
22
import NIO
3+
import OrderedCollections
34

45
/**
56
* Terminology
@@ -97,8 +98,8 @@ public protocol FieldExecutionStrategy {
9798
parentType: GraphQLObjectType,
9899
sourceValue: Any,
99100
path: IndexPath,
100-
fields: [String: [Field]]
101-
) throws -> Future<[String: Any]>
101+
fields: OrderedDictionary<String, [Field]>
102+
) throws -> Future<OrderedDictionary<String, Any>>
102103
}
103104

104105
public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {}
@@ -117,9 +118,9 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, Mutatio
117118
parentType: GraphQLObjectType,
118119
sourceValue: Any,
119120
path: IndexPath,
120-
fields: [String: [Field]]
121-
) throws -> Future<[String: Any]> {
122-
var results = [String: Future<Any>]()
121+
fields: OrderedDictionary<String, [Field]>
122+
) throws -> Future<OrderedDictionary<String, Any>> {
123+
var results = OrderedDictionary<String, Future<Any>>()
123124

124125
try fields.forEach { field in
125126
let fieldASTs = field.value
@@ -169,15 +170,16 @@ public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrat
169170
parentType: GraphQLObjectType,
170171
sourceValue: Any,
171172
path: IndexPath,
172-
fields: [String: [Field]]
173-
) throws -> Future<[String: Any]> {
173+
fields: OrderedDictionary<String, [Field]>
174+
) throws -> Future<OrderedDictionary<String, Any>> {
174175
let resultsQueue = DispatchQueue(
175176
label: "\(dispatchQueue.label) results",
176177
qos: dispatchQueue.qos
177178
)
178179

179180
let group = DispatchGroup()
180-
var results: [String: Future<Any>] = [:]
181+
// preserve field order by assigning to null and filtering later
182+
var results: OrderedDictionary<String, Future<Any>?> = fields.mapValues { _ -> Future<Any>? in return nil }
181183
var err: Error? = nil
182184

183185
fields.forEach { field in
@@ -213,7 +215,7 @@ public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrat
213215
throw error
214216
}
215217

216-
return results.flatten(on: exeContext.eventLoopGroup)
218+
return results.compactMapValues({ $0 }).flatten(on: exeContext.eventLoopGroup)
217219
}
218220

219221
}
@@ -323,7 +325,6 @@ func execute(
323325
// errors: executeErrors,
324326
// result: result
325327
// )
326-
327328
return result
328329
}
329330
} catch let error as GraphQLError {
@@ -417,9 +418,9 @@ func executeOperation(
417418
exeContext: ExecutionContext,
418419
operation: OperationDefinition,
419420
rootValue: Any
420-
) throws -> Future<[String: Any]> {
421+
) throws -> Future<OrderedDictionary<String, Any>> {
421422
let type = try getOperationRootType(schema: exeContext.schema, operation: operation)
422-
var inputFields: [String : [Field]] = [:]
423+
var inputFields: OrderedDictionary<String, [Field]> = [:]
423424
var visitedFragmentNames: [String : Bool] = [:]
424425

425426
let fields = try collectFields(
@@ -494,9 +495,9 @@ func collectFields(
494495
exeContext: ExecutionContext,
495496
runtimeType: GraphQLObjectType,
496497
selectionSet: SelectionSet,
497-
fields: inout [String: [Field]],
498+
fields: inout OrderedDictionary<String, [Field]>,
498499
visitedFragmentNames: inout [String: Bool]
499-
) throws -> [String: [Field]] {
500+
) throws -> OrderedDictionary<String, [Field]> {
500501
var visitedFragmentNames = visitedFragmentNames
501502

502503
for selection in selectionSet.selections {
@@ -1109,7 +1110,7 @@ func completeObjectValue(
11091110
}
11101111

11111112
// Collect sub-fields to execute to complete this value.
1112-
var subFieldASTs: [String: [Field]] = [:]
1113+
var subFieldASTs: OrderedDictionary<String, [Field]> = [:]
11131114
var visitedFragmentNames: [String: Bool] = [:]
11141115

11151116
for fieldAST in fieldASTs {

Sources/GraphQL/Map/Map.swift

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import OrderedCollections
2+
13
// MARK: MapError
24

35
public enum MapError : Error {
@@ -14,7 +16,7 @@ public enum Map {
1416
case number(Number)
1517
case string(String)
1618
case array([Map])
17-
case dictionary([String: Map])
19+
case dictionary(OrderedDictionary<String, Map>)
1820

1921
public static func int(_ value: Int) -> Map {
2022
return .number(Number(value))
@@ -58,7 +60,7 @@ extension Map {
5860
self = .array(array)
5961
}
6062

61-
public init(_ dictionary: [String: Map]) {
63+
public init(_ dictionary: OrderedDictionary<String, Map>) {
6264
self = .dictionary(dictionary)
6365
}
6466

@@ -86,7 +88,7 @@ extension Map {
8688
self = array.map({ Map($0) }) ?? .null
8789
}
8890

89-
public init(_ dictionary: [String: Map]?) {
91+
public init(_ dictionary: OrderedDictionary<String, Map>?) {
9092
self = dictionary.map({ Map($0) }) ?? .null
9193
}
9294
}
@@ -107,8 +109,8 @@ public func map(from value: Any?) throws -> Map {
107109
}
108110

109111
if
110-
let value = value as? [String: Any],
111-
let dictionary: [String: Map] = try? value.reduce(into: [:], { result, pair in
112+
let value = value as? OrderedDictionary<String, Any>,
113+
let dictionary: OrderedDictionary<String, Map> = try? value.reduce(into: [:], { result, pair in
112114
result[pair.key] = try map(from: pair.value)
113115
})
114116
{
@@ -152,7 +154,7 @@ extension Map {
152154
self = .string(string)
153155
case let array as [Map]:
154156
self = .array(array)
155-
case let dictionary as [String: Map]:
157+
case let dictionary as OrderedDictionary<String, Map>:
156158
self = .dictionary(dictionary)
157159
default:
158160
throw MapError.incompatibleType
@@ -243,7 +245,7 @@ extension Map {
243245
return try? arrayValue()
244246
}
245247

246-
public var dictionary: [String: Map]? {
248+
public var dictionary: OrderedDictionary<String, Map>? {
247249
return try? dictionaryValue()
248250
}
249251
}
@@ -372,7 +374,7 @@ extension Map {
372374
}
373375
}
374376

375-
public func dictionaryValue(converting: Bool = false) throws -> [String: Map] {
377+
public func dictionaryValue(converting: Bool = false) throws -> OrderedDictionary<String, Map> {
376378
guard converting else {
377379
return try get()
378380
}
@@ -487,7 +489,7 @@ extension Map {
487489
if let existingDictionary = dictionary[key]?.dictionary,
488490
let newDictionary = newValue.dictionary,
489491
merging {
490-
var combinedDictionary: [String: Map] = [:]
492+
var combinedDictionary: OrderedDictionary<String, Map> = [:]
491493

492494
for (key, value) in existingDictionary {
493495
combinedDictionary[key] = value
@@ -617,8 +619,20 @@ extension Map : Codable {
617619
else if let array = try? container.decode([Map].self) {
618620
self = .array(array)
619621
}
620-
621-
else if let dictionary = try? container.decode([String: Map].self) {
622+
623+
else if let _ = try? container.decode([String: Map].self) {
624+
// Override OrderedDictionary default (unkeyed alternating key-value)
625+
// Instead decode as a keyed container (like normal Dictionary) but use the order of the input
626+
let container = try decoder.container(keyedBy: _DictionaryCodingKey.self)
627+
var orderedDictionary: OrderedDictionary<String, Map> = [:]
628+
for key in container.allKeys {
629+
let value = try container.decode(Map.self, forKey: key)
630+
orderedDictionary[key.stringValue] = value
631+
}
632+
self = .dictionary(orderedDictionary)
633+
}
634+
635+
else if let dictionary = try? container.decode(OrderedDictionary<String, Map>.self) {
622636
self = .dictionary(dictionary)
623637
}
624638

@@ -642,9 +656,32 @@ extension Map : Codable {
642656
case let .array(array):
643657
try container.encode(array)
644658
case let .dictionary(dictionary):
645-
try container.encode(dictionary)
659+
// Override OrderedDictionary default (unkeyed alternating key-value)
660+
// Instead decode as a keyed container (like normal Dictionary) in the order of our OrderedDictionary
661+
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
662+
for (key, value) in dictionary {
663+
let codingKey = _DictionaryCodingKey(stringValue: key)!
664+
try container.encode(value, forKey: codingKey)
665+
}
646666
}
647667
}
668+
669+
/// A wrapper for dictionary keys which are Strings or Ints.
670+
/// This is copied from Swift core: https://github.com/apple/swift/blob/256a9c5ad96378daa03fa2d5197b4201bf16db27/stdlib/public/core/Codable.swift#L5508
671+
internal struct _DictionaryCodingKey: CodingKey {
672+
internal let stringValue: String
673+
internal let intValue: Int?
674+
675+
internal init?(stringValue: String) {
676+
self.stringValue = stringValue
677+
self.intValue = Int(stringValue)
678+
}
679+
680+
internal init?(intValue: Int) {
681+
self.stringValue = "\(intValue)"
682+
self.intValue = intValue
683+
}
684+
}
648685
}
649686
// MARK: Equatable
650687

@@ -738,7 +775,7 @@ extension Map : ExpressibleByArrayLiteral {
738775

739776
extension Map : ExpressibleByDictionaryLiteral {
740777
public init(dictionaryLiteral elements: (String, Map)...) {
741-
var dictionary = [String: Map](minimumCapacity: elements.count)
778+
var dictionary = OrderedDictionary<String, Map>(minimumCapacity: elements.count)
742779

743780
for (key, value) in elements {
744781
dictionary[key] = value
@@ -847,7 +884,7 @@ extension Map {
847884
}
848885
}
849886

850-
func serialize(dictionary: [String: Map]) -> String {
887+
func serialize(dictionary: OrderedDictionary<String, Map>) -> String {
851888
var string = "{"
852889
var index = 0
853890

0 commit comments

Comments
 (0)