Support for the /meta endpoint

This commit is contained in:
Aaron Sky 2022-06-04 19:49:11 -04:00
parent 587811d65e
commit eeeb1ae194
54 changed files with 1614 additions and 1200 deletions

4
.gitignore vendored
View File

@ -1,6 +1,8 @@
.DS_Store
/.build
/Example/.build
/.vscode
/Examples/.build
/Packages
/*.xcodeproj
xcuserdata/
Package.resolved

56
.swift-format.json Normal file
View File

@ -0,0 +1,56 @@
{
"fileScopedDeclarationPrivacy": {
"accessLevel": "private"
},
"indentation": {
"spaces": 4
},
"indentConditionalCompilationBlocks": false,
"indentSwitchCaseLabels": false,
"lineBreakAroundMultilineExpressionChainComponents": true,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": true,
"lineBreakBeforeEachGenericRequirement": false,
"lineLength": 120,
"maximumBlankLines": 1,
"prioritizeKeepingFunctionOutputTogether": true,
"respectsExistingLineBreaks": true,
"rules": {
"AllPublicDeclarationsHaveDocumentation": false,
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"BeginDocumentationCommentWithOneLineSummary": false,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileScopedDeclarationPrivacy": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
"NeverForceUnwrap": false,
"NeverUseForceTry": false,
"NeverUseImplicitlyUnwrappedOptionals": false,
"NoAccessLevelOnExtensionDeclaration": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
"NoEmptyTrailingClosureParentheses": true,
"NoLabelsInCasePatterns": true,
"NoLeadingUnderscores": false,
"NoParensAroundConditions": true,
"NoVoidReturnOnFunctionSignature": true,
"OneCasePerLine": true,
"OneVariableDeclarationPerLine": true,
"OnlyOneTrailingClosureArgument": true,
"OrderedImports": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"UseEarlyExits": true,
"UseLetInEveryBoundCaseVariable": true,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"UseSynthesizedInitializer": true,
"UseTripleSlashForDocumentationComments": true,
"UseWhereClausesInForLoops": true,
"ValidateDocumentationComments": true
},
"tabWidth": 8,
"version": 1
}

View File

@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "container:../..">
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,69 @@
//
// main.swift
// graphql
//
// Created by Aaron Sky on 5/5/20.
//
#if compiler(>=5.5.2) && canImport(_Concurrency)
import Foundation
import Combine
import Buildkite
struct MyPipeline: Codable {
var organization: Organization?
struct Organization: Codable {
var pipelines: Pipelines
struct Pipelines: Codable {
var edges: [PipelineEdge]
struct PipelineEdge: Codable {
var node: Pipeline
struct Pipeline: Codable {
var name: String
var uuid: UUID
}
}
}
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
@main struct GraphQLExample {
static func main() async {
let client = BuildkiteClient()
client.token = "..."
let query = """
query MyPipelines($first: Int!) {
organization(slug: "buildkite") {
pipelines(first: $first) {
edges {
node {
name
uuid
}
}
}
}
}
"""
do {
let pipelines = try await client.sendQuery(GraphQL<MyPipeline>(
rawQuery: query,
variables: ["first": 30]
))
print(pipelines)
} catch {
print(error)
exit(1)
}
}
}
#endif

View File

@ -1,62 +0,0 @@
//
// main.swift
// graphql
//
// Created by Aaron Sky on 5/5/20.
//
import Foundation
import Combine
import Buildkite
let client = BuildkiteClient()
client.token = "..."
let query = """
query MyPipelines($first: Int!) {
organization(slug: "buildkite") {
pipelines(first: $first) {
edges {
node {
name
uuid
}
}
}
}
}
"""
struct MyPipeline: Codable {
var organization: Organization?
struct Organization: Codable {
var pipelines: Pipelines
struct Pipelines: Codable {
var edges: [PipelineEdge]
struct PipelineEdge: Codable {
var node: Pipeline
struct Pipeline: Codable {
var name: String
var uuid: UUID
}
}
}
}
}
client.sendQuery(GraphQL<MyPipeline>(rawQuery: query, variables: ["first": 30])) { result in
do {
let pipelines = try result.get()
print(pipelines)
exit(0)
} catch {
print(error)
exit(1)
}
}
RunLoop.main.run()

30
Makefile Normal file
View File

@ -0,0 +1,30 @@
SWIFT_FORMAT_BIN := swift format
GIT_REPO_TOPLEVEL := $(shell git rev-parse --show-toplevel)
SWIFT_FORMAT_CONFIG_FILE := $(GIT_REPO_TOPLEVEL)/.swift-format.json
format:
$(SWIFT_FORMAT_BIN) \
--configuration $(SWIFT_FORMAT_CONFIG_FILE) \
--ignore-unparsable-files \
--in-place \
--recursive \
$(GIT_REPO_TOPLEVEL)/Package.swift $(GIT_REPO_TOPLEVEL)/Sources $(GIT_REPO_TOPLEVEL)/Tests
lint:
$(SWIFT_FORMAT_BIN) lint \
--configuration $(SWIFT_FORMAT_CONFIG_FILE) \
--ignore-unparsable-files \
--recursive \
$(GIT_REPO_TOPLEVEL)/Package.swift $(GIT_REPO_TOPLEVEL)/Sources $(GIT_REPO_TOPLEVEL)/Tests
test:
swift test --parallel --enable-code-coverage
coverage: test
xcrun llvm-cov export \
-format=lcov \
-instr-profile=.build/arm64-apple-macosx/debug/codecov/default.profdata \
.build/arm64-apple-macosx/debug/BuildkitePackageTests.xctest/Contents/MacOS/BuildkitePackageTests \
> lcov.info
.PHONY: format lint test coverage

View File

@ -8,12 +8,13 @@ let package = Package(
.iOS(.v10),
.macOS(.v10_12),
.tvOS(.v10),
.watchOS(.v3)
.watchOS(.v3),
],
products: [
.library(
name: "Buildkite",
targets: ["Buildkite"])
targets: ["Buildkite"]
)
],
targets: [
.target(
@ -34,6 +35,13 @@ let package = Package(
.testTarget(
name: "BuildkiteTests",
dependencies: ["Buildkite"]
)
),
]
)
#if swift(>=5.6)
// Add the documentation compiler plugin if possible
package.dependencies.append(
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
)
#endif

View File

@ -22,6 +22,7 @@ The entire publicly documented REST API surface is supported by this package.
- ``Build/Resources``
- ``Emoji/Resources``
- ``Job/Resources``
- ``Meta/Resources``
- ``Organization/Resources``
- ``Pipeline/Resources``
- ``Team/Resources``
@ -50,6 +51,7 @@ The entire publicly documented REST API surface is supported by this package.
- ``Build``
- ``Emoji``
- ``Job``
- ``Meta``
- ``Organization``
- ``Pipeline``
- ``Team``

View File

@ -47,12 +47,17 @@ public final class BuildkiteClient {
/// - Parameters:
/// - configuration: Configures supported API versions and the access token. Uses the latest supported API versions by default. See ``token`` for setting the client access token if using the default configuration.
/// - transport: Transport layer used for API communication. Uses the shared URLSession by default.
public init(configuration: Configuration = .default, transport: Transport = URLSession.shared) {
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 {
private func handleContentfulResponse<Content: Decodable>(
completion: @escaping (Result<Response<Content>, Error>) -> Void
) -> Transport.Completion {
return { [weak self] result in
guard let self = self else {
return
@ -72,7 +77,9 @@ public final class BuildkiteClient {
}
}
private func handleEmptyResponse(completion: @escaping (Result<Response<Void>, Error>) -> Void) -> Transport.Completion {
private func handleEmptyResponse(
completion: @escaping (Result<Response<Void>, Error>) -> Void
) -> Transport.Completion {
return { [weak self] result in
guard let self = self else {
return
@ -91,12 +98,14 @@ public final class BuildkiteClient {
private func checkResponseForIssues(_ response: URLResponse, data: Data? = nil) throws {
guard let httpResponse = response as? HTTPURLResponse,
let statusCode = StatusCode(rawValue: httpResponse.statusCode) else {
let statusCode = StatusCode(rawValue: httpResponse.statusCode)
else {
throw ResponseError.incompatibleResponse(response)
}
if !statusCode.isSuccess {
guard let data = data,
let errorIntermediary = try? decoder.decode(BuildkiteError.Intermediary.self, from: data) else {
let errorIntermediary = try? decoder.decode(BuildkiteError.Intermediary.self, from: data)
else {
throw statusCode
}
throw BuildkiteError(statusCode: statusCode, intermediary: errorIntermediary)
@ -106,12 +115,13 @@ public final class BuildkiteClient {
// MARK: - Closure API
public extension BuildkiteClient {
extension BuildkiteClient {
/// Performs the given resource asynchronously, then calls a handler upon completion.
/// - Parameters:
/// - resource:A resource.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) where R: Resource, R.Content: Decodable {
public func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void)
where R: Resource, R.Content: Decodable {
do {
let request = try URLRequest(resource, configuration: configuration)
transport.send(request: request, completion: handleContentfulResponse(completion: completion))
@ -125,7 +135,11 @@ public extension BuildkiteClient {
/// - resource:A resource.
/// - pageOptions: Page options to perform pagination.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func send<R>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) where R: PaginatedResource {
public func send<R>(
_ resource: R,
pageOptions: PageOptions? = nil,
completion: @escaping (Result<Response<R.Content>, Error>) -> Void
) where R: PaginatedResource {
do {
let request = try URLRequest(resource, configuration: configuration, pageOptions: pageOptions)
transport.send(request: request, completion: handleContentfulResponse(completion: completion))
@ -138,7 +152,8 @@ public extension BuildkiteClient {
/// - Parameters:
/// - resource:A resource.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) where R: Resource, R.Body: Encodable, R.Content: Decodable {
public func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void)
where R: Resource, R.Body: Encodable, R.Content: Decodable {
do {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder)
transport.send(request: request, completion: handleContentfulResponse(completion: completion))
@ -152,9 +167,18 @@ public extension BuildkiteClient {
/// - resource:A resource.
/// - pageOptions: Page options to perform pagination.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func send<R>(_ resource: R, pageOptions: PageOptions? = nil, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) where R: PaginatedResource, R.Body: Encodable {
public func send<R>(
_ resource: R,
pageOptions: PageOptions? = nil,
completion: @escaping (Result<Response<R.Content>, Error>) -> Void
) where R: PaginatedResource, R.Body: Encodable {
do {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions)
let request = try URLRequest(
resource,
configuration: configuration,
encoder: encoder,
pageOptions: pageOptions
)
transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} catch {
completion(.failure(error))
@ -165,7 +189,8 @@ public extension BuildkiteClient {
/// - Parameters:
/// - resource:A resource.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) where R: Resource, R.Content == Void {
public func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void)
where R: Resource, R.Content == Void {
do {
let request = try URLRequest(resource, configuration: configuration)
transport.send(request: request, completion: handleEmptyResponse(completion: completion))
@ -178,7 +203,8 @@ public extension BuildkiteClient {
/// - Parameters:
/// - resource:A resource.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void) where R: Resource, R.Body: Encodable, R.Content == Void {
public func send<R>(_ resource: R, completion: @escaping (Result<Response<R.Content>, Error>) -> Void)
where R: Resource, R.Body: Encodable, R.Content == Void {
do {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder)
transport.send(request: request, completion: handleEmptyResponse(completion: completion))
@ -191,7 +217,7 @@ public extension BuildkiteClient {
/// - Parameters:
/// - resource:A resource.
/// - completion:The completion handler to call when the operation has completed. This handler is called on whatever queue the transport layer is implemented to use. You should generally assume this is happening on a global background queue, such as the case when using the shared URLSession.
func sendQuery<T>(_ resource: GraphQL<T>, completion: @escaping (Result<T, Error>) -> Void) {
public func sendQuery<T>(_ resource: GraphQL<T>, completion: @escaping (Result<T, Error>) -> Void) {
send(resource) { result in
do {
switch (try result.get()).content {
@ -213,11 +239,12 @@ public extension BuildkiteClient {
import Combine
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public extension BuildkiteClient {
extension BuildkiteClient {
/// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error.
func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> where R: Resource, R.Content: Decodable {
public func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error>
where R: Resource, R.Content: Decodable {
Result { try URLRequest(resource, configuration: configuration) }
.publisher
.flatMap(transport.sendPublisher)
@ -234,7 +261,10 @@ public extension BuildkiteClient {
/// - resource: A resource.
/// - pageOptions: Page options to perform pagination.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error.
func sendPublisher<R>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> where R: PaginatedResource {
public func sendPublisher<R>(
_ resource: R,
pageOptions: PageOptions? = nil
) -> AnyPublisher<Response<R.Content>, Error> where R: PaginatedResource {
Result { try URLRequest(resource, configuration: configuration, pageOptions: pageOptions) }
.publisher
.flatMap(transport.sendPublisher)
@ -249,7 +279,8 @@ public extension BuildkiteClient {
/// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error.
func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> where R: Resource, R.Body: Encodable, R.Content: Decodable {
public func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error>
where R: Resource, R.Body: Encodable, R.Content: Decodable {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder) }
.publisher
.flatMap(transport.sendPublisher)
@ -266,7 +297,10 @@ public extension BuildkiteClient {
/// - resource: A resource.
/// - pageOptions: Page options to perform pagination.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error.
func sendPublisher<R>(_ resource: R, pageOptions: PageOptions? = nil) -> AnyPublisher<Response<R.Content>, Error> where R: PaginatedResource, R.Body: Encodable {
public func sendPublisher<R>(
_ resource: R,
pageOptions: PageOptions? = nil
) -> AnyPublisher<Response<R.Content>, Error> where R: PaginatedResource, R.Body: Encodable {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) }
.publisher
.flatMap(transport.sendPublisher)
@ -281,7 +315,8 @@ public extension BuildkiteClient {
/// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error.
func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> where R: Resource, R.Content == Void {
public func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error>
where R: Resource, R.Content == Void {
Result { try URLRequest(resource, configuration: configuration) }
.publisher
.flatMap(transport.sendPublisher)
@ -295,7 +330,8 @@ public extension BuildkiteClient {
/// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error.
func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error> where R: Resource, R.Body: Encodable, R.Content == Void {
public func sendPublisher<R>(_ resource: R) -> AnyPublisher<Response<R.Content>, Error>
where R: Resource, R.Body: Encodable, R.Content == Void {
Result { try URLRequest(resource, configuration: configuration, encoder: encoder) }
.publisher
.flatMap(transport.sendPublisher)
@ -309,7 +345,7 @@ public extension BuildkiteClient {
/// Performs the given GraphQL query or mutation and publishes the content asynchronously.
/// - Parameter resource: A resource.
/// - Returns: The publisher publishes the content when the operation completes, or terminates if the operation fails with an error.
func sendQueryPublisher<T>(_ resource: GraphQL<T>) -> AnyPublisher<T, Error> {
public func sendQueryPublisher<T>(_ resource: GraphQL<T>) -> AnyPublisher<T, Error> {
sendPublisher(resource)
.map(\.content)
.tryMap { try $0.get() }
@ -323,11 +359,11 @@ public extension BuildkiteClient {
// MARK: - Async/Await API
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public extension BuildkiteClient {
extension BuildkiteClient {
/// Performs the given resource asynchronously.
/// - Parameter resource: A resource.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation.
func send<R>(_ resource: R) async throws -> Response<R.Content> where R: Resource, R.Content: Decodable {
public func send<R>(_ resource: R) async throws -> Response<R.Content> where R: Resource, R.Content: Decodable {
let request = try URLRequest(resource, configuration: configuration)
let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data)
@ -340,7 +376,8 @@ public extension BuildkiteClient {
/// - resource: A resource.
/// - pageOptions: Page options to perform pagination.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation.
func send<R>(_ resource: R, pageOptions: PageOptions? = nil) async throws -> Response<R.Content> where R: PaginatedResource {
public func send<R>(_ resource: R, pageOptions: PageOptions? = nil) async throws -> Response<R.Content>
where R: PaginatedResource {
let request = try URLRequest(resource, configuration: configuration, pageOptions: pageOptions)
let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data)
@ -351,7 +388,8 @@ public extension BuildkiteClient {
/// Performs the given resource asynchronously.
/// - Parameter resource: A resource.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation.
func send<R>(_ resource: R) async throws -> Response<R.Content> where R: Resource, R.Body: Encodable, R.Content: Decodable {
public func send<R>(_ resource: R) async throws -> Response<R.Content>
where R: Resource, R.Body: Encodable, R.Content: Decodable {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder)
let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data)
@ -364,7 +402,8 @@ public extension BuildkiteClient {
/// - resource: A resource.
/// - pageOptions: Page options to perform pagination.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation.
func send<R>(_ resource: R, pageOptions: PageOptions? = nil) async throws -> Response<R.Content> where R: PaginatedResource, R.Body: Encodable {
public func send<R>(_ resource: R, pageOptions: PageOptions? = nil) async throws -> Response<R.Content>
where R: PaginatedResource, R.Body: Encodable {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions)
let (data, response) = try await transport.send(request: request)
@ -376,7 +415,7 @@ public extension BuildkiteClient {
/// Performs the given resource asynchronously.
/// - Parameter resource: A resource.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation.
func send<R>(_ resource: R) async throws -> Response<R.Content> where R: Resource, R.Content == Void {
public func send<R>(_ resource: R) async throws -> Response<R.Content> where R: Resource, R.Content == Void {
let request = try URLRequest(resource, configuration: configuration)
let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data)
@ -386,7 +425,8 @@ public extension BuildkiteClient {
/// Performs the given resource asynchronously.
/// - Parameter resource: A resource.
/// - Returns: A response containing information about the HTTP operation, and no content.
func send<R>(_ resource: R) async throws -> Response<R.Content> where R: Resource, R.Body: Encodable, R.Content == Void {
public func send<R>(_ resource: R) async throws -> Response<R.Content>
where R: Resource, R.Body: Encodable, R.Content == Void {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder)
let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data)
@ -397,7 +437,7 @@ public extension BuildkiteClient {
/// - Parameter resource: A resource.
/// - Returns: Content of the resolved GraphQL operation.
/// - Throws: An error either of type ``BuildkiteError`` or ``GraphQL/Errors``.
func sendQuery<T>(_ resource: GraphQL<T>) async throws -> T {
public func sendQuery<T>(_ resource: GraphQL<T>) async throws -> T {
let response = try await send(resource)
return try response.content.get()
}

View File

@ -25,7 +25,9 @@ public enum Job: Codable, Equatable {
case trigger
}
public init(from decoder: Decoder) throws {
public init(
from decoder: Decoder
) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(Unassociated.self, forKey: .type)
switch type {

View File

@ -0,0 +1,29 @@
//
// Meta.swift
// Buildkite
//
// Created by Aaron Sky on 5/29/22.
// Copyright © 2022 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct Meta: Codable, Equatable {
/// A list of IP addresses in CIDR notation that Buildkite uses to
/// send outbound traffic such as webhooks and commit statuses.
/// These are subject to change from time to time.
///
/// Buildkite recommends checking for new addresses daily, and
/// will try to advertise new addresses for at least 7 days
/// before they are used.
public var webhookIPRanges: [String]
private enum CodingKeys: String, CodingKey {
// This corresponds to the key "webhook_ips" from the Buildkite payload.
case webhookIPRanges = "webhookIps"
}
}

View File

@ -93,7 +93,9 @@ extension Pipeline {
case trigger
}
public init(from decoder: Decoder) throws {
public init(
from decoder: Decoder
) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(Unassociated.self, forKey: .type)
switch type {

View File

@ -26,7 +26,10 @@ public struct APIVersion: Equatable {
public let baseURL: URL
public let version: String
init(baseURL: URL, version: String) {
init(
baseURL: URL,
version: String
) {
self.baseURL = baseURL
self.version = version
}

View File

@ -18,11 +18,16 @@ public struct Configuration {
public var graphQLVersion: APIVersion
public static var `default`: Configuration {
.init(version: APIVersion.REST.v2,
graphQLVersion: APIVersion.GraphQL.v1)
.init(
version: APIVersion.REST.v2,
graphQLVersion: APIVersion.GraphQL.v1
)
}
public init(version: APIVersion = APIVersion.REST.v2, graphQLVersion: APIVersion = APIVersion.GraphQL.v1) {
public init(
version: APIVersion = APIVersion.REST.v2,
graphQLVersion: APIVersion = APIVersion.GraphQL.v1
) {
self.version = version
self.graphQLVersion = graphQLVersion
}

View File

@ -49,7 +49,12 @@ enum Formatters {
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
guard let date = dateIfPossible(fromISO8601: dateString) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Expected date string to be ISO8601-formatted."
)
)
}
return date
}

View File

@ -24,12 +24,12 @@ public enum JSONValue {
indirect case object([String: JSONValue])
}
public extension JSONValue {
subscript(dynamicMember key: JSONValue) -> JSONValue? {
extension JSONValue {
public subscript(dynamicMember key: JSONValue) -> JSONValue? {
self[key]
}
subscript(_ key: JSONValue) -> JSONValue? {
public subscript(_ key: JSONValue) -> JSONValue? {
if case let .number(key) = key {
return self[Int(key)]
} else if case let .string(key) = key {
@ -38,14 +38,14 @@ public extension JSONValue {
return nil
}
subscript(_ index: Int) -> JSONValue? {
public subscript(_ index: Int) -> JSONValue? {
guard case let .array(array) = self else {
return nil
}
return array[index]
}
subscript(_ key: String) -> JSONValue? {
public subscript(_ key: String) -> JSONValue? {
guard case let .object(object) = self else {
return nil
}
@ -76,7 +76,9 @@ extension JSONValue: Encodable {
}
extension JSONValue: Decodable {
public init(from decoder: Decoder) throws {
public init(
from decoder: Decoder
) throws {
let singleValueContainer = try decoder.singleValueContainer()
if singleValueContainer.decodeNil() {
@ -94,49 +96,64 @@ extension JSONValue: Decodable {
} else {
throw DecodingError.dataCorruptedError(
in: singleValueContainer,
debugDescription: "invalid JSON structure or the input was not JSON")
debugDescription: "invalid JSON structure or the input was not JSON"
)
}
}
}
extension JSONValue: ExpressibleByNilLiteral {
public init(nilLiteral: Void) {
public init(
nilLiteral: Void
) {
self = .null
}
}
extension JSONValue: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: BooleanLiteralType) {
public init(
booleanLiteral value: BooleanLiteralType
) {
self = .bool(value)
}
}
extension JSONValue: ExpressibleByIntegerLiteral {
public init(integerLiteral value: IntegerLiteralType) {
public init(
integerLiteral value: IntegerLiteralType
) {
self = .number(Double(value))
}
}
extension JSONValue: ExpressibleByFloatLiteral {
public init(floatLiteral value: FloatLiteralType) {
public init(
floatLiteral value: FloatLiteralType
) {
self = .number(value)
}
}
extension JSONValue: ExpressibleByStringLiteral {
public init(stringLiteral value: StringLiteralType) {
public init(
stringLiteral value: StringLiteralType
) {
self = .string(value)
}
}
extension JSONValue: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: JSONValue...) {
public init(
arrayLiteral elements: JSONValue...
) {
self = .array(elements)
}
}
extension JSONValue: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, JSONValue)...) {
public init(
dictionaryLiteral elements: (String, JSONValue)...
) {
self = .object(Dictionary(uniqueKeysWithValues: elements))
}
}

View File

@ -18,13 +18,16 @@ public struct Page {
public var firstPage: Int?
public var lastPage: Int?
init?(for header: String) {
init?(
for header: String
) {
guard !header.isEmpty else {
return nil
}
for link in header.split(separator: ",") {
let segments = link
let segments =
link
.trimmingCharacters(in: .whitespacesAndNewlines)
.split(separator: ";")
guard
@ -36,7 +39,8 @@ public struct Page {
let url = URLComponents(string: String(urlString.dropFirst().dropLast())),
let pageString = url.queryItems?.first(where: { $0.name == "page" })?.value,
let page = Int(pageString) else {
let page = Int(pageString)
else {
continue
}
@ -62,7 +66,10 @@ public struct PageOptions {
public var page: Int
public var perPage: Int
public init(page: Int, perPage: Int) {
public init(
page: Int,
perPage: Int
) {
self.page = page
self.perPage = perPage
}
@ -71,7 +78,8 @@ public struct PageOptions {
extension URLRequest {
mutating func appendPageOptions(_ options: PageOptions) {
guard let url = self.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return
}
var queryItems = components.queryItems ?? []
@ -81,8 +89,8 @@ extension URLRequest {
}
}
private extension Array where Element == URLQueryItem {
mutating func append(pageOptions: PageOptions) {
extension Array where Element == URLQueryItem {
fileprivate mutating func append(pageOptions: PageOptions) {
append(URLQueryItem(name: "page", value: String(pageOptions.page)))
append(URLQueryItem(name: "per_page", value: String(pageOptions.perPage)))
}

View File

@ -43,10 +43,15 @@ extension Resource where Body == Void {
public protocol PaginatedResource: Resource where Content: Decodable {}
extension URLRequest {
init<R: Resource>(_ resource: R, configuration: Configuration) throws {
init<R: Resource>(
_ resource: R,
configuration: Configuration
) throws {
let version = resource.version
guard version == configuration.version
|| version == configuration.graphQLVersion else {
guard
version == configuration.version
|| version == configuration.graphQLVersion
else {
throw ResourceError.incompatibleVersion(version)
}
@ -58,19 +63,32 @@ extension URLRequest {
self = request
}
init<R: Resource>(_ resource: R, configuration: Configuration, encoder: JSONEncoder) throws where R.Body: Encodable {
init<R: Resource>(
_ resource: R,
configuration: Configuration,
encoder: JSONEncoder
) throws where R.Body: Encodable {
try self.init(resource, configuration: configuration)
httpBody = try encoder.encode(resource.body)
}
init<R: Resource & PaginatedResource>(_ resource: R, configuration: Configuration, pageOptions: PageOptions? = nil) throws {
init<R: Resource & PaginatedResource>(
_ resource: R,
configuration: Configuration,
pageOptions: PageOptions? = nil
) throws {
try self.init(resource, configuration: configuration)
if let options = pageOptions {
appendPageOptions(options)
}
}
init<R: Resource & PaginatedResource>(_ resource: R, configuration: Configuration, encoder: JSONEncoder, pageOptions: PageOptions? = nil) throws where R.Body: Encodable {
init<R: Resource & PaginatedResource>(
_ resource: R,
configuration: Configuration,
encoder: JSONEncoder,
pageOptions: PageOptions? = nil
) throws where R.Body: Encodable {
try self.init(resource, configuration: configuration, encoder: encoder)
if let options = pageOptions {
appendPageOptions(options)

View File

@ -17,7 +17,10 @@ public struct Response<T> {
public let response: URLResponse
public let page: Page?
init(content: T, response: URLResponse) {
init(
content: T,
response: URLResponse
) {
self.content = content
self.response = response
if let response = response as? HTTPURLResponse, let link = response.allHeaderFields["Link"] as? String {
@ -37,7 +40,10 @@ public struct BuildkiteError: Error {
public var message: String
public var errors: [String]
init(statusCode: StatusCode, intermediary: Intermediary) {
init(
statusCode: StatusCode,
intermediary: Intermediary
) {
self.statusCode = statusCode
self.message = intermediary.message ?? ""
self.errors = intermediary.errors ?? []

View File

@ -26,15 +26,15 @@ public protocol Transport {
func send(request: URLRequest, completion: @escaping Completion)
#if canImport(Combine)
#if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
func sendPublisher(request: URLRequest) -> AnyPublisher<Output, Error>
#endif
#endif
#if compiler(>=5.5.2) && canImport(_Concurrency)
#if compiler(>=5.5.2) && canImport(_Concurrency)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
func send(request: URLRequest) async throws -> Output
#endif
#endif
}
extension URLSession: Transport {
@ -53,21 +53,19 @@ extension URLSession: Transport {
task.resume()
}
#if canImport(Combine)
#if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func sendPublisher(request: URLRequest) -> AnyPublisher<Output, Error> {
dataTaskPublisher(for: request)
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
#endif
#endif
#if compiler(>=5.5.2) && canImport(_Concurrency)
#if compiler(>=5.5.2) && canImport(_Concurrency)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func send(request: URLRequest) async throws -> Output {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) {
return try await data(for: request)
} else {
guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else {
return try await withCheckedThrowingContinuation { continuation in
let task = dataTask(with: request) { data, response, error in
if let error = error {
@ -76,7 +74,8 @@ extension URLSession: Transport {
}
guard let data = data,
let response = response else {
let response = response
else {
continuation.resume(throwing: TransportError.noResponse)
return
}
@ -86,6 +85,7 @@ extension URLSession: Transport {
task.resume()
}
}
return try await data(for: request)
}
#endif
#endif
}

View File

@ -40,25 +40,36 @@ extension Array where Element == URLQueryItem {
} else if items.count == 1 {
appendIfNeeded(items.first, forKey: key)
} else {
append(contentsOf: items
append(
contentsOf:
items
.enumerated()
.map {
URLQueryItem(name: "\(key)\(arrayFormat.format(for: $0.offset))",
value: $0.element)
})
URLQueryItem(
name: "\(key)\(arrayFormat.format(for: $0.offset))",
value: $0.element
)
}
)
}
}
mutating func append(_ items: [String: String], forKey key: String) {
append(contentsOf: items.map {
URLQueryItem(name: "\(key)[\($0.key)]",
value: $0.value)
})
append(
contentsOf: items.map {
URLQueryItem(
name: "\(key)[\($0.key)]",
value: $0.value
)
}
)
}
}
extension Date: LosslessStringConvertible {
public init?(_ description: String) {
public init?(
_ description: String
) {
guard let date = Formatters.dateIfPossible(fromISO8601: description) else {
return nil
}
@ -67,7 +78,9 @@ extension Date: LosslessStringConvertible {
}
extension UUID: LosslessStringConvertible {
public init?(_ description: String) {
public init?(
_ description: String
) {
guard let id = UUID(uuidString: description) else {
return nil
}

View File

@ -37,13 +37,16 @@ extension Agent.Resources {
"organizations/\(organization)/agents"
}
public init(organization: String) {
public init(
organization: String
) {
self.organization = organization
}
public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return
}
var queryItems: [URLQueryItem] = []
@ -67,7 +70,10 @@ extension Agent.Resources {
"organizations/\(organization)/agents/\(agentId)"
}
public init(organization: String, agentId: UUID) {
public init(
organization: String,
agentId: UUID
) {
self.organization = organization
self.agentId = agentId
}
@ -88,7 +94,9 @@ extension Agent.Resources {
/// If the agent is currently processing a job, the job and the build will be canceled.
public var force: Bool?
public init(force: Bool? = nil) {
public init(
force: Bool? = nil
) {
self.force = force
}
}
@ -97,7 +105,11 @@ extension Agent.Resources {
"organizations/\(organization)/agents/\(agentId)/stop"
}
public init(organization: String, agentId: UUID, force: Bool? = nil) {
public init(
organization: String,
agentId: UUID,
force: Bool? = nil
) {
self.organization = organization
self.agentId = agentId
self.body = Body(force: force)

View File

@ -33,7 +33,11 @@ extension Annotation.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/annotations"
}
public init(organization: String, pipeline: String, build: Int) {
public init(
organization: String,
pipeline: String,
build: Int
) {
self.organization = organization
self.pipeline = pipeline
self.build = build

View File

@ -33,7 +33,11 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/artifacts"
}
public init(organization: String, pipeline: String, build: Int) {
public init(
organization: String,
pipeline: String,
build: Int
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -58,7 +62,12 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts"
}
public init(organization: String, pipeline: String, build: Int, jobId: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
jobId: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -84,7 +93,13 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts/\(artifactId)"
}
public init(organization: String, pipeline: String, build: Int, jobId: UUID, artifactId: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
jobId: UUID,
artifactId: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -113,7 +128,13 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts/\(artifactId)/download"
}
public init(organization: String, pipeline: String, build: Int, jobId: UUID, artifactId: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
jobId: UUID,
artifactId: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -141,7 +162,13 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts/\(artifactId)"
}
public init(organization: String, pipeline: String, build: Int, jobId: UUID, artifactId: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
jobId: UUID,
artifactId: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -174,13 +201,25 @@ extension Resource where Self == Artifact.Resources.Get {
}
extension Resource where Self == Artifact.Resources.Download {
public static func downloadArtifact(_ id: UUID, in organization: String, pipeline: String, build: Int, job: UUID) -> Self {
public static func downloadArtifact(
_ id: UUID,
in organization: String,
pipeline: String,
build: Int,
job: UUID
) -> Self {
Self(organization: organization, pipeline: pipeline, build: build, jobId: job, artifactId: id)
}
}
extension Resource where Self == Artifact.Resources.Delete {
public static func deleteArtifact(_ id: UUID, in organization: String, pipeline: String, build: Int, job: UUID) -> Self {
public static func deleteArtifact(
_ id: UUID,
in organization: String,
pipeline: String,
build: Int,
job: UUID
) -> Self {
Self(organization: organization, pipeline: pipeline, build: build, jobId: job, artifactId: id)
}
}

View File

@ -27,13 +27,16 @@ extension Build.Resources {
public var queryOptions: QueryOptions?
public init(queryOptions: Build.Resources.QueryOptions? = nil) {
public init(
queryOptions: Build.Resources.QueryOptions? = nil
) {
self.queryOptions = queryOptions
}
public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return
}
var queryItems: [URLQueryItem] = []
@ -60,14 +63,18 @@ extension Build.Resources {
"organizations/\(organization)/builds"
}
public init(organization: String, queryOptions: Build.Resources.QueryOptions? = nil) {
public init(
organization: String,
queryOptions: Build.Resources.QueryOptions? = nil
) {
self.organization = organization
self.queryOptions = queryOptions
}
public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return
}
var queryItems: [URLQueryItem] = []
@ -95,7 +102,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds"
}
public init(organization: String, pipeline: String, queryOptions: Build.Resources.QueryOptions? = nil) {
public init(
organization: String,
pipeline: String,
queryOptions: Build.Resources.QueryOptions? = nil
) {
self.organization = organization
self.pipeline = pipeline
self.queryOptions = queryOptions
@ -103,7 +114,8 @@ extension Build.Resources {
public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return
}
var queryItems: [URLQueryItem] = []
@ -129,7 +141,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)"
}
public init(organization: String, pipeline: String, build: Int) {
public init(
organization: String,
pipeline: String,
build: Int
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -151,7 +167,10 @@ extension Build.Resources {
public var name: String
public var email: String
public init(name: String, email: String) {
public init(
name: String,
email: String
) {
self.name = name
self.email = email
}
@ -181,7 +200,19 @@ extension Build.Resources {
/// For a pull request build, the git repository of the pull request.
public var pullRequestRepository: String?
public init(commit: String, branch: String, author: Author? = nil, cleanCheckout: Bool? = nil, env: [String: String]? = nil, ignorePipelineBranchFilters: Bool? = nil, message: String? = nil, metaData: [String: String]? = nil, pullRequestBaseBranch: String? = nil, pullRequestId: Int? = nil, pullRequestRepository: String? = nil) {
public init(
commit: String,
branch: String,
author: Author? = nil,
cleanCheckout: Bool? = nil,
env: [String: String]? = nil,
ignorePipelineBranchFilters: Bool? = nil,
message: String? = nil,
metaData: [String: String]? = nil,
pullRequestBaseBranch: String? = nil,
pullRequestId: Int? = nil,
pullRequestRepository: String? = nil
) {
self.commit = commit
self.branch = branch
self.author = author
@ -201,7 +232,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds"
}
public init(organization: String, pipeline: String, body: Body) {
public init(
organization: String,
pipeline: String,
body: Body
) {
self.organization = organization
self.pipeline = pipeline
self.body = body
@ -228,7 +263,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/cancel"
}
public init(organization: String, pipeline: String, build: Int) {
public init(
organization: String,
pipeline: String,
build: Int
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -255,7 +294,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/rebuild"
}
public init(organization: String, pipeline: String, build: Int) {
public init(
organization: String,
pipeline: String,
build: Int
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -286,7 +329,17 @@ extension Build.Resources {
/// Filters the results by the given build state. The finished state is a shortcut to automatically search for builds with passed, failed, blocked, canceled states.
public var state: [Build.State]
public init(branches: [String] = [], commit: String? = nil, createdFrom: Date? = nil, createdTo: Date? = nil, creator: UUID? = nil, finishedFrom: Date? = nil, includeRetriedJobs: Bool? = nil, metadata: [String: String] = [:], state: [Build.State] = []) {
public init(
branches: [String] = [],
commit: String? = nil,
createdFrom: Date? = nil,
createdTo: Date? = nil,
creator: UUID? = nil,
finishedFrom: Date? = nil,
includeRetriedJobs: Bool? = nil,
metadata: [String: String] = [:],
state: [Build.State] = []
) {
self.branches = branches
self.commit = commit
self.createdFrom = createdFrom
@ -300,8 +353,10 @@ extension Build.Resources {
}
}
private extension Array where Element == URLQueryItem {
init(queryOptions: Build.Resources.QueryOptions) {
extension Array where Element == URLQueryItem {
fileprivate init(
queryOptions: Build.Resources.QueryOptions
) {
self.init()
append(queryOptions.branches, forKey: "branch")
appendIfNeeded(queryOptions.commit, forKey: "commit")
@ -322,13 +377,18 @@ extension Resource where Self == Build.Resources.ListAll {
}
extension Resource where Self == Build.Resources.ListForOrganization {
public static func builds(inOrganization organization: String, options: Build.Resources.QueryOptions? = nil) -> Self {
public static func builds(inOrganization organization: String, options: Build.Resources.QueryOptions? = nil) -> Self
{
Self(organization: organization, queryOptions: options)
}
}
extension Resource where Self == Build.Resources.ListForPipeline {
public static func builds(forPipeline pipeline: String, in organization: String, options: Build.Resources.QueryOptions? = nil) -> Self {
public static func builds(
forPipeline pipeline: String,
in organization: String,
options: Build.Resources.QueryOptions? = nil
) -> Self {
Self(organization: organization, pipeline: pipeline, queryOptions: options)
}
}

View File

@ -29,7 +29,9 @@ extension Emoji.Resources {
"organizations/\(organization)/emojis"
}
public init(organization: String) {
public init(
organization: String
) {
self.organization = organization
}
}

View File

@ -21,11 +21,15 @@ public struct Followable<R: Resource>: Codable, Equatable, Resource {
private var url: URL
init(url: URL) {
init(
url: URL
) {
self.url = url
}
public init(from decoder: Decoder) throws {
public init(
from decoder: Decoder
) throws {
let container = try decoder.singleValueContainer()
let url = try container.decode(URL.self)
self.init(url: url)

View File

@ -27,7 +27,9 @@ public struct GraphQL<T: Decodable>: Resource {
case data(T)
case errors(Errors)
public init(from decoder: Decoder) throws {
public init(
from decoder: Decoder
) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let errors = try container.decodeIfPresent([Error].self, forKey: .errors) {
let type = try container.decodeIfPresent(String.self, forKey: .type)
@ -38,7 +40,8 @@ public struct GraphQL<T: Decodable>: Resource {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "The GraphQL response does not contain either errors or data. One is required. If errors are present, they will be considered instead of any data that may have also been sent."
debugDescription:
"The GraphQL response does not contain either errors or data. One is required. If errors are present, they will be considered instead of any data that may have also been sent."
)
)
}
@ -68,7 +71,10 @@ public struct GraphQL<T: Decodable>: Resource {
public let path = ""
public init(rawQuery query: String, variables: [String: JSONValue] = [:]) {
public init(
rawQuery query: String,
variables: [String: JSONValue] = [:]
) {
self.body = Body(query: query, variables: .object(variables))
}

View File

@ -35,7 +35,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/retry"
}
public init(organization: String, pipeline: String, build: Int, job: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
job: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -67,7 +72,10 @@ extension Job.Resources {
public var unblocker: UUID?
public var fields: [String: String]
public init(unblocker: UUID? = nil, fields: [String: String] = [:]) {
public init(
unblocker: UUID? = nil,
fields: [String: String] = [:]
) {
self.unblocker = unblocker
self.fields = fields
}
@ -77,7 +85,13 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/unblock"
}
public init(organization: String, pipeline: String, build: Int, job: UUID, body: Body) {
public init(
organization: String,
pipeline: String,
build: Int,
job: UUID,
body: Body
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -106,7 +120,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log"
}
public init(organization: String, pipeline: String, build: Int, job: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
job: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -129,7 +148,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log"
}
public init(organization: String, pipeline: String, build: Int, job: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
job: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -157,7 +181,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/env"
}
public init(organization: String, pipeline: String, build: Int, job: UUID) {
public init(
organization: String,
pipeline: String,
build: Int,
job: UUID
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -189,7 +218,13 @@ extension Job.Resources.LogOutput {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log.\(format)"
}
public init(organization: String, pipeline: String, build: Int, job: UUID, format: Format) {
public init(
organization: String,
pipeline: String,
build: Int,
job: UUID,
format: Format
) {
self.organization = organization
self.pipeline = pipeline
self.build = build
@ -206,7 +241,13 @@ extension Resource where Self == Job.Resources.Retry {
}
extension Resource where Self == Job.Resources.Unblock {
public static func unblockJob(_ job: UUID, in organization: String, pipeline: String, build: Int, with body: Self.Body) -> Self {
public static func unblockJob(
_ job: UUID,
in organization: String,
pipeline: String,
build: Int,
with body: Self.Body
) -> Self {
Self(organization: organization, pipeline: pipeline, build: build, job: job, body: body)
}
}
@ -224,13 +265,24 @@ extension Resource where Self == Job.Resources.DeleteLogOutput {
}
extension Resource where Self == Job.Resources.EnvironmentVariables {
public static func environmentVariables(for job: UUID, in organization: String, pipeline: String, build: Int) -> Self {
public static func environmentVariables(
for job: UUID,
in organization: String,
pipeline: String,
build: Int
) -> Self {
Self(organization: organization, pipeline: pipeline, build: build, job: job)
}
}
extension Resource where Self == Job.Resources.LogOutput.Alternative {
public static func logOutput(_ format: Job.Resources.LogOutput.Alternative.Format, for job: UUID, in organization: String, pipeline: String, build: Int) -> Self {
public static func logOutput(
_ format: Job.Resources.LogOutput.Alternative.Format,
for job: UUID,
in organization: String,
pipeline: String,
build: Int
) -> Self {
Self(organization: organization, pipeline: pipeline, build: build, job: job, format: format)
}
}

View File

@ -0,0 +1,39 @@
//
// Metas.swift
// Buildkite
//
// Created by Aaron Sky on 5/29/22.
// Copyright © 2022 Aaron Sky. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension Meta {
/// Resources for requesting meta information about Buildkite.
public enum Resources {}
}
extension Meta.Resources {
/// Get an object with properties describing Buildkite
///
/// Returns meta information about Buildkite.
public struct Get: Resource {
public typealias Content = Meta
public let path = "meta"
public init() {}
}
}
extension Resource where Self == Meta.Resources.Get {
/// Get an object with properties describing Buildkite
///
/// Returns meta information about Buildkite.
public static var meta: Self {
Self()
}
}

View File

@ -38,7 +38,9 @@ extension Organization.Resources {
"organizations/\(organization)"
}
public init(organization: String) {
public init(
organization: String
) {
self.organization = organization
}
}

View File

@ -30,7 +30,9 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines"
}
public init(organization: String) {
public init(
organization: String
) {
self.organization = organization
}
}
@ -47,7 +49,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)"
}
public init(organization: String, pipeline: String) {
public init(
organization: String,
pipeline: String
) {
self.organization = organization
self.pipeline = pipeline
}
@ -88,7 +93,20 @@ extension Pipeline.Resources {
/// An array of team UUIDs to add this pipeline to. You can find your teams UUID either via the GraphQL API, or on the settings page for a team. This property is only available if your organization has enabled Teams.
public var teamUUIDs: [UUID]?
public init(name: String, repository: URL, configuration: String, branchConfiguration: String? = nil, cancelRunningBranchBuilds: Bool? = nil, cancelRunningBranchBuildsFilter: String? = nil, defaultBranch: String? = nil, description: String? = nil, providerSettings: Pipeline.Provider.Settings? = nil, skipQueuedBranchBuilds: Bool? = nil, skipQueuedBranchBuildsFilter: String? = nil, teamUUIDs: [UUID]? = nil) {
public init(
name: String,
repository: URL,
configuration: String,
branchConfiguration: String? = nil,
cancelRunningBranchBuilds: Bool? = nil,
cancelRunningBranchBuildsFilter: String? = nil,
defaultBranch: String? = nil,
description: String? = nil,
providerSettings: Pipeline.Provider.Settings? = nil,
skipQueuedBranchBuilds: Bool? = nil,
skipQueuedBranchBuildsFilter: String? = nil,
teamUUIDs: [UUID]? = nil
) {
self.name = name
self.repository = repository
self.configuration = configuration
@ -108,7 +126,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines"
}
public init(organization: String, body: Body) {
public init(
organization: String,
body: Body
) {
self.organization = organization
self.body = body
}
@ -155,7 +176,21 @@ extension Pipeline.Resources {
/// An array of team UUIDs to add this pipeline to. You can find your teams UUID either via the GraphQL API, or on the settings page for a team. This property is only available if your organization has enabled Teams.
public var teamUUIDs: [UUID]?
public init(name: String, repository: URL, steps: [Pipeline.Step], branchConfiguration: String? = nil, cancelRunningBranchBuilds: Bool? = nil, cancelRunningBranchBuildsFilter: String? = nil, defaultBranch: String? = nil, description: String? = nil, env: [String: String]? = nil, providerSettings: Pipeline.Provider.Settings? = nil, skipQueuedBranchBuilds: Bool? = nil, skipQueuedBranchBuildsFilter: String? = nil, teamUUIDs: [UUID]? = nil) {
public init(
name: String,
repository: URL,
steps: [Pipeline.Step],
branchConfiguration: String? = nil,
cancelRunningBranchBuilds: Bool? = nil,
cancelRunningBranchBuildsFilter: String? = nil,
defaultBranch: String? = nil,
description: String? = nil,
env: [String: String]? = nil,
providerSettings: Pipeline.Provider.Settings? = nil,
skipQueuedBranchBuilds: Bool? = nil,
skipQueuedBranchBuildsFilter: String? = nil,
teamUUIDs: [UUID]? = nil
) {
self.name = name
self.repository = repository
self.steps = steps
@ -176,7 +211,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines"
}
public init(organization: String, body: Body) {
public init(
organization: String,
body: Body
) {
self.organization = organization
self.body = body
}
@ -226,7 +264,21 @@ extension Pipeline.Resources {
/// Whether the pipeline is visible to everyone, including users outside this organization.
public var visibility: String?
public init(branchConfiguration: String? = nil, cancelRunningBranchBuilds: Bool? = nil, cancelRunningBranchBuildsFilter: String? = nil, defaultBranch: String? = nil, description: String? = nil, env: [String: String]? = nil, name: String? = nil, providerSettings: Pipeline.Provider.Settings? = nil, repository: URL? = nil, steps: [Pipeline.Step]? = nil, skipQueuedBranchBuilds: Bool? = nil, skipQueuedBranchBuildsFilter: String? = nil, visibility: String? = nil) {
public init(
branchConfiguration: String? = nil,
cancelRunningBranchBuilds: Bool? = nil,
cancelRunningBranchBuildsFilter: String? = nil,
defaultBranch: String? = nil,
description: String? = nil,
env: [String: String]? = nil,
name: String? = nil,
providerSettings: Pipeline.Provider.Settings? = nil,
repository: URL? = nil,
steps: [Pipeline.Step]? = nil,
skipQueuedBranchBuilds: Bool? = nil,
skipQueuedBranchBuildsFilter: String? = nil,
visibility: String? = nil
) {
self.branchConfiguration = branchConfiguration
self.cancelRunningBranchBuilds = cancelRunningBranchBuilds
self.cancelRunningBranchBuildsFilter = cancelRunningBranchBuildsFilter
@ -247,7 +299,11 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)"
}
public init(organization: String, pipeline: String, body: Body) {
public init(
organization: String,
pipeline: String,
body: Body
) {
self.organization = organization
self.pipeline = pipeline
self.body = body
@ -266,7 +322,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/archive"
}
public init(organization: String, pipeline: String) {
public init(
organization: String,
pipeline: String
) {
self.organization = organization
self.pipeline = pipeline
}
@ -288,7 +347,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/unarchive"
}
public init(organization: String, pipeline: String) {
public init(
organization: String,
pipeline: String
) {
self.organization = organization
self.pipeline = pipeline
}
@ -309,7 +371,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)"
}
public init(organization: String, pipeline: String) {
public init(
organization: String,
pipeline: String
) {
self.organization = organization
self.pipeline = pipeline
}
@ -326,7 +391,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/webhook"
}
public init(organization: String, pipeline: String) {
public init(
organization: String,
pipeline: String
) {
self.organization = organization
self.pipeline = pipeline
}

View File

@ -30,14 +30,18 @@ extension Team.Resources {
"organizations/\(organization)/teams"
}
public init(organization: String, userId: UUID? = nil) {
public init(
organization: String,
userId: UUID? = nil
) {
self.organization = organization
self.userId = userId
}
public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return
}
var queryItems: [URLQueryItem] = []

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -33,24 +34,37 @@ class BuildkiteClientTests: XCTestCase {
var client: BuildkiteClient
var resources = MockResources()
init(testCase: Case = .success) throws {
init(
testCase: Case = .success
) throws {
let responses: [(Data, URLResponse)]
switch testCase {
case .success:
responses = [try MockData.mockingSuccess(with: resources.content, url: configuration.version.baseURL)]
case .successPaginated:
responses = [try MockData.mockingSuccess(with: resources.paginatedContent, url: configuration.version.baseURL)]
responses = [
try MockData.mockingSuccess(with: resources.paginatedContent, url: configuration.version.baseURL)
]
case .successNoContent:
responses = [MockData.mockingSuccessNoContent(url: configuration.version.baseURL)]
case .successHasBody:
responses = [MockData.mockingSuccessNoContent(url: configuration.version.baseURL)]
case .successHasBodyAndContent:
responses = [try MockData.mockingSuccess(with: resources.bodyAndContent, url: configuration.version.baseURL)]
responses = [
try MockData.mockingSuccess(with: resources.bodyAndContent, url: configuration.version.baseURL)
]
case .successHasBodyPaginated:
responses = [try MockData.mockingSuccess(with: resources.bodyAndPaginatedContent, url: configuration.version.baseURL)]
responses = [
try MockData.mockingSuccess(
with: resources.bodyAndPaginatedContent,
url: configuration.version.baseURL
)
]
case .successGraphQL:
responses = [try MockData.mockingSuccess(with: resources.graphQLIntermediary, url: configuration.version.baseURL)]
responses = [
try MockData.mockingSuccess(with: resources.graphQLIntermediary, url: configuration.version.baseURL)
]
case .badResponse:
responses = [MockData.mockingIncompatibleResponse(for: configuration.version.baseURL)]
case .unsuccessfulResponse:
@ -59,8 +73,10 @@ class BuildkiteClientTests: XCTestCase {
responses = []
}
client = BuildkiteClient(configuration: configuration,
transport: MockTransport(responses: responses))
client = BuildkiteClient(
configuration: configuration,
transport: MockTransport(responses: responses)
)
client.token = "a valid token, i guess"
XCTAssertEqual(client.token, client.configuration.token)
}
@ -108,7 +124,10 @@ extension BuildkiteClientTests {
let testData = try TestData(testCase: .success)
let expectation = XCTestExpectation()
testData.client.send(testData.resources.paginatedContentResource, pageOptions: PageOptions(page: 1, perPage: 30)) { result in
testData.client.send(
testData.resources.paginatedContentResource,
pageOptions: PageOptions(page: 1, perPage: 30)
) { result in
do {
let response = try result.get()
XCTAssertEqual(testData.resources.paginatedContent, response.content)
@ -161,7 +180,10 @@ extension BuildkiteClientTests {
let testData = try TestData(testCase: .successHasBodyPaginated)
let expectation = XCTestExpectation()
testData.client.send(testData.resources.bodyAndPaginatedResource, pageOptions: PageOptions(page: 1, perPage: 30)) { result in
testData.client.send(
testData.resources.bodyAndPaginatedResource,
pageOptions: PageOptions(page: 1, perPage: 30)
) { result in
do {
let response = try result.get()
XCTAssertEqual(testData.resources.bodyAndPaginatedContent, response.content)
@ -246,13 +268,15 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.contentResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
},
receiveValue: { XCTAssertEqual(testData.resources.content, $0.content) })
receiveValue: { XCTAssertEqual(testData.resources.content, $0.content) }
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -261,8 +285,10 @@ extension BuildkiteClientTests {
let testData = try TestData(testCase: .success)
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.paginatedContentResource, pageOptions: PageOptions(page: 1, perPage: 30))
.sink(receiveCompletion: {
testData.client
.sendPublisher(testData.resources.paginatedContentResource, pageOptions: PageOptions(page: 1, perPage: 30))
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
@ -271,7 +297,8 @@ extension BuildkiteClientTests {
receiveValue: {
XCTAssertEqual(testData.resources.paginatedContent, $0.content)
XCTAssertNotNil($0.page)
})
}
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -281,12 +308,15 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.noContentNoBodyResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}, receiveValue: { _ in })
},
receiveValue: { _ in }
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -296,12 +326,15 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.bodyResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}, receiveValue: { _ in })
},
receiveValue: { _ in }
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -311,7 +344,8 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.bodyAndContentResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
@ -319,7 +353,8 @@ extension BuildkiteClientTests {
},
receiveValue: {
XCTAssertEqual(testData.resources.bodyAndContent, $0.content)
})
}
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -328,8 +363,10 @@ extension BuildkiteClientTests {
let testData = try TestData(testCase: .successHasBodyPaginated)
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.bodyAndPaginatedResource, pageOptions: PageOptions(page: 1, perPage: 30))
.sink(receiveCompletion: {
testData.client
.sendPublisher(testData.resources.bodyAndPaginatedResource, pageOptions: PageOptions(page: 1, perPage: 30))
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
@ -338,7 +375,8 @@ extension BuildkiteClientTests {
receiveValue: {
XCTAssertEqual(testData.resources.bodyAndPaginatedContent, $0.content)
XCTAssertNotNil($0.page)
})
}
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -348,14 +386,17 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendQueryPublisher(testData.resources.graphQLResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}, receiveValue: {
},
receiveValue: {
XCTAssertEqual(testData.resources.graphQLContent, $0)
})
}
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -365,12 +406,15 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.contentResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case .finished = $0 {
XCTFail("Expected to have failed with an error, but publisher fulfilled normally")
}
expectation.fulfill()
}, receiveValue: { _ in })
},
receiveValue: { _ in }
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -380,12 +424,15 @@ extension BuildkiteClientTests {
let expectation = XCTestExpectation()
var cancellables: Set<AnyCancellable> = []
testData.client.sendPublisher(testData.resources.contentResource)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case .finished = $0 {
XCTFail("Expected to have failed with an error, but publisher fulfilled normally")
}
expectation.fulfill()
}, receiveValue: { _ in })
},
receiveValue: { _ in }
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -405,8 +452,10 @@ extension BuildkiteClientTests {
func testAsyncBasedRequestWithPagination() async throws {
let testData = try TestData(testCase: .success)
let response = try await testData.client.send(testData.resources.paginatedContentResource,
pageOptions: PageOptions(page: 1, perPage: 30))
let response = try await testData.client.send(
testData.resources.paginatedContentResource,
pageOptions: PageOptions(page: 1, perPage: 30)
)
XCTAssertEqual(testData.resources.paginatedContent, response.content)
XCTAssertNotNil(response.page)
}
@ -429,8 +478,10 @@ extension BuildkiteClientTests {
func testAsyncBasedRequestHasBodyWithPagination() async throws {
let testData = try TestData(testCase: .successHasBodyPaginated)
let response = try await testData.client.send(testData.resources.bodyAndPaginatedResource,
pageOptions: PageOptions(page: 1, perPage: 30))
let response = try await testData.client.send(
testData.resources.bodyAndPaginatedResource,
pageOptions: PageOptions(page: 1, perPage: 30)
)
XCTAssertEqual(testData.resources.bodyAndPaginatedContent, response.content)
XCTAssertNotNil(response.page)
}

View File

@ -10,6 +10,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -19,40 +20,40 @@ import FoundationNetworking
final class JSONValueTests: XCTestCase {
func testDecodeArray() throws {
let json = """
{ "fooKey": [true, 1] }
"""
{ "fooKey": [true, 1] }
"""
let expected = JSONValue.object(["fooKey": .array([.bool(true), .number(1)])])
try decodingTest(json, expected)
}
func testDecodeBool() throws {
let json = """
{ "fooKey": true }
"""
{ "fooKey": true }
"""
let expected = JSONValue.object(["fooKey": .bool(true)])
try decodingTest(json, expected)
}
func testDecodeDouble() throws {
let json = """
{ "fooKey": 1.2345 }
"""
{ "fooKey": 1.2345 }
"""
let expected = JSONValue.object(["fooKey": .number(1.2345)])
try decodingTest(json, expected)
}
func testDecodeNull() throws {
let json = """
{ "fooKey": null }
"""
{ "fooKey": null }
"""
let expected = JSONValue.object(["fooKey": .null])
try decodingTest(json, expected)
}
func testDecodeString() throws {
let json = """
{ "fooKey": "fooVal" }
"""
{ "fooKey": "fooVal" }
"""
let expected = JSONValue.object(["fooKey": .string("fooVal")])
let data = try XCTUnwrap(json.data(using: .utf8))
let actual = try JSONDecoder().decode(JSONValue.self, from: data)
@ -94,16 +95,15 @@ final class JSONValueTests: XCTestCase {
123,
[
"abc": false,
"qqq": [:]
]
"qqq": [:],
],
]
,
],
"qux": [
"1": nil,
"2": "2",
"3": 33333333.0
]
"3": 33333333.0,
],
])
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)

View File

@ -6,10 +6,11 @@
// Copyright © 2020 Aaron Sky. All rights reserved.
//
@testable import Buildkite
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@ -68,7 +69,8 @@ extension TransportTests {
func testURLSessionSendClosureBasedRequest() {
let request = URLRequest(url: URL())
let expectation = XCTestExpectation()
createSession().send(request: request) {
createSession()
.send(request: request) {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
@ -80,7 +82,8 @@ extension TransportTests {
func testURLSessionSendClosureBasedRequestFailure() {
let request = URLRequest(url: URL())
let expectation = XCTestExpectation()
createSession(testCase: .error).send(request: request) {
createSession(testCase: .error)
.send(request: request) {
if case .success(_) = $0 {
XCTFail("Expected to have failed with an error, but closure fulfilled normally")
}
@ -103,12 +106,15 @@ extension TransportTests {
var cancellables: Set<AnyCancellable> = []
createSession()
.sendPublisher(request: request)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case let .failure(error) = $0 {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}, receiveValue: { _ in })
},
receiveValue: { _ in }
)
.store(in: &cancellables)
wait(for: [expectation])
}
@ -119,12 +125,15 @@ extension TransportTests {
var cancellables: Set<AnyCancellable> = []
createSession(testCase: .error)
.sendPublisher(request: request)
.sink(receiveCompletion: {
.sink(
receiveCompletion: {
if case .finished = $0 {
XCTFail("Expected to have failed with an error, but publisher fulfilled normally")
}
expectation.fulfill()
}, receiveValue: { _ in })
},
receiveValue: { _ in }
)
.store(in: &cancellables)
wait(for: [expectation])
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -15,37 +16,17 @@ import FoundationNetworking
#endif
class AccessTokensTests: XCTestCase {
func testAccessTokenGet() throws {
func testAccessTokenGet() async throws {
let expected = AccessToken(uuid: UUID(), scopes: [])
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.getAccessToken)
context.client.send(.getAccessToken) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testAccessTokenDelete() throws {
func testAccessTokenDelete() async throws {
let context = MockContext()
let expectation = XCTestExpectation()
context.client.send(.revokeAccessToken) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
_ = try await context.client.send(.revokeAccessToken)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,7 +17,9 @@ import FoundationNetworking
extension Agent {
init() {
let job = Job.script(Job.Command(id: UUID(),
let job = Job.script(
Job.Command(
id: UUID(),
name: "📦",
state: "passed",
command: nil,
@ -40,9 +43,12 @@ extension Agent {
retriedInJobId: nil,
retriesCount: nil,
parallelGroupIndex: nil,
parallelGroupTotal: nil))
parallelGroupTotal: nil
)
)
self.init(id: UUID(),
self.init(
id: UUID(),
url: Followable(),
webUrl: URL(),
name: "jeffrey",
@ -56,59 +62,31 @@ extension Agent {
job: job,
lastJobFinishedAt: nil,
priority: nil,
metaData: [])
metaData: []
)
}
}
class AgentsTests: XCTestCase {
func testAgentsList() throws {
func testAgentsList() async throws {
let expected = [Agent(), Agent()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.agents(in: "buildkite"))
context.client.send(.agents(in: "buildkite")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testAgentsGet() throws {
func testAgentsGet() async throws {
let expected = Agent()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
context.client.send(.agent(UUID(), in: "buildkite")) { result in
do {
let response = try result.get()
let response = try await context.client.send(.agent(UUID(), in: "buildkite"))
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testAgentsStop() throws {
func testAgentsStop() async throws {
let context = MockContext()
let expectation = XCTestExpectation()
context.client.send(.stopAgent(UUID(), in: "buildkite", force: true)) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
_ = try await context.client.send(.stopAgent(UUID(), in: "buildkite", force: true))
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,31 +17,24 @@ import FoundationNetworking
extension Annotation {
init() {
self.init(id: UUID(),
self.init(
id: UUID(),
context: "message",
style: .info,
bodyHtml: "<div></div>",
createdAt: Date(timeIntervalSince1970: 1000),
updatedAt: Date(timeIntervalSince1970: 1001))
updatedAt: Date(timeIntervalSince1970: 1001)
)
}
}
class AnnotationsTests: XCTestCase {
func testAnnotationsList() throws {
func testAnnotationsList() async throws {
let expected = [Annotation(), Annotation()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.annotations(in: "buildkite", pipeline: "my-pipeline", build: 1))
context.client.send(.annotations(in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,7 +17,8 @@ import FoundationNetworking
extension Artifact {
init() {
self.init(id: UUID(),
self.init(
id: UUID(),
jobId: UUID(),
url: Followable(),
downloadUrl: Followable(),
@ -26,96 +28,59 @@ extension Artifact {
filename: "",
mimeType: "",
fileSize: 0,
sha1sum: "")
sha1sum: ""
)
}
}
class ArtifactsTests: XCTestCase {
func testArtifactsListByBuild() throws {
func testArtifactsListByBuild() async throws {
let expected = [Artifact(), Artifact()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.artifacts(byBuild: 1, in: "buildkite", pipeline: "my-pipeline"))
context.client.send(.artifacts(byBuild: 1, in: "buildkite", pipeline: "my-pipeline")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testArtifactsListByJob() throws {
func testArtifactsListByJob() async throws {
let expected = [Artifact(), Artifact()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.artifacts(byJob: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.artifacts(byJob: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testArtifactsGet() throws {
func testArtifactsGet() async throws {
let expected = Artifact()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.artifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())
)
context.client.send(.artifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testArtifactsDownload() throws {
func testArtifactsDownload() async throws {
let expected = Artifact.URLs(url: URL())
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.downloadArtifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())
)
context.client.send(.downloadArtifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testArtifactsDelete() throws {
func testArtifactsDelete() async throws {
let context = MockContext()
let expectation = XCTestExpectation()
context.client.send(.deleteArtifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
_ = try await context.client.send(
.deleteArtifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())
)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,7 +17,8 @@ import FoundationNetworking
extension Build {
init() {
self.init(id: UUID(),
self.init(
id: UUID(),
url: Followable(),
webUrl: URL(),
number: 1,
@ -35,36 +37,29 @@ extension Build {
finishedAt: Date(timeIntervalSince1970: 1001),
metaData: [:],
pullRequest: [:],
pipeline: Pipeline())
pipeline: Pipeline()
)
}
}
class BuildsTests: XCTestCase {
func testBuildsListAllDefaultQuery() throws {
func testBuildsListAllDefaultQuery() async throws {
let expected = [Build(), Build()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.builds())
context.client.send(.builds()) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsListAllSpecializedQuery() throws {
func testBuildsListAllSpecializedQuery() async throws {
let expected = [Build(), Build()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
context.client.send(.builds(options: .init(branches: ["master"],
let response = try await context.client.send(
.builds(
options: .init(
branches: ["master"],
commit: "HEAD",
createdFrom: Date(timeIntervalSince1970: 1000),
createdTo: Date(timeIntervalSince1970: 1000),
@ -72,79 +67,53 @@ class BuildsTests: XCTestCase {
finishedFrom: Date(timeIntervalSince1970: 1000),
includeRetriedJobs: true,
metadata: ["buildkite": "is cool"],
state: [.passed, .blocked, .failed]))) { result in
do {
let response = try result.get()
state: [.passed, .blocked, .failed]
)
)
)
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsListForOrganization() throws {
func testBuildsListForOrganization() async throws {
let expected = [Build(), Build()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.builds(inOrganization: "buildkite", options: .init()))
context.client.send(.builds(inOrganization: "buildkite", options: .init())) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsListForPipeline() throws {
func testBuildsListForPipeline() async throws {
let expected = [Build(), Build()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.builds(forPipeline: "my-pipeline", in: "buildkite", options: .init())
)
context.client.send(.builds(forPipeline: "my-pipeline", in: "buildkite", options: .init())) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsGet() throws {
func testBuildsGet() async throws {
let expected = Build()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.build(1, in: "buildkite", pipeline: "my-pipeline"))
context.client.send(.build(1, in: "buildkite", pipeline: "my-pipeline")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsCreate() throws {
func testBuildsCreate() async throws {
let expected = Build()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
context.client.send(.createBuild(in: "buildkite", pipeline: "my-pipeline", with: .init(commit: "HEAD",
let response = try await context.client.send(
.createBuild(
in: "buildkite",
pipeline: "my-pipeline",
with: .init(
commit: "HEAD",
branch: "master",
author: Build.Resources.Create.Body.Author(name: "", email: ""),
cleanCheckout: nil,
@ -154,51 +123,29 @@ class BuildsTests: XCTestCase {
metaData: nil,
pullRequestBaseBranch: nil,
pullRequestId: nil,
pullRequestRepository: nil))) { result in
do {
let response = try result.get()
pullRequestRepository: nil
)
)
)
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsCancel() throws {
func testBuildsCancel() async throws {
let expected = Build()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.cancelBuild(1, in: "buildkite", pipeline: "my-pipeline"))
context.client.send(.cancelBuild(1, in: "buildkite", pipeline: "my-pipeline")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testBuildsRebuild() throws {
func testBuildsRebuild() async throws {
let expected = Build()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.rebuild(1, in: "buildkite", pipeline: "my-pipeline"))
context.client.send(.rebuild(1, in: "buildkite", pipeline: "my-pipeline")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,27 +17,20 @@ import FoundationNetworking
extension Emoji {
init() {
self.init(name: "jeff",
url: URL())
self.init(
name: "jeff",
url: URL()
)
}
}
class EmojisTests: XCTestCase {
func testEmojisList() throws {
func testEmojisList() async throws {
let expected = [Emoji(), Emoji()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.emojis(in: "buildkite"))
context.client.send(.emojis(in: "buildkite")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -21,29 +22,13 @@ extension Followable {
}
class FollowableTests: XCTestCase {
func testFollowable() throws {
func testFollowable() async throws {
let expected = Organization()
let context = try MockContext(content: Organization(), expected)
let expectation = XCTestExpectation()
let organizationResponse = try await context.client.send(.organization("buildkite"))
let followableResponse = try await context.client.send(organizationResponse.content.url)
context.client.send(.organization("buildkite")) { result in
do {
let response = try result.get()
context.client.send(response.content.url) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
} catch {
XCTFail(error.localizedDescription)
}
}
wait(for: [expectation])
XCTAssertEqual(expected, followableResponse.content)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -15,43 +16,46 @@ import FoundationNetworking
#endif
class GraphQLTests: XCTestCase {
func testGraphQLSuccess() throws {
func testGraphQLSuccess() async throws {
let expected: JSONValue = ["jeff": [1, 2, 3], "horses": false]
let content: JSONValue = ["data": expected]
let context = try MockContext(content: content)
let expectation = XCTestExpectation()
let response = try await context.client.sendQuery(
GraphQL<JSONValue>(rawQuery: "query MyQuery{jeff,horses}", variables: [:])
)
context.client.sendQuery(GraphQL<JSONValue>(rawQuery: "query MyQuery{jeff,horses}", variables: [:])) { result in
do {
let content = try result.get()
XCTAssertEqual(expected, content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
XCTAssertEqual(expected, response)
}
func testGraphQLErrors() throws {
let expected: GraphQL<JSONValue>.Errors = .init(errors: [
.init(message: "Field 'id' doesn't exist on type 'Query'",
func testGraphQLErrors() async throws {
let expected: GraphQL<JSONValue>.Errors = .init(
errors: [
.init(
message: "Field 'id' doesn't exist on type 'Query'",
locations: [.init(line: 2, column: 3)],
path: ["query SimpleQuery", "id"],
extensions: [
"code": "undefinedField",
"typeName": "Query",
"fieldName": "id"
])
], type: nil)
"fieldName": "id",
]
)
],
type: nil
)
let content: JSONValue = [
"errors": .array(expected.errors.map { error in
"errors": .array(
expected.errors.map { error in
let messageJSON: JSONValue = .string(error.message)
let locationsJSON: JSONValue
if let locations = error.locations {
locationsJSON = .array(locations.map { .object(["line": .number(Double($0.line)), "column": .number(Double($0.column))]) })
locationsJSON = .array(
locations.map {
.object(["line": .number(Double($0.line)), "column": .number(Double($0.column))])
}
)
} else {
locationsJSON = .null
}
@ -67,39 +71,33 @@ class GraphQLTests: XCTestCase {
"message": messageJSON,
"locations": locationsJSON,
"path": pathJSON,
"extensions": extensionsJSON
"extensions": extensionsJSON,
]
})
}
)
]
let context = try MockContext(content: content)
let expectation = XCTestExpectation()
context.client.sendQuery(GraphQL<JSONValue>(rawQuery: "query MyQuery{jeff,horses}", variables: [:])) { result in
do {
_ = try result.get()
_ = try await context.client.sendQuery(
GraphQL<JSONValue>(rawQuery: "query MyQuery{jeff,horses}", variables: [:])
)
XCTFail("Expected to have failed with an error, but closure fulfilled normally")
} catch let error as GraphQL<JSONValue>.Errors {
XCTAssertEqual(expected, error)
} catch {
XCTFail("Expected to have failed with an error, but closure failed with unexpected error type")
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testGraphQLIncompatibleResponse() throws {
func testGraphQLIncompatibleResponse() async throws {
let content: JSONValue = [:]
let context = try MockContext(content: content)
let expectation = XCTestExpectation()
context.client.sendQuery(GraphQL<JSONValue>(rawQuery: "", variables: [:])) {
try? XCTAssertThrowsError($0.get(), "Expected to have failed with an error, but closure fulfilled normally")
expectation.fulfill()
}
wait(for: [expectation])
do {
_ = try await context.client.sendQuery(GraphQL<JSONValue>(rawQuery: "", variables: [:]))
XCTFail("Expected to have failed with an error, but closure fulfilled normally")
} catch {}
}
func testGraphQLContentGet() throws {

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -15,26 +16,21 @@ import FoundationNetworking
#endif
class JobsTests: XCTestCase {
func testJobsRetryWaiter() throws {
func testJobsRetryWaiter() async throws {
let expected: Job = .waiter(Job.Wait(id: UUID()))
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.retryJob(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.retryJob(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testJobsRetryTrigger() throws {
let expected: Job = .trigger(Job.Trigger(name: nil,
func testJobsRetryTrigger() async throws {
let expected: Job = .trigger(
Job.Trigger(
name: nil,
state: nil,
buildUrl: URL(),
webUrl: URL(),
@ -42,145 +38,105 @@ class JobsTests: XCTestCase {
scheduledAt: nil,
finishedAt: nil,
runnableAt: nil,
triggeredBuild: Job.Trigger.TriggeredBuild(id: UUID(),
triggeredBuild: Job.Trigger.TriggeredBuild(
id: UUID(),
number: 0,
url: URL(),
webUrl: URL())))
webUrl: URL()
)
)
)
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.retryJob(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.retryJob(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testJobsUnblock() throws {
let expected: Job = .manual(Job.Block(id: UUID(),
func testJobsUnblock() async throws {
let expected: Job = .manual(
Job.Block(
id: UUID(),
label: "",
state: "",
webUrl: nil,
unblockedBy: User(),
unblockedAt: Date(timeIntervalSince1970: 1000),
unblockable: true,
unblockUrl: URL()))
unblockUrl: URL()
)
)
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.unblockJob(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, with: .init())
)
context.client.send(.unblockJob(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, with: .init())) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testJobsLogOutput() throws {
func testJobsLogOutput() async throws {
let expected = Job.LogOutput()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.logOutput(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.logOutput(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testJobsLogOutputAlternativePlainText() throws {
func testJobsLogOutputAlternativePlainText() async throws {
let expected = "hello friends"
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.logOutput(.plainText, for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.logOutput(.plainText, for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testJobsLogOutputAlternativeHTML() throws {
func testJobsLogOutputAlternativeHTML() async throws {
let expected = "hello friends"
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.logOutput(.html, for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.logOutput(.html, for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testJobsDeleteLogOutput() throws {
func testJobsDeleteLogOutput() async throws {
let context = MockContext()
let expectation = XCTestExpectation()
context.client.send(.deleteLogOutput(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
_ = try await context.client.send(
.deleteLogOutput(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
}
func testJobsEnvironmentVariables() throws {
func testJobsEnvironmentVariables() async throws {
let expected = Job.EnvironmentVariables(env: [:])
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(
.environmentVariables(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
)
context.client.send(.environmentVariables(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}
extension Job.LogOutput {
init() {
self.init(url: Followable(),
self.init(
url: Followable(),
content: "hello friends",
size: 13,
headerTimes: [])
headerTimes: []
)
}
}

View File

@ -0,0 +1,27 @@
//
// MetasTests.swift
// Buildkite
//
// Created by Aaron Sky on 5/29/22.
// Copyright © 2022 Aaron Sky. All rights reserved.
//
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
class MetasTests: XCTestCase {
func testMetaGet() async throws {
let expected = Meta(webhookIPRanges: ["1.1.1.1/32"])
let context = try MockContext(content: expected)
let response = try await context.client.send(.meta)
XCTAssertEqual(expected, response.content)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,7 +17,8 @@ import FoundationNetworking
extension Organization {
init() {
self.init(id: UUID(),
self.init(
id: UUID(),
url: Followable(),
webUrl: URL(),
name: "Buildkite",
@ -24,44 +26,27 @@ extension Organization {
pipelinesUrl: Followable(),
agentsUrl: Followable(),
emojisUrl: Followable(),
createdAt: Date(timeIntervalSince1970: 1000))
createdAt: Date(timeIntervalSince1970: 1000)
)
}
}
class OrganizationsTests: XCTestCase {
func testOrganizationsList() throws {
func testOrganizationsList() async throws {
let expected = [Organization()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.organizations)
context.client.send(.organizations) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testOrganizationsGet() throws {
func testOrganizationsGet() async throws {
let expected = Organization()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.organization("buildkite"))
context.client.send(.organization("buildkite")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -15,8 +16,11 @@ import FoundationNetworking
#endif
extension Pipeline {
init(steps: [Step] = []) {
self.init(id: UUID(),
init(
steps: [Step] = []
) {
self.init(
id: UUID(),
url: Followable(),
webUrl: URL(),
name: "My Pipeline",
@ -24,9 +28,11 @@ extension Pipeline {
repository: "git@github.com:buildkite/agent.git",
branchConfiguration: nil,
defaultBranch: "master",
provider: Provider(id: "github",
provider: Provider(
id: "github",
webhookUrl: URL(),
settings: Provider.Settings(repository: nil,
settings: Provider.Settings(
repository: nil,
buildPullRequests: nil,
pullRequestBranchFilterEnabled: nil,
pullRequestBranchFilterConfiguration: nil,
@ -40,7 +46,9 @@ extension Pipeline {
buildPullRequestForks: nil,
prefixPullRequestForkBranchNames: nil,
separatePullRequestStatuses: nil,
publishBlockedAsPending: nil)),
publishBlockedAsPending: nil
)
),
skipQueuedBranchBuilds: false,
skipQueuedBranchBuildsFilter: nil,
cancelRunningBranchBuilds: false,
@ -55,77 +63,63 @@ extension Pipeline {
waitingJobsCount: 0,
visibility: "private",
steps: steps,
env: [:])
env: [:]
)
}
}
class PipelinesTests: XCTestCase {
func testPipelinesList() throws {
func testPipelinesList() async throws {
let expected = [Pipeline(), Pipeline()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.pipelines(in: "buildkite"))
context.client.send(.pipelines(in: "buildkite")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testPipelinesGet() throws {
func testPipelinesGet() async throws {
let expected = Pipeline()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.pipeline("buildkite", in: "organization"))
context.client.send(.pipeline("buildkite", in: "organization")) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
func testPipelinesCreate() throws {
func testPipelinesCreate() async throws {
let steps: [Pipeline.Step] = [
.script(Pipeline.Step.Command(name: "📦",
.script(
Pipeline.Step.Command(
name: "📦",
command: "echo true",
label: "📦",
env: [:],
agentQueryRules: []))
agentQueryRules: []
)
)
]
let expected = Pipeline(steps: steps)
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
context.client.send(.createPipeline(.init(name: "My Pipeline",
_ = try await context.client.send(
.createPipeline(
.init(
name: "My Pipeline",
repository: URL(),
configuration: "steps:\n\t- label: \"📦\"\n\t command: \"echo true\""),
in: "buildkite")) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
configuration: "steps:\n\t- label: \"📦\"\n\t command: \"echo true\""
),
in: "buildkite"
)
)
}
func testPipelinesCreateVisualSteps() throws {
func testPipelinesCreateVisualSteps() async throws {
let steps: [Pipeline.Step] = [
.script(Pipeline.Step.Command(name: "📦",
.script(
Pipeline.Step.Command(
name: "📦",
command: "echo true",
label: "📦",
artifactPaths: "*",
@ -135,21 +129,33 @@ class PipelinesTests: XCTestCase {
agentQueryRules: [],
async: nil,
concurrency: nil,
parallelism: nil)),
.waiter(Pipeline.Step.Wait(label: "wait",
continueAfterFailure: true)),
parallelism: nil
)
),
.waiter(
Pipeline.Step.Wait(
label: "wait",
continueAfterFailure: true
)
),
.manual(Pipeline.Step.Block(label: "manual")),
.trigger(Pipeline.Step.Trigger(triggerProjectSlug: "my-other-pipeline",
.trigger(
Pipeline.Step.Trigger(
triggerProjectSlug: "my-other-pipeline",
label: "trigger",
triggerCommit: nil,
triggerBranch: nil,
triggerAsync: nil))
triggerAsync: nil
)
),
]
let expected = Pipeline(steps: steps)
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
context.client.send(.createVisualStepsPipeline(.init(name: "My Pipeline",
_ = try await context.client.send(
.createVisualStepsPipeline(
.init(
name: "My Pipeline",
repository: URL(),
steps: steps,
branchConfiguration: nil,
@ -161,26 +167,23 @@ class PipelinesTests: XCTestCase {
providerSettings: nil,
skipQueuedBranchBuilds: nil,
skipQueuedBranchBuildsFilter: nil,
teamUUIDs: nil),
in: "buildkite")) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
teamUUIDs: nil
),
in: "buildkite"
)
)
}
func testPipelinesUpdate() throws {
func testPipelinesUpdate() async throws {
let expected = Pipeline()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
context.client.send(.updatePipeline("my-pipeline",
_ = try await context.client.send(
.updatePipeline(
"my-pipeline",
in: "buildkite",
with: .init(branchConfiguration: nil,
with: .init(
branchConfiguration: nil,
cancelRunningBranchBuilds: nil,
cancelRunningBranchBuildsFilter: nil,
defaultBranch: nil,
@ -192,30 +195,15 @@ class PipelinesTests: XCTestCase {
steps: nil,
skipQueuedBranchBuilds: nil,
skipQueuedBranchBuildsFilter: nil,
visibility: nil))) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
visibility: nil
)
)
)
}
func testPipelinesDelete() throws {
func testPipelinesDelete() async throws {
let context = MockContext()
let expectation = XCTestExpectation()
context.client.send(.deletePipeline("my-pipeline", in: "buildkite")) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
_ = try await context.client.send(.deletePipeline("my-pipeline", in: "buildkite"))
}
}

View File

@ -8,41 +8,35 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
private extension Team {
init() {
self.init(id: UUID(),
extension Team {
fileprivate init() {
self.init(
id: UUID(),
name: "",
slug: "",
description: "",
privacy: .visible,
default: true,
createdAt: Date(timeIntervalSince1970: 1000),
createdBy: User())
createdBy: User()
)
}
}
class TeamsTests: XCTestCase {
func testTeamsList() throws {
func testTeamsList() async throws {
let expected = [Team(), Team()]
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.teams(in: "buildkite", byUser: UUID()))
context.client.send(.teams(in: "buildkite", byUser: UUID())) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import XCTest
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -16,30 +17,23 @@ import FoundationNetworking
extension User {
init() {
self.init(id: UUID(),
self.init(
id: UUID(),
name: "Jeff",
email: "jeff@buildkite.com",
avatarUrl: URL(),
createdAt: Date(timeIntervalSince1970: 1000))
createdAt: Date(timeIntervalSince1970: 1000)
)
}
}
class UsersTests: XCTestCase {
func testUserMe() throws {
func testUserMe() async throws {
let expected = User()
let context = try MockContext(content: expected)
let expectation = XCTestExpectation()
let response = try await context.client.send(.me)
context.client.send(.me) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
}
}

View File

@ -6,8 +6,8 @@
// Copyright © 2020 Aaron Sky. All rights reserved.
//
import Foundation
import Buildkite
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
@ -19,21 +19,34 @@ struct MockContext {
init() {
let configuration = Configuration.default
self.init(configuration: configuration, responses: [
self.init(
configuration: configuration,
responses: [
MockData.mockingSuccessNoContent(url: configuration.version.baseURL)
])
]
)
}
init<Content: Codable>(content: Content...) throws {
init<Content: Codable>(
content: Content...
) throws {
let configuration = Configuration.default
try self.init(configuration: configuration, responses: content.map {
try self.init(
configuration: configuration,
responses: content.map {
try MockData.mockingSuccess(with: $0, url: configuration.version.baseURL)
})
}
)
}
private init(configuration: Configuration, responses: [(Data, URLResponse)]) {
private init(
configuration: Configuration,
responses: [(Data, URLResponse)]
) {
let transport = MockTransport(responses: responses)
client = BuildkiteClient(configuration: configuration,
transport: transport)
client = BuildkiteClient(
configuration: configuration,
transport: transport
)
}
}

View File

@ -7,6 +7,7 @@
//
import Foundation
@testable import Buildkite
#if canImport(FoundationNetworking)
@ -143,9 +144,14 @@ extension MockData {
}
private static func urlResponse(for url: URL, rawStatus status: Int) -> URLResponse {
HTTPURLResponse(url: url,
HTTPURLResponse(
url: url,
statusCode: status,
httpVersion: "HTTP/1.1",
headerFields: ["Link": #"<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=3>; rel="prev",<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=5>; rel="next",<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=1>; rel="first", <https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=10>; rel="last""#])!
headerFields: [
"Link":
#"<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=3>; rel="prev",<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=5>; rel="next",<https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=1>; rel="first", <https://api.buildkite.com/v2/organizations/my-great-org/pipelines/my-pipeline/builds?api_key=f8582f070276d764ce3dd4c6d57be92574dccf86&page=10>; rel="last""#
]
)!
}
}

View File

@ -6,8 +6,8 @@
// Copyright © 2020 Aaron Sky. All rights reserved.
//
import Foundation
import Buildkite
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
@ -25,7 +25,9 @@ final class MockTransport {
var history: [URLRequest] = []
var responses: [Transport.Output]
init(responses: [Transport.Output]) {
init(
responses: [Transport.Output]
) {
self.responses = responses
}
}
@ -40,7 +42,7 @@ extension MockTransport: Transport {
completion(.success(responses.removeFirst()))
}
#if canImport(Combine)
#if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
func sendPublisher(request: URLRequest) -> AnyPublisher<Output, Swift.Error> {
history.append(request)
@ -53,11 +55,12 @@ extension MockTransport: Transport {
return
}
promise(.success(self.responses.removeFirst()))
}.eraseToAnyPublisher()
}
#endif
.eraseToAnyPublisher()
}
#endif
#if compiler(>=5.5.2) && canImport(_Concurrency)
#if compiler(>=5.5.2) && canImport(_Concurrency)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
func send(request: URLRequest) async throws -> Output {
history.append(request)
@ -66,5 +69,5 @@ extension MockTransport: Transport {
}
return responses.removeFirst()
}
#endif
#endif
}