Resources now declare their required API versions, making GraphQL support a little cleaner

This commit is contained in:
Aaron Sky 2020-05-19 08:42:25 -04:00
parent e4ae58b14b
commit 135510dcb9
6 changed files with 69 additions and 30 deletions

View File

@ -101,12 +101,24 @@ public final class Buildkite {
public extension Buildkite { public extension Buildkite {
func send<R: Resource & HasResponseBody>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) { func send<R: Resource & HasResponseBody>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) {
let request = URLRequest(resource, configuration: configuration) let request: URLRequest
do {
request = try URLRequest(resource, configuration: configuration)
} catch {
completion(.failure(error))
return
}
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} }
func send<R: Resource & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) { func send<R: Resource & Paginated>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) {
let request = URLRequest(resource, configuration: configuration, pageOptions: pageOptions) let request: URLRequest
do {
request = try URLRequest(resource, configuration: configuration, pageOptions: pageOptions)
} catch {
completion(.failure(error))
return
}
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} }
@ -133,7 +145,13 @@ public extension Buildkite {
} }
func send<R: Resource>(_ resource: R, completion: @escaping (Result<Response<Void>, Error>) -> Void) { func send<R: Resource>(_ resource: R, completion: @escaping (Result<Response<Void>, Error>) -> Void) {
let request = URLRequest(resource, configuration: configuration) let request: URLRequest
do {
request = try URLRequest(resource, configuration: configuration)
} catch {
completion(.failure(error))
return
}
transport.send(request: request, completion: handleEmptyResponse(completion: completion)) transport.send(request: request, completion: handleEmptyResponse(completion: completion))
} }
@ -157,7 +175,9 @@ import Combine
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Buildkite { extension Buildkite {
public func sendPublisher<R: Resource & HasResponseBody>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> { public func sendPublisher<R: Resource & HasResponseBody>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> {
transport.sendPublisher(request: URLRequest(resource, configuration: configuration)) Result { try URLRequest(resource, configuration: configuration) }
.publisher
.flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response, data: $0.data) try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data)
@ -167,7 +187,9 @@ extension Buildkite {
} }
public func sendPublisher<R: Resource & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> { public func sendPublisher<R: Resource & HasResponseBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> {
transport.sendPublisher(request: URLRequest(resource, configuration: configuration, pageOptions: pageOptions)) Result { try URLRequest(resource, configuration: configuration, pageOptions: pageOptions) }
.publisher
.flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response, data: $0.data) try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data)
@ -201,7 +223,9 @@ extension Buildkite {
} }
public func sendPublisher<R: Resource>(_ resource: R) -> AnyPublisher<Response<Void>, Error> { public func sendPublisher<R: Resource>(_ resource: R) -> AnyPublisher<Response<Void>, Error> {
transport.sendPublisher(request: URLRequest(resource, configuration: configuration)) Result { try URLRequest(resource, configuration: configuration) }
.publisher
.flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response) try self.checkResponseForIssues($0.response)
return Response(content: (), response: $0.response) return Response(content: (), response: $0.response)

View File

@ -12,7 +12,7 @@ import Foundation
import FoundationNetworking import FoundationNetworking
#endif #endif
public struct APIVersion { public struct APIVersion: Equatable {
public enum REST { public enum REST {
private static let baseURL = URL(string: "https://api.buildkite.com")! private static let baseURL = URL(string: "https://api.buildkite.com")!
public static let v2 = APIVersion(baseURL: baseURL, version: "v2") public static let v2 = APIVersion(baseURL: baseURL, version: "v2")

View File

@ -12,18 +12,18 @@ import Foundation
import FoundationNetworking import FoundationNetworking
#endif #endif
let libraryVersion = "0.0.1"
public struct Configuration { public struct Configuration {
public let userAgent = "buildkite-swift/\(libraryVersion)" public let userAgent = "buildkite-swift"
public var version: APIVersion public var version: APIVersion
public var graphQLVersion: APIVersion
public static var `default`: Configuration { public static var `default`: Configuration {
.init(version: APIVersion.REST.v2) .init(version: APIVersion.REST.v2, graphQLVersion: APIVersion.GraphQL.v1)
} }
public init(version: APIVersion) { public init(version: APIVersion = APIVersion.REST.v2, graphQLVersion: APIVersion = APIVersion.GraphQL.v1) {
self.version = version self.version = version
self.graphQLVersion = graphQLVersion
} }
var token: String? var token: String?

View File

@ -14,11 +14,16 @@ import FoundationNetworking
public protocol Resource { public protocol Resource {
associatedtype Content associatedtype Content
var version: APIVersion { get }
var path: String { get } var path: String { get }
func transformRequest(_ request: inout URLRequest) func transformRequest(_ request: inout URLRequest)
} }
extension Resource { extension Resource {
public var version: APIVersion {
APIVersion.REST.v2
}
public func transformRequest(_ request: inout URLRequest) {} public func transformRequest(_ request: inout URLRequest) {}
} }
@ -34,8 +39,14 @@ public protocol HasResponseBody {
public protocol Paginated: HasResponseBody {} public protocol Paginated: HasResponseBody {}
extension URLRequest { extension URLRequest {
init<R: Resource>(_ resource: R, configuration: Configuration) { init<R: Resource>(_ resource: R, configuration: Configuration) throws {
let url = configuration.version.url(for: resource.path) let version = resource.version
guard version == configuration.version
|| version == configuration.graphQLVersion else {
throw ResponseError.incompatibleVersion
}
let url = version.url(for: resource.path)
var request = URLRequest(url: url) var request = URLRequest(url: url)
configuration.transformRequest(&request) configuration.transformRequest(&request)
resource.transformRequest(&request) resource.transformRequest(&request)
@ -43,12 +54,12 @@ extension URLRequest {
} }
init<R: Resource & HasRequestBody>(_ resource: R, configuration: Configuration, encoder: JSONEncoder) throws { init<R: Resource & HasRequestBody>(_ resource: R, configuration: Configuration, encoder: JSONEncoder) throws {
self.init(resource, configuration: configuration) try self.init(resource, configuration: configuration)
httpBody = try encoder.encode(resource.body) httpBody = try encoder.encode(resource.body)
} }
init<R: Resource & Paginated>(_ resource: R, configuration: Configuration, pageOptions: PageOptions? = nil) { init<R: Resource & Paginated>(_ resource: R, configuration: Configuration, pageOptions: PageOptions? = nil) throws {
self.init(resource, configuration: configuration) try self.init(resource, configuration: configuration)
if let options = pageOptions { if let options = pageOptions {
appendPageOptions(options) appendPageOptions(options)
} }

View File

@ -13,6 +13,7 @@ import FoundationNetworking
#endif #endif
enum ResponseError: Error { enum ResponseError: Error {
case incompatibleVersion
case missingResponse case missingResponse
case unexpectedlyNoContent case unexpectedlyNoContent
} }

View File

@ -40,6 +40,10 @@ public struct GraphQL<T: Codable>: Resource, HasResponseBody, HasRequestBody {
public var body: Body public var body: Body
public var version: APIVersion {
APIVersion.GraphQL.v1
}
public let path = "" public let path = ""
public init(rawQuery query: String, variables: [String: JSONValue] = [:]) { public init(rawQuery query: String, variables: [String: JSONValue] = [:]) {
@ -47,7 +51,6 @@ public struct GraphQL<T: Codable>: Resource, HasResponseBody, HasRequestBody {
} }
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
request.url = APIVersion.GraphQL.v1.url(for: path)
request.httpMethod = "POST" request.httpMethod = "POST"
} }
} }