Skip to content

Preserves result order #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "swift-collections",
"repositoryURL": "https://github.com/apple/swift-collections",
"state": {
"branch": null,
"revision": "d45e63421d3dff834949ac69d3c37691e994bd69",
"version": "0.0.3"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")),
.package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "0.0.3")),
],
targets: [
.target(
name: "GraphQL",
dependencies: [
.product(name: "NIO", package: "swift-nio"),
.product(name: "OrderedCollections", package: "swift-collections"),
]
),
.testTarget(name: "GraphQLTests", dependencies: ["GraphQL"]),
Expand Down
31 changes: 16 additions & 15 deletions Sources/GraphQL/Execution/Execute.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Dispatch
import NIO
import OrderedCollections

/**
* Terminology
Expand Down Expand Up @@ -97,8 +98,8 @@ public protocol FieldExecutionStrategy {
parentType: GraphQLObjectType,
sourceValue: Any,
path: IndexPath,
fields: [String: [Field]]
) throws -> Future<[String: Any]>
fields: OrderedDictionary<String, [Field]>
) throws -> Future<OrderedDictionary<String, Any>>
}

public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {}
Expand All @@ -117,9 +118,9 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, Mutatio
parentType: GraphQLObjectType,
sourceValue: Any,
path: IndexPath,
fields: [String: [Field]]
) throws -> Future<[String: Any]> {
var results = [String: Future<Any>]()
fields: OrderedDictionary<String, [Field]>
) throws -> Future<OrderedDictionary<String, Any>> {
var results = OrderedDictionary<String, Future<Any>>()

try fields.forEach { field in
let fieldASTs = field.value
Expand Down Expand Up @@ -169,15 +170,16 @@ public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrat
parentType: GraphQLObjectType,
sourceValue: Any,
path: IndexPath,
fields: [String: [Field]]
) throws -> Future<[String: Any]> {
fields: OrderedDictionary<String, [Field]>
) throws -> Future<OrderedDictionary<String, Any>> {
let resultsQueue = DispatchQueue(
label: "\(dispatchQueue.label) results",
qos: dispatchQueue.qos
)

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

fields.forEach { field in
Expand Down Expand Up @@ -213,7 +215,7 @@ public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrat
throw error
}

return results.flatten(on: exeContext.eventLoopGroup)
return results.compactMapValues({ $0 }).flatten(on: exeContext.eventLoopGroup)
}

}
Expand Down Expand Up @@ -323,7 +325,6 @@ func execute(
// errors: executeErrors,
// result: result
// )

return result
}
} catch let error as GraphQLError {
Expand Down Expand Up @@ -417,9 +418,9 @@ func executeOperation(
exeContext: ExecutionContext,
operation: OperationDefinition,
rootValue: Any
) throws -> Future<[String: Any]> {
) throws -> Future<OrderedDictionary<String, Any>> {
let type = try getOperationRootType(schema: exeContext.schema, operation: operation)
var inputFields: [String : [Field]] = [:]
var inputFields: OrderedDictionary<String, [Field]> = [:]
var visitedFragmentNames: [String : Bool] = [:]

let fields = try collectFields(
Expand Down Expand Up @@ -494,9 +495,9 @@ func collectFields(
exeContext: ExecutionContext,
runtimeType: GraphQLObjectType,
selectionSet: SelectionSet,
fields: inout [String: [Field]],
fields: inout OrderedDictionary<String, [Field]>,
visitedFragmentNames: inout [String: Bool]
) throws -> [String: [Field]] {
) throws -> OrderedDictionary<String, [Field]> {
var visitedFragmentNames = visitedFragmentNames

for selection in selectionSet.selections {
Expand Down Expand Up @@ -1109,7 +1110,7 @@ func completeObjectValue(
}

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

for fieldAST in fieldASTs {
Expand Down
65 changes: 51 additions & 14 deletions Sources/GraphQL/Map/Map.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import OrderedCollections

// MARK: MapError

public enum MapError : Error {
Expand All @@ -14,7 +16,7 @@ public enum Map {
case number(Number)
case string(String)
case array([Map])
case dictionary([String: Map])
case dictionary(OrderedDictionary<String, Map>)

public static func int(_ value: Int) -> Map {
return .number(Number(value))
Expand Down Expand Up @@ -58,7 +60,7 @@ extension Map {
self = .array(array)
}

public init(_ dictionary: [String: Map]) {
public init(_ dictionary: OrderedDictionary<String, Map>) {
self = .dictionary(dictionary)
}

Expand Down Expand Up @@ -86,7 +88,7 @@ extension Map {
self = array.map({ Map($0) }) ?? .null
}

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

if
let value = value as? [String: Any],
let dictionary: [String: Map] = try? value.reduce(into: [:], { result, pair in
let value = value as? OrderedDictionary<String, Any>,
let dictionary: OrderedDictionary<String, Map> = try? value.reduce(into: [:], { result, pair in
result[pair.key] = try map(from: pair.value)
})
{
Expand Down Expand Up @@ -152,7 +154,7 @@ extension Map {
self = .string(string)
case let array as [Map]:
self = .array(array)
case let dictionary as [String: Map]:
case let dictionary as OrderedDictionary<String, Map>:
self = .dictionary(dictionary)
default:
throw MapError.incompatibleType
Expand Down Expand Up @@ -243,7 +245,7 @@ extension Map {
return try? arrayValue()
}

public var dictionary: [String: Map]? {
public var dictionary: OrderedDictionary<String, Map>? {
return try? dictionaryValue()
}
}
Expand Down Expand Up @@ -372,7 +374,7 @@ extension Map {
}
}

public func dictionaryValue(converting: Bool = false) throws -> [String: Map] {
public func dictionaryValue(converting: Bool = false) throws -> OrderedDictionary<String, Map> {
guard converting else {
return try get()
}
Expand Down Expand Up @@ -487,7 +489,7 @@ extension Map {
if let existingDictionary = dictionary[key]?.dictionary,
let newDictionary = newValue.dictionary,
merging {
var combinedDictionary: [String: Map] = [:]
var combinedDictionary: OrderedDictionary<String, Map> = [:]

for (key, value) in existingDictionary {
combinedDictionary[key] = value
Expand Down Expand Up @@ -617,8 +619,20 @@ extension Map : Codable {
else if let array = try? container.decode([Map].self) {
self = .array(array)
}

else if let dictionary = try? container.decode([String: Map].self) {

else if let _ = try? container.decode([String: Map].self) {
// Override OrderedDictionary default (unkeyed alternating key-value)
// Instead decode as a keyed container (like normal Dictionary) but use the order of the input
let container = try decoder.container(keyedBy: _DictionaryCodingKey.self)
var orderedDictionary: OrderedDictionary<String, Map> = [:]
for key in container.allKeys {
let value = try container.decode(Map.self, forKey: key)
orderedDictionary[key.stringValue] = value
}
self = .dictionary(orderedDictionary)
}

else if let dictionary = try? container.decode(OrderedDictionary<String, Map>.self) {
self = .dictionary(dictionary)
}

Expand All @@ -642,9 +656,32 @@ extension Map : Codable {
case let .array(array):
try container.encode(array)
case let .dictionary(dictionary):
try container.encode(dictionary)
// Override OrderedDictionary default (unkeyed alternating key-value)
// Instead decode as a keyed container (like normal Dictionary) in the order of our OrderedDictionary
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
for (key, value) in dictionary {
let codingKey = _DictionaryCodingKey(stringValue: key)!
try container.encode(value, forKey: codingKey)
}
}
}

/// A wrapper for dictionary keys which are Strings or Ints.
/// This is copied from Swift core: https://github.com/apple/swift/blob/256a9c5ad96378daa03fa2d5197b4201bf16db27/stdlib/public/core/Codable.swift#L5508
internal struct _DictionaryCodingKey: CodingKey {
internal let stringValue: String
internal let intValue: Int?

internal init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = Int(stringValue)
}

internal init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
}
// MARK: Equatable

Expand Down Expand Up @@ -738,7 +775,7 @@ extension Map : ExpressibleByArrayLiteral {

extension Map : ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, Map)...) {
var dictionary = [String: Map](minimumCapacity: elements.count)
var dictionary = OrderedDictionary<String, Map>(minimumCapacity: elements.count)

for (key, value) in elements {
dictionary[key] = value
Expand Down Expand Up @@ -847,7 +884,7 @@ extension Map {
}
}

func serialize(dictionary: [String: Map]) -> String {
func serialize(dictionary: OrderedDictionary<String, Map>) -> String {
var string = "{"
var index = 0

Expand Down
20 changes: 11 additions & 9 deletions Sources/GraphQL/Map/MapCoder.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import CoreFoundation
import Foundation
import OrderedCollections

/// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary`

/// A marker protocol used to determine whether a value is a `String`-keyed `OrderedDictionary`
/// containing `Encodable` values (in which case it should be exempt from key conversion strategies).
///
fileprivate protocol _MapStringDictionaryEncodableMarker { }

extension Dictionary : _MapStringDictionaryEncodableMarker where Key == String, Value: Encodable { }
extension OrderedDictionary : _MapStringDictionaryEncodableMarker where Key == String, Value: Encodable { }

/// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary`
/// A marker protocol used to determine whether a value is a `String`-keyed `OrderedDictionary`
/// containing `Decodable` values (in which case it should be exempt from key conversion strategies).
///
/// The marker protocol also provides access to the type of the `Decodable` values,
Expand All @@ -18,7 +20,7 @@ fileprivate protocol _MapStringDictionaryDecodableMarker {
static var elementType: Decodable.Type { get }
}

extension Dictionary : _MapStringDictionaryDecodableMarker where Key == String, Value: Decodable {
extension OrderedDictionary : _MapStringDictionaryDecodableMarker where Key == String, Value: Decodable {
static var elementType: Decodable.Type { return Value.self }
}

Expand Down Expand Up @@ -798,7 +800,7 @@ extension _MapEncoder {
}
}

fileprivate func box(_ dict: [String : Encodable]) throws -> NSObject? {
fileprivate func box(_ dict: OrderedDictionary<String, Encodable>) throws -> NSObject? {
let depth = self.storage.count
let result = self.storage.pushKeyedContainer()
do {
Expand Down Expand Up @@ -845,7 +847,7 @@ extension _MapEncoder {
// MapSerialization can consume NSDecimalNumber values.
return NSDecimalNumber(decimal: value as! Decimal)
} else if value is _MapStringDictionaryEncodableMarker {
return try box((value as Any) as! [String : Encodable])
return try box((value as Any) as! OrderedDictionary<String, Encodable>)
}

#else
Expand All @@ -862,7 +864,7 @@ extension _MapEncoder {
// MapSerialization can consume NSDecimalNumber values.
return NSDecimalNumber(decimal: value as! Decimal)
} else if value is _MapStringDictionaryEncodableMarker {
return try box((value as Any) as! [String : Encodable])
return try box((value as Any) as! OrderedDictionary<String, Encodable>)
}
#endif

Expand Down Expand Up @@ -2405,11 +2407,11 @@ extension _MapDecoder {
return Decimal(doubleValue)
}
}

fileprivate func unbox<T>(_ value: Any, as type: _MapStringDictionaryDecodableMarker.Type) throws -> T? {
guard !(value is NSNull) else { return nil }

var result = [String : Any]()
var result: OrderedDictionary<String, Any> = [:]
guard let dict = value as? NSDictionary else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}
Expand Down
15 changes: 10 additions & 5 deletions Sources/GraphQL/Map/MapSerialization.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import OrderedCollections

public struct MapSerialization {
static func map(with object: NSObject) throws -> Map {
Expand All @@ -13,14 +14,13 @@ public struct MapSerialization {
let array: [Map] = try array.map { value in
try self.map(with: value as! NSObject)
}

return .array(array)
case let dictionary as NSDictionary:
let dictionary: [String : Map] = try dictionary.reduce(into: [:]) { (dictionary, pair) in
// Extract from an unordered dictionary, using NSDictionary extraction order
let orderedDictionary: OrderedDictionary<String, Map> = try dictionary.reduce(into: [:]) { (dictionary, pair) in
dictionary[pair.key as! String] = try self.map(with: pair.value as! NSObject)
}

return .dictionary(dictionary)
return .dictionary(orderedDictionary)
default:
throw EncodingError.invalidValue(
object,
Expand All @@ -45,7 +45,12 @@ public struct MapSerialization {
case let .array(array):
return try array.map({ try object(with: $0) }) as NSArray
case let .dictionary(dictionary):
return try dictionary.mapValues({ try object(with: $0) }) as NSDictionary
// Coerce to an unordered dictionary
var unorderedDictionary: [String: NSObject] = [:]
for (key, value) in dictionary {
try unorderedDictionary[key] = object(with: value)
}
return unorderedDictionary as NSDictionary
}
}
}
Loading