Skip to content

Commit b1a6761

Browse files
Merge pull request #628 from apollographql/fix/sorted-keys
Fix type error + Use Sorted Keys Where Possible
2 parents fb05a10 + f3fbfbc commit b1a6761

4 files changed

Lines changed: 93 additions & 2 deletions

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
1111
5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */; };
1212
9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */; };
13+
9BA1244A22D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift */; };
1314
9BDE43D122C6655300FD7C7F /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDE43D022C6655200FD7C7F /* Cancellable.swift */; };
1415
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDE43DC22C6705300FD7C7F /* GraphQLHTTPResponseError.swift */; };
1516
9BDE43DF22C6708600FD7C7F /* GraphQLHTTPRequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDE43DE22C6708600FD7C7F /* GraphQLHTTPRequestError.swift */; };
@@ -252,6 +253,7 @@
252253
90690D2422433C8000FC2E54 /* Apollo-Target-PerformanceTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-PerformanceTests.xcconfig"; sourceTree = "<group>"; };
253254
90690D2522433CAF00FC2E54 /* Apollo-Target-TestSupport.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-TestSupport.xcconfig"; sourceTree = "<group>"; };
254255
9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GETTransformerTests.swift; sourceTree = "<group>"; };
256+
9BA1244922D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialziation+Sorting.swift"; sourceTree = "<group>"; };
255257
9BDE43D022C6655200FD7C7F /* Cancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = "<group>"; };
256258
9BDE43DC22C6705300FD7C7F /* GraphQLHTTPResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPResponseError.swift; sourceTree = "<group>"; };
257259
9BDE43DE22C6708600FD7C7F /* GraphQLHTTPRequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPRequestError.swift; sourceTree = "<group>"; };
@@ -520,6 +522,7 @@
520522
9FC4B91F1D2A6F8D0046A641 /* JSON.swift */,
521523
9FEB050C1DB5732300DA3B44 /* JSONSerializationFormat.swift */,
522524
9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */,
525+
9BA1244922D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift */,
523526
);
524527
name = JSON;
525528
sourceTree = "<group>";
@@ -1118,6 +1121,7 @@
11181121
9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */,
11191122
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */,
11201123
9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */,
1124+
9BA1244A22D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift in Sources */,
11211125
9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */,
11221126
9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */,
11231127
9FEC15B41E681DAD00D461B4 /* Collections.swift in Sources */,

Sources/Apollo/GraphQLGETTransformer.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ struct GraphQLGETTransformer {
4444
queryItems.append(URLQueryItem(name: self.queryKey, value: query))
4545
components.queryItems = queryItems
4646

47-
guard let variables = self.body.jsonObject[self.variablesKey] as? [String: AnyHashable] else {
47+
guard let variables = self.body.jsonObject[self.variablesKey] as? [String: Any] else {
4848
return components.url
4949
}
5050

5151
guard
52-
let serializedData = try? JSONSerialization.data(withJSONObject: variables),
52+
let serializedData = try? JSONSerialization.dataSortedIfPossible(withJSONObject: variables),
5353
let jsonString = String(bytes: serializedData, encoding: .utf8) else {
5454
return components.url
5555
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// JSONSerialziation+Sorting.swift
3+
// Apollo
4+
//
5+
// Created by Ellen Shapiro on 7/12/19.
6+
// Copyright © 2019 Apollo GraphQL. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
extension JSONSerialization {
12+
13+
/// Uses `sortedKeys` to create a stable representation of JSON objects when the operating system supports it.
14+
///
15+
/// - Parameter object: The object to serialize
16+
/// - Returns: The serialized data
17+
/// - Throws: Errors related to the serialization of data.
18+
static func dataSortedIfPossible(withJSONObject object: Any) throws -> Data {
19+
// The `sortedKeys` option is not available on all platforms we
20+
// presently support, but we should use it where we can in
21+
// order to get stable JSON representations, especially if being
22+
// used in queries.
23+
if #available(iOS 11, macOS 13, watchOS 4, tvOS 11, *) {
24+
return try self.data(withJSONObject: object, options: [.sortedKeys])
25+
} else {
26+
return try self.data(withJSONObject: object)
27+
}
28+
}
29+
}

Tests/ApolloTests/GETTransformerTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,64 @@ class GETTransformerTests: XCTestCase {
2828
XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D")
2929
}
3030

31+
func testEncodingQueryWithMoreThanOneParameterIncludingNonHashableValue() {
32+
let operation = HeroNameTypeSpecificConditionalInclusionQuery(episode: .jedi, includeName: true)
33+
let body: GraphQLMap = [
34+
"query": operation.queryDocument,
35+
"variables": operation.variables,
36+
]
37+
38+
let transformer = GraphQLGETTransformer(body: body, url: self.url)
39+
40+
let url = transformer.createGetURL()
41+
42+
if #available(iOS 11, macOS 13, tvOS 11, watchOS 4, *) {
43+
// Here, we know that everything should be encoded in a stable order,
44+
// and we can check the encoded URL string directly.
45+
XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?query=query%20HeroNameTypeSpecificConditionalInclusion($episode:%20Episode,%20$includeName:%20Boolean!)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%20@include(if:%20$includeName)%0A%20%20%20%20...%20on%20Droid%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22JEDI%22,%22includeName%22:true%7D")
46+
} else {
47+
// We can't guarantee order of encoding, so we need to pull the JSON back
48+
// out and check that it has the correct and correctly typed properties.
49+
guard let transformedURL = url else {
50+
XCTFail("URL not created!")
51+
return
52+
}
53+
54+
guard let urlComponents = URLComponents(url: transformedURL, resolvingAgainstBaseURL: false) else {
55+
XCTFail("Couldn't access URL components")
56+
return
57+
}
58+
59+
guard let queryItems = urlComponents.queryItems else {
60+
XCTFail("No query items!")
61+
return
62+
}
63+
64+
65+
guard
66+
let variablesQueryItem = queryItems.first(where: { $0.name == "variables" }),
67+
let variables = variablesQueryItem.value else {
68+
XCTFail("Query items did not contain variables!")
69+
return
70+
}
71+
72+
guard let data = variables.data(using: .utf8) else {
73+
XCTFail("Couldn't convert data to UTF8 string!")
74+
return
75+
}
76+
77+
guard
78+
let object = try? JSONSerialization.jsonObject(with: data),
79+
let dict = object as? [String: Any] else {
80+
XCTFail("Couldn't get dictionary out of json!")
81+
return
82+
}
83+
84+
XCTAssertEqual(dict["includeName"] as? Bool, true)
85+
XCTAssertEqual(dict["episode"] as? String, "JEDI")
86+
}
87+
}
88+
3189
func testEncodingQueryWithNullDefaultParameter() {
3290
let operation = HeroNameQuery()
3391
let body: GraphQLMap = [

0 commit comments

Comments
 (0)