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

@ -19,17 +19,17 @@ public final class Buildkite {
encoder.keyEncodingStrategy = .convertToSnakeCase
return encoder
}()
let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(Formatters.decodeISO8601)
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
var configuration: Configuration
var transport: Transport
public var token: String? {
get {
configuration.token
@ -38,12 +38,12 @@ public final class Buildkite {
configuration.token = newValue
}
}
public init(configuration: Configuration = .default, transport: Transport = URLSession.shared) {
self.configuration = configuration
self.transport = transport
}
private func handleContentfulResponse<Content: Decodable>(completion: @escaping (Result<Response<Content>, Error>) -> Void) -> Transport.Completion {
return { [weak self] result in
guard let self = self else {
@ -63,7 +63,7 @@ public final class Buildkite {
completion(.success(Response(content: content, response: response)))
}
}
private func handleEmptyResponse(completion: @escaping (Result<Response<Void>, Error>) -> Void) -> Transport.Completion {
return { [weak self] result in
guard let self = self else {
@ -80,7 +80,7 @@ public final class Buildkite {
completion(.success(Response(content: (), response: response)))
}
}
private func checkResponseForIssues(_ response: URLResponse, data: Data? = nil) throws {
guard let response = response as? HTTPURLResponse,
let statusCode = StatusCode(rawValue: response.statusCode) else {
@ -101,12 +101,24 @@ public final class Buildkite {
public extension Buildkite {
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))
}
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))
}
@ -133,7 +145,13 @@ public extension Buildkite {
}
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))
}
@ -157,7 +175,9 @@ import Combine
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Buildkite {
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 {
try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data)
@ -165,9 +185,11 @@ extension Buildkite {
}
.eraseToAnyPublisher()
}
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 {
try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data)
@ -175,7 +197,7 @@ extension Buildkite {
}
.eraseToAnyPublisher()
}
public func sendPublisher<R: Resource & HasRequestBody & HasResponseBody>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder) }
.publisher
@ -187,7 +209,7 @@ extension Buildkite {
}
.eraseToAnyPublisher()
}
public func sendPublisher<R: Resource & HasRequestBody & Paginated>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) }
.publisher
@ -199,16 +221,18 @@ extension Buildkite {
}
.eraseToAnyPublisher()
}
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 {
try self.checkResponseForIssues($0.response)
return Response(content: (), response: $0.response)
}
.eraseToAnyPublisher()
}
func sendPublisher<R: Resource & HasRequestBody>(_ resource: R) -> AnyPublisher<Response<Void>, Error> {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder) }
.publisher

View File

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

View File

@ -12,18 +12,18 @@ import Foundation
import FoundationNetworking
#endif
let libraryVersion = "0.0.1"
public struct Configuration {
public let userAgent = "buildkite-swift/\(libraryVersion)"
public let userAgent = "buildkite-swift"
public var version: APIVersion
public var graphQLVersion: APIVersion
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.graphQLVersion = graphQLVersion
}
var token: String?

View File

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

View File

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

View File

@ -40,6 +40,10 @@ public struct GraphQL<T: Codable>: Resource, HasResponseBody, HasRequestBody {
public var body: Body
public var version: APIVersion {
APIVersion.GraphQL.v1
}
public let path = ""
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) {
request.url = APIVersion.GraphQL.v1.url(for: path)
request.httpMethod = "POST"
}
}