Skip to content

Commit 3bfde02

Browse files
GET method for ApolloSchemaDownloader (#2010)
* GET method for ApolloSchemaDownloader * Minor improvements to HTTP method enum * Remove ApolloSchemaDownload scope from name * Add documentation * Add HTTP method string constants as output * Add error for unsupported HTTP method when using Apollo Registry * Move HTTP method support into DownloadMethod * Build requests based on DownloadMethod * Add tests for DownloadMethod HTTP method configurations * Clean up and clarify documentation * Add associated values to URL-related errors Co-authored-by: Calvin Cestari <calvin.cestari@gmail.com>
1 parent 9cab672 commit 3bfde02

3 files changed

Lines changed: 202 additions & 42 deletions

File tree

Sources/ApolloCodegenLib/ApolloSchemaDownloadConfiguration.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public struct ApolloSchemaDownloadConfiguration {
99
/// The Apollo Schema Registry, which serves as a central hub for managing your graph.
1010
case apolloRegistry(_ settings: ApolloRegistrySettings)
1111
/// GraphQL Introspection connecting to the specified URL.
12-
case introspection(endpointURL: URL)
12+
case introspection(endpointURL: URL, httpMethod: HTTPMethod = .POST)
1313

1414
public struct ApolloRegistrySettings: Equatable {
1515
/// The API key to use when retrieving your schema from the Apollo Registry.
@@ -33,11 +33,30 @@ public struct ApolloSchemaDownloadConfiguration {
3333
self.variant = variant
3434
}
3535
}
36+
37+
/// The HTTP request method. This is an option on Introspection schema downloads only. Apollo Registry downloads are always
38+
/// POST requests.
39+
public enum HTTPMethod: Equatable, CustomStringConvertible {
40+
/// Use POST for HTTP requests. This is the default for GraphQL.
41+
case POST
42+
/// Use GET for HTTP requests with the GraphQL query being sent in the query string parameter named in
43+
/// `queryParameterName`.
44+
case GET(queryParameterName: String)
45+
46+
public var description: String {
47+
switch self {
48+
case .POST:
49+
return "POST"
50+
case .GET:
51+
return "GET"
52+
}
53+
}
54+
}
3655

3756
public static func == (lhs: DownloadMethod, rhs: DownloadMethod) -> Bool {
3857
switch (lhs, rhs) {
39-
case (.introspection(let lhsURL), introspection(let rhsURL)):
40-
return lhsURL == rhsURL
58+
case (.introspection(let lhsURL, let lhsHTTPMethod), .introspection(let rhsURL, let rhsHTTPMethod)):
59+
return lhsURL == rhsURL && lhsHTTPMethod == rhsHTTPMethod
4160
case (.apolloRegistry(let lhsSettings), .apolloRegistry(let rhsSettings)):
4261
return lhsSettings == rhsSettings
4362
default:

Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public struct ApolloSchemaDownloader {
1313
case couldNotExtractSDLFromRegistryJSON
1414
case couldNotCreateSDLDataToWrite(schema: String)
1515
case couldNotConvertIntrospectionJSONToSDL(underlying: Error)
16+
case couldNotCreateURLComponentsFromEndpointURL(url: URL)
17+
case couldNotGetURLFromURLComponents(components: URLComponents)
1618

1719
public var errorDescription: String? {
1820
switch self {
@@ -29,7 +31,11 @@ public struct ApolloSchemaDownloader {
2931
case .couldNotCreateSDLDataToWrite(let schema):
3032
return "Could not convert SDL schema into data to write to the filesystem. Schema: \(schema)"
3133
case .couldNotConvertIntrospectionJSONToSDL(let underlying):
32-
return "Could not convert downloaded introspection JSON into SDL format. Underlying error: \(underlying)"
34+
return "Could not convert downloaded introspection JSON into SDL format. Underlying error: \(underlying)"
35+
case .couldNotCreateURLComponentsFromEndpointURL(let url):
36+
return "Could not create URLComponents from \(url) for Introspection."
37+
case .couldNotGetURLFromURLComponents(let components):
38+
return "Could not get URL from \(components)."
3339
}
3440
}
3541
}
@@ -43,18 +49,36 @@ public struct ApolloSchemaDownloader {
4349
try FileManager.default.apollo.createContainingFolderIfNeeded(for: configuration.outputURL)
4450

4551
switch configuration.downloadMethod {
46-
case .introspection(let endpointURL):
47-
try self.downloadViaIntrospection(from: endpointURL, configuration: configuration)
52+
case .introspection(let endpointURL, let httpMethod):
53+
try self.downloadViaIntrospection(from: endpointURL, httpMethod: httpMethod, configuration: configuration)
4854
case .apolloRegistry(let settings):
4955
try self.downloadFromRegistry(with: settings, configuration: configuration)
5056
}
5157
}
5258

59+
private static func request(url: URL,
60+
httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod,
61+
headers: [ApolloSchemaDownloadConfiguration.HTTPHeader],
62+
bodyData: Data? = nil) -> URLRequest {
63+
64+
var request = URLRequest(url: url)
65+
66+
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
67+
for header in headers {
68+
request.addValue(header.value, forHTTPHeaderField: header.key)
69+
}
70+
71+
request.httpMethod = String(describing: httpMethod)
72+
request.httpBody = bodyData
73+
74+
return request
75+
}
76+
5377
// MARK: - Schema Registry
5478

5579
static let RegistryEndpoint = URL(string: "https://graphql.api.apollographql.com/api/graphql")!
5680

57-
private static let RegistryDownloadQuery = """
81+
static let RegistryDownloadQuery = """
5882
query DownloadSchema($graphID: ID!, $variant: String!) {
5983
service(id: $graphID) {
6084
variant(name: $variant) {
@@ -74,27 +98,9 @@ public struct ApolloSchemaDownloader {
7498

7599
CodegenLogger.log("Downloading schema from registry", logLevel: .debug)
76100

77-
var variables = [String: String]()
78-
variables["graphID"] = settings.graphID
79-
80-
if let variant = settings.variant {
81-
variables["variant"] = variant
82-
}
83-
84-
let body = UntypedGraphQLRequestBodyCreator.requestBody(for: self.RegistryDownloadQuery,
85-
variables: variables,
86-
operationName: "DownloadSchema")
87-
88-
var urlRequest = URLRequest(url: self.RegistryEndpoint)
89-
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
90-
urlRequest.addValue(settings.apiKey, forHTTPHeaderField: "x-api-key")
91-
for header in configuration.headers {
92-
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
93-
}
94-
urlRequest.httpMethod = "POST"
95-
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.sortedKeys])
101+
let urlRequest = try registryRequest(with: settings, headers: configuration.headers)
96102
let jsonOutputURL = configuration.outputURL.apollo.parentFolderURL().appendingPathComponent("registry_response.json")
97-
103+
98104
try URLDownloader().downloadSynchronously(with: urlRequest,
99105
to: jsonOutputURL,
100106
timeout: configuration.downloadTimeout)
@@ -104,6 +110,31 @@ public struct ApolloSchemaDownloader {
104110
CodegenLogger.log("Successfully downloaded schema from registry", logLevel: .debug)
105111
}
106112

113+
static func registryRequest(with settings: ApolloSchemaDownloadConfiguration.DownloadMethod.ApolloRegistrySettings,
114+
headers: [ApolloSchemaDownloadConfiguration.HTTPHeader]) throws -> URLRequest {
115+
116+
var variables = [String: String]()
117+
variables["graphID"] = settings.graphID
118+
if let variant = settings.variant {
119+
variables["variant"] = variant
120+
}
121+
122+
let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: self.RegistryDownloadQuery,
123+
variables: variables,
124+
operationName: "DownloadSchema")
125+
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])
126+
127+
var allHeaders = headers
128+
allHeaders.append(ApolloSchemaDownloadConfiguration.HTTPHeader(key: "x-api-key", value: settings.apiKey))
129+
130+
let urlRequest = request(url: self.RegistryEndpoint,
131+
httpMethod: .POST,
132+
headers: allHeaders,
133+
bodyData: bodyData)
134+
135+
return urlRequest
136+
}
137+
107138
static func convertFromRegistryJSONToSDLFile(jsonFileURL: URL, configuration: ApolloSchemaDownloadConfiguration) throws {
108139
let jsonData: Data
109140

@@ -143,7 +174,7 @@ public struct ApolloSchemaDownloader {
143174

144175
// MARK: - Schema Introspection
145176

146-
private static let IntrospectionQuery = """
177+
static let IntrospectionQuery = """
147178
query IntrospectionQuery {
148179
__schema {
149180
queryType { name }
@@ -235,21 +266,13 @@ public struct ApolloSchemaDownloader {
235266
"""
236267

237268

238-
static func downloadViaIntrospection(from endpointURL: URL, configuration: ApolloSchemaDownloadConfiguration) throws {
269+
static func downloadViaIntrospection(from endpointURL: URL,
270+
httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod,
271+
configuration: ApolloSchemaDownloadConfiguration) throws {
272+
239273
CodegenLogger.log("Downloading schema via introspection from \(endpointURL)", logLevel: .debug)
240-
241-
var urlRequest = URLRequest(url: endpointURL)
242-
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
243274

244-
for header in configuration.headers {
245-
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
246-
}
247-
248-
let body = UntypedGraphQLRequestBodyCreator.requestBody(for: self.IntrospectionQuery,
249-
variables: nil,
250-
operationName: "IntrospectionQuery")
251-
urlRequest.httpMethod = "POST"
252-
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.sortedKeys])
275+
let urlRequest = try introspectionRequest(from: endpointURL, httpMethod: httpMethod, headers: configuration.headers)
253276
let jsonOutputURL = configuration.outputURL.apollo.parentFolderURL().appendingPathComponent("introspection_response.json")
254277

255278
try URLDownloader().downloadSynchronously(with: urlRequest,
@@ -260,7 +283,38 @@ public struct ApolloSchemaDownloader {
260283

261284
CodegenLogger.log("Successfully downloaded schema via introspection", logLevel: .debug)
262285
}
263-
286+
287+
static func introspectionRequest(from endpointURL: URL,
288+
httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod,
289+
headers: [ApolloSchemaDownloadConfiguration.HTTPHeader]) throws -> URLRequest {
290+
let urlRequest: URLRequest
291+
292+
switch httpMethod {
293+
case .POST:
294+
let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: self.IntrospectionQuery,
295+
variables: nil,
296+
operationName: "IntrospectionQuery")
297+
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])
298+
urlRequest = request(url: endpointURL,
299+
httpMethod: httpMethod,
300+
headers: headers,
301+
bodyData: bodyData)
302+
303+
case .GET(let queryParameterName):
304+
guard var components = URLComponents(url: endpointURL, resolvingAgainstBaseURL: true) else {
305+
throw SchemaDownloadError.couldNotCreateURLComponentsFromEndpointURL(url: endpointURL)
306+
}
307+
components.queryItems = [URLQueryItem(name: queryParameterName, value: IntrospectionQuery)]
308+
309+
guard let url = components.url else {
310+
throw SchemaDownloadError.couldNotGetURLFromURLComponents(components: components)
311+
}
312+
urlRequest = request(url: url, httpMethod: httpMethod, headers: headers)
313+
}
314+
315+
return urlRequest
316+
}
317+
264318
static func convertFromIntrospectionJSONToSDLFile(jsonFileURL: URL, configuration: ApolloSchemaDownloadConfiguration) throws {
265319
let frontend = try ApolloCodegenFrontend()
266320
let schema: GraphQLSchema

Tests/ApolloCodegenTests/ApolloSchemaInternalTests.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,92 @@ class ApolloSchemaInternalTests: XCTestCase {
3535

3636
XCTAssertEqual(downloadConfiguration.outputURL, codegenOptions.urlToSchemaFile)
3737
}
38+
39+
func testRequest_givenIntrospectionGETDownload_shouldOutputGETRequest() throws {
40+
let url = ApolloTestSupport.TestURL.mockServer.url
41+
let queryParameterName = "customParam"
42+
let headers: [ApolloSchemaDownloadConfiguration.HTTPHeader] = [
43+
.init(key: "key1", value: "value1"),
44+
.init(key: "key2", value: "value2")
45+
]
46+
47+
let request = try ApolloSchemaDownloader.introspectionRequest(from: url,
48+
httpMethod: .GET(queryParameterName: queryParameterName),
49+
headers: headers)
50+
51+
XCTAssertEqual(request.httpMethod, "GET")
52+
XCTAssertNil(request.httpBody)
53+
54+
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
55+
for header in headers {
56+
XCTAssertEqual(request.allHTTPHeaderFields?[header.key], header.value)
57+
}
58+
59+
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
60+
components?.queryItems = [URLQueryItem(name: queryParameterName, value: ApolloSchemaDownloader.IntrospectionQuery)]
61+
62+
XCTAssertNotNil(components?.url)
63+
XCTAssertEqual(request.url, components?.url)
64+
}
65+
66+
func testRequest_givenIntrospectionPOSTDownload_shouldOutputPOSTRequest() throws {
67+
let url = ApolloTestSupport.TestURL.mockServer.url
68+
let headers: [ApolloSchemaDownloadConfiguration.HTTPHeader] = [
69+
.init(key: "key1", value: "value1"),
70+
.init(key: "key2", value: "value2")
71+
]
72+
73+
let request = try ApolloSchemaDownloader.introspectionRequest(from: url, httpMethod: .POST, headers: headers)
74+
75+
XCTAssertEqual(request.httpMethod, "POST")
76+
XCTAssertEqual(request.url, url)
77+
78+
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
79+
for header in headers {
80+
XCTAssertEqual(request.allHTTPHeaderFields?[header.key], header.value)
81+
}
82+
83+
let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: ApolloSchemaDownloader.IntrospectionQuery,
84+
variables: nil,
85+
operationName: "IntrospectionQuery")
86+
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])
87+
88+
XCTAssertEqual(request.httpBody, bodyData)
89+
}
90+
91+
func testRequest_givenRegistryDownload_shouldOutputPOSTRequest() throws {
92+
let apiKey = "custom-api-key"
93+
let graphID = "graph-id"
94+
let variant = "a-variant"
95+
let headers: [ApolloSchemaDownloadConfiguration.HTTPHeader] = [
96+
.init(key: "key1", value: "value1"),
97+
.init(key: "key2", value: "value2"),
98+
]
99+
100+
let request = try ApolloSchemaDownloader.registryRequest(with: .init(apiKey: apiKey,
101+
graphID: graphID,
102+
variant: variant),
103+
headers: headers)
104+
105+
XCTAssertEqual(request.httpMethod, "POST")
106+
XCTAssertEqual(request.url, ApolloSchemaDownloader.RegistryEndpoint)
107+
108+
XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
109+
XCTAssertEqual(request.allHTTPHeaderFields?["x-api-key"], apiKey)
110+
for header in headers {
111+
XCTAssertEqual(request.allHTTPHeaderFields?[header.key], header.value)
112+
}
113+
114+
let variables: [String: String] = [
115+
"graphID": graphID,
116+
"variant": variant
117+
]
118+
let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: ApolloSchemaDownloader.RegistryDownloadQuery,
119+
variables: variables,
120+
operationName: "DownloadSchema")
121+
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])
122+
123+
XCTAssertEqual(request.httpBody, bodyData)
124+
}
38125
}
39126

0 commit comments

Comments
 (0)