Skip to content
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
4 changes: 4 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */; };
9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */; };
9BA1244A22D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BA1244922D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift */; };
9BDE43D122C6655300FD7C7F /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDE43D022C6655200FD7C7F /* Cancellable.swift */; };
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDE43DC22C6705300FD7C7F /* GraphQLHTTPResponseError.swift */; };
9BDE43DF22C6708600FD7C7F /* GraphQLHTTPRequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDE43DE22C6708600FD7C7F /* GraphQLHTTPRequestError.swift */; };
Expand Down Expand Up @@ -252,6 +253,7 @@
90690D2422433C8000FC2E54 /* Apollo-Target-PerformanceTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-PerformanceTests.xcconfig"; sourceTree = "<group>"; };
90690D2522433CAF00FC2E54 /* Apollo-Target-TestSupport.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-TestSupport.xcconfig"; sourceTree = "<group>"; };
9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GETTransformerTests.swift; sourceTree = "<group>"; };
9BA1244922D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialziation+Sorting.swift"; sourceTree = "<group>"; };
9BDE43D022C6655200FD7C7F /* Cancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = "<group>"; };
9BDE43DC22C6705300FD7C7F /* GraphQLHTTPResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPResponseError.swift; sourceTree = "<group>"; };
9BDE43DE22C6708600FD7C7F /* GraphQLHTTPRequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPRequestError.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -520,6 +522,7 @@
9FC4B91F1D2A6F8D0046A641 /* JSON.swift */,
9FEB050C1DB5732300DA3B44 /* JSONSerializationFormat.swift */,
9F27D4631D40379500715680 /* JSONStandardTypeConversions.swift */,
9BA1244922D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift */,
);
name = JSON;
sourceTree = "<group>";
Expand Down Expand Up @@ -1118,6 +1121,7 @@
9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */,
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */,
9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */,
9BA1244A22D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift in Sources */,
9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */,
9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */,
9FEC15B41E681DAD00D461B4 /* Collections.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Sources/Apollo/GraphQLGETTransformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ struct GraphQLGETTransformer {
queryItems.append(URLQueryItem(name: self.queryKey, value: query))
components.queryItems = queryItems

guard let variables = self.body.jsonObject[self.variablesKey] as? [String: AnyHashable] else {
guard let variables = self.body.jsonObject[self.variablesKey] as? [String: Any] else {
return components.url
}

guard
let serializedData = try? JSONSerialization.data(withJSONObject: variables),
let serializedData = try? JSONSerialization.dataSortedIfPossible(withJSONObject: variables),
let jsonString = String(bytes: serializedData, encoding: .utf8) else {
return components.url
}
Expand Down
29 changes: 29 additions & 0 deletions Sources/Apollo/JSONSerialziation+Sorting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// JSONSerialziation+Sorting.swift
// Apollo
//
// Created by Ellen Shapiro on 7/12/19.
// Copyright © 2019 Apollo GraphQL. All rights reserved.
//

import Foundation

extension JSONSerialization {

/// Uses `sortedKeys` to create a stable representation of JSON objects when the operating system supports it.
///
/// - Parameter object: The object to serialize
/// - Returns: The serialized data
/// - Throws: Errors related to the serialization of data.
static func dataSortedIfPossible(withJSONObject object: Any) throws -> Data {
// The `sortedKeys` option is not available on all platforms we
// presently support, but we should use it where we can in
// order to get stable JSON representations, especially if being
// used in queries.
if #available(iOS 11, macOS 13, watchOS 4, tvOS 11, *) {
return try self.data(withJSONObject: object, options: [.sortedKeys])
} else {
return try self.data(withJSONObject: object)
}
}
}
58 changes: 58 additions & 0 deletions Tests/ApolloTests/GETTransformerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,64 @@ class GETTransformerTests: XCTestCase {
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")
}

func testEncodingQueryWithMoreThanOneParameterIncludingNonHashableValue() {
let operation = HeroNameTypeSpecificConditionalInclusionQuery(episode: .jedi, includeName: true)
let body: GraphQLMap = [
"query": operation.queryDocument,
"variables": operation.variables,
]

let transformer = GraphQLGETTransformer(body: body, url: self.url)

let url = transformer.createGetURL()

if #available(iOS 11, macOS 13, tvOS 11, watchOS 4, *) {
// Here, we know that everything should be encoded in a stable order,
// and we can check the encoded URL string directly.
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")
} else {
// We can't guarantee order of encoding, so we need to pull the JSON back
// out and check that it has the correct and correctly typed properties.
guard let transformedURL = url else {
XCTFail("URL not created!")
return
}

guard let urlComponents = URLComponents(url: transformedURL, resolvingAgainstBaseURL: false) else {
XCTFail("Couldn't access URL components")
return
}

guard let queryItems = urlComponents.queryItems else {
XCTFail("No query items!")
return
}


guard
let variablesQueryItem = queryItems.first(where: { $0.name == "variables" }),
let variables = variablesQueryItem.value else {
XCTFail("Query items did not contain variables!")
return
}

guard let data = variables.data(using: .utf8) else {
XCTFail("Couldn't convert data to UTF8 string!")
return
}

guard
let object = try? JSONSerialization.jsonObject(with: data),
let dict = object as? [String: Any] else {
XCTFail("Couldn't get dictionary out of json!")
return
}

XCTAssertEqual(dict["includeName"] as? Bool, true)
XCTAssertEqual(dict["episode"] as? String, "JEDI")
}
}

func testEncodingQueryWithNullDefaultParameter() {
let operation = HeroNameQuery()
let body: GraphQLMap = [
Expand Down