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 .DS_Store
/.build /.build
/Example/.build /.vscode
/Examples/.build
/Packages /Packages
/*.xcodeproj /*.xcodeproj
xcuserdata/ 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 <Workspace
version = "1.0"> version = "1.0">
<FileRef <FileRef
location = "container:../.."> location = "self:">
</FileRef> </FileRef>
</Workspace> </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), .iOS(.v10),
.macOS(.v10_12), .macOS(.v10_12),
.tvOS(.v10), .tvOS(.v10),
.watchOS(.v3) .watchOS(.v3),
], ],
products: [ products: [
.library( .library(
name: "Buildkite", name: "Buildkite",
targets: ["Buildkite"]) targets: ["Buildkite"]
)
], ],
targets: [ targets: [
.target( .target(
@ -34,6 +35,13 @@ let package = Package(
.testTarget( .testTarget(
name: "BuildkiteTests", name: "BuildkiteTests",
dependencies: ["Buildkite"] 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`` - ``Build/Resources``
- ``Emoji/Resources`` - ``Emoji/Resources``
- ``Job/Resources`` - ``Job/Resources``
- ``Meta/Resources``
- ``Organization/Resources`` - ``Organization/Resources``
- ``Pipeline/Resources`` - ``Pipeline/Resources``
- ``Team/Resources`` - ``Team/Resources``
@ -50,6 +51,7 @@ The entire publicly documented REST API surface is supported by this package.
- ``Build`` - ``Build``
- ``Emoji`` - ``Emoji``
- ``Job`` - ``Job``
- ``Meta``
- ``Organization`` - ``Organization``
- ``Pipeline`` - ``Pipeline``
- ``Team`` - ``Team``

View File

@ -47,12 +47,17 @@ public final class BuildkiteClient {
/// - Parameters: /// - 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. /// - 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. /// - 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.configuration = configuration
self.transport = transport 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 return { [weak self] result in
guard let self = self else { guard let self = self else {
return 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 return { [weak self] result in
guard let self = self else { guard let self = self else {
return return
@ -91,14 +98,16 @@ public final class BuildkiteClient {
private func checkResponseForIssues(_ response: URLResponse, data: Data? = nil) throws { private func checkResponseForIssues(_ response: URLResponse, data: Data? = nil) throws {
guard let httpResponse = response as? HTTPURLResponse, guard let httpResponse = response as? HTTPURLResponse,
let statusCode = StatusCode(rawValue: httpResponse.statusCode) else { let statusCode = StatusCode(rawValue: httpResponse.statusCode)
throw ResponseError.incompatibleResponse(response) else {
} throw ResponseError.incompatibleResponse(response)
}
if !statusCode.isSuccess { if !statusCode.isSuccess {
guard let data = data, 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)
throw statusCode else {
} throw statusCode
}
throw BuildkiteError(statusCode: statusCode, intermediary: errorIntermediary) throw BuildkiteError(statusCode: statusCode, intermediary: errorIntermediary)
} }
} }
@ -106,12 +115,13 @@ public final class BuildkiteClient {
// MARK: - Closure API // MARK: - Closure API
public extension BuildkiteClient { extension BuildkiteClient {
/// Performs the given resource asynchronously, then calls a handler upon completion. /// Performs the given resource asynchronously, then calls a handler upon completion.
/// - Parameters: /// - Parameters:
/// - resource:A resource. /// - 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. /// - 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 { do {
let request = try URLRequest(resource, configuration: configuration) let request = try URLRequest(resource, configuration: configuration)
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
@ -125,7 +135,11 @@ public extension BuildkiteClient {
/// - resource:A resource. /// - resource:A resource.
/// - pageOptions: Page options to perform pagination. /// - 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. /// - 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 { do {
let request = try URLRequest(resource, configuration: configuration, pageOptions: pageOptions) let request = try URLRequest(resource, configuration: configuration, pageOptions: pageOptions)
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
@ -138,7 +152,8 @@ public extension BuildkiteClient {
/// - Parameters: /// - Parameters:
/// - resource:A resource. /// - 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. /// - 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 { do {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder) let request = try URLRequest(resource, configuration: configuration, encoder: encoder)
transport.send(request: request, completion: handleContentfulResponse(completion: completion)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
@ -152,9 +167,18 @@ public extension BuildkiteClient {
/// - resource:A resource. /// - resource:A resource.
/// - pageOptions: Page options to perform pagination. /// - 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. /// - 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 { 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)) transport.send(request: request, completion: handleContentfulResponse(completion: completion))
} catch { } catch {
completion(.failure(error)) completion(.failure(error))
@ -165,7 +189,8 @@ public extension BuildkiteClient {
/// - Parameters: /// - Parameters:
/// - resource:A resource. /// - 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. /// - 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 { do {
let request = try URLRequest(resource, configuration: configuration) let request = try URLRequest(resource, configuration: configuration)
transport.send(request: request, completion: handleEmptyResponse(completion: completion)) transport.send(request: request, completion: handleEmptyResponse(completion: completion))
@ -178,7 +203,8 @@ public extension BuildkiteClient {
/// - Parameters: /// - Parameters:
/// - resource:A resource. /// - 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. /// - 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 { do {
let request = try URLRequest(resource, configuration: configuration, encoder: encoder) let request = try URLRequest(resource, configuration: configuration, encoder: encoder)
transport.send(request: request, completion: handleEmptyResponse(completion: completion)) transport.send(request: request, completion: handleEmptyResponse(completion: completion))
@ -191,7 +217,7 @@ public extension BuildkiteClient {
/// - Parameters: /// - Parameters:
/// - resource:A resource. /// - 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. /// - 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 send(resource) { result in
do { do {
switch (try result.get()).content { switch (try result.get()).content {
@ -213,20 +239,21 @@ public extension BuildkiteClient {
import Combine import Combine
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public extension BuildkiteClient { extension BuildkiteClient {
/// Performs the given resource and publishes the response asynchronously. /// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error. /// - 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) } Result { try URLRequest(resource, configuration: configuration) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response, data: $0.data) try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
/// Performs the given resource and publishes the response asynchronously. /// Performs the given resource and publishes the response asynchronously.
@ -234,31 +261,35 @@ public extension BuildkiteClient {
/// - resource: A resource. /// - resource: A resource.
/// - pageOptions: Page options to perform pagination. /// - 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. /// - 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) } Result { try URLRequest(resource, configuration: configuration, pageOptions: pageOptions) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response, data: $0.data) try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
/// Performs the given resource and publishes the response asynchronously. /// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error. /// - 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) } Result { try URLRequest(resource, configuration: configuration, encoder: encoder) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response, data: $0.data) try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
/// Performs the given resource and publishes the response asynchronously. /// Performs the given resource and publishes the response asynchronously.
@ -266,50 +297,55 @@ public extension BuildkiteClient {
/// - resource: A resource. /// - resource: A resource.
/// - pageOptions: Page options to perform pagination. /// - 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. /// - 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) } Result { try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response, data: $0.data) try self.checkResponseForIssues($0.response, data: $0.data)
let content = try self.decoder.decode(R.Content.self, from: $0.data) let content = try self.decoder.decode(R.Content.self, from: $0.data)
return Response(content: content, response: $0.response) return Response(content: content, response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
/// Performs the given resource and publishes the response asynchronously. /// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error. /// - 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) } Result { try URLRequest(resource, configuration: configuration) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response) try self.checkResponseForIssues($0.response)
return Response(content: (), response: $0.response) return Response(content: (), response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
/// Performs the given resource and publishes the response asynchronously. /// Performs the given resource and publishes the response asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: The publisher publishes the response when the operation completes, or terminates if the operation fails with an error. /// - 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) } Result { try URLRequest(resource, configuration: configuration, encoder: encoder) }
.publisher .publisher
.flatMap(transport.sendPublisher) .flatMap(transport.sendPublisher)
.tryMap { .tryMap {
try self.checkResponseForIssues($0.response) try self.checkResponseForIssues($0.response)
return Response(content: (), response: $0.response) return Response(content: (), response: $0.response)
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
/// Performs the given GraphQL query or mutation and publishes the content asynchronously. /// Performs the given GraphQL query or mutation and publishes the content asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: The publisher publishes the content when the operation completes, or terminates if the operation fails with an error. /// - 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) sendPublisher(resource)
.map(\.content) .map(\.content)
.tryMap { try $0.get() } .tryMap { try $0.get() }
@ -323,11 +359,11 @@ public extension BuildkiteClient {
// MARK: - Async/Await API // MARK: - Async/Await API
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public extension BuildkiteClient { extension BuildkiteClient {
/// Performs the given resource asynchronously. /// Performs the given resource asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation. /// - 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 request = try URLRequest(resource, configuration: configuration)
let (data, response) = try await transport.send(request: request) let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data) try checkResponseForIssues(response, data: data)
@ -340,7 +376,8 @@ public extension BuildkiteClient {
/// - resource: A resource. /// - resource: A resource.
/// - pageOptions: Page options to perform pagination. /// - 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. /// - 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 request = try URLRequest(resource, configuration: configuration, pageOptions: pageOptions)
let (data, response) = try await transport.send(request: request) let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data) try checkResponseForIssues(response, data: data)
@ -351,7 +388,8 @@ public extension BuildkiteClient {
/// Performs the given resource asynchronously. /// Performs the given resource asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation. /// - 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 request = try URLRequest(resource, configuration: configuration, encoder: encoder)
let (data, response) = try await transport.send(request: request) let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data) try checkResponseForIssues(response, data: data)
@ -364,7 +402,8 @@ public extension BuildkiteClient {
/// - resource: A resource. /// - resource: A resource.
/// - pageOptions: Page options to perform pagination. /// - 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. /// - 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 request = try URLRequest(resource, configuration: configuration, encoder: encoder, pageOptions: pageOptions)
let (data, response) = try await transport.send(request: request) let (data, response) = try await transport.send(request: request)
@ -376,7 +415,7 @@ public extension BuildkiteClient {
/// Performs the given resource asynchronously. /// Performs the given resource asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: A response containing the content of the response body, as well as other information about the HTTP operation. /// - 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 request = try URLRequest(resource, configuration: configuration)
let (data, response) = try await transport.send(request: request) let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data) try checkResponseForIssues(response, data: data)
@ -386,7 +425,8 @@ public extension BuildkiteClient {
/// Performs the given resource asynchronously. /// Performs the given resource asynchronously.
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: A response containing information about the HTTP operation, and no content. /// - 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 request = try URLRequest(resource, configuration: configuration, encoder: encoder)
let (data, response) = try await transport.send(request: request) let (data, response) = try await transport.send(request: request)
try checkResponseForIssues(response, data: data) try checkResponseForIssues(response, data: data)
@ -397,7 +437,7 @@ public extension BuildkiteClient {
/// - Parameter resource: A resource. /// - Parameter resource: A resource.
/// - Returns: Content of the resolved GraphQL operation. /// - Returns: Content of the resolved GraphQL operation.
/// - Throws: An error either of type ``BuildkiteError`` or ``GraphQL/Errors``. /// - 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) let response = try await send(resource)
return try response.content.get() return try response.content.get()
} }

View File

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

View File

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

View File

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

View File

@ -49,7 +49,12 @@ enum Formatters {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self) let dateString = try container.decode(String.self)
guard let date = dateIfPossible(fromISO8601: dateString) else { 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 return date
} }

View File

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

View File

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

View File

@ -43,11 +43,16 @@ extension Resource where Body == Void {
public protocol PaginatedResource: Resource where Content: Decodable {} public protocol PaginatedResource: Resource where Content: Decodable {}
extension URLRequest { extension URLRequest {
init<R: Resource>(_ resource: R, configuration: Configuration) throws { init<R: Resource>(
_ resource: R,
configuration: Configuration
) throws {
let version = resource.version let version = resource.version
guard version == configuration.version guard
|| version == configuration.graphQLVersion else { version == configuration.version
throw ResourceError.incompatibleVersion(version) || version == configuration.graphQLVersion
else {
throw ResourceError.incompatibleVersion(version)
} }
let url = version.url(for: resource.path) let url = version.url(for: resource.path)
@ -58,19 +63,32 @@ extension URLRequest {
self = request 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) try self.init(resource, configuration: configuration)
httpBody = try encoder.encode(resource.body) 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) try self.init(resource, configuration: configuration)
if let options = pageOptions { if let options = pageOptions {
appendPageOptions(options) 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) try self.init(resource, configuration: configuration, encoder: encoder)
if let options = pageOptions { if let options = pageOptions {
appendPageOptions(options) appendPageOptions(options)

View File

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

View File

@ -26,15 +26,15 @@ public protocol Transport {
func send(request: URLRequest, completion: @escaping Completion) func send(request: URLRequest, completion: @escaping Completion)
#if canImport(Combine) #if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
func sendPublisher(request: URLRequest) -> AnyPublisher<Output, Error> 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, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
func send(request: URLRequest) async throws -> Output func send(request: URLRequest) async throws -> Output
#endif #endif
} }
extension URLSession: Transport { extension URLSession: Transport {
@ -53,21 +53,19 @@ extension URLSession: Transport {
task.resume() task.resume()
} }
#if canImport(Combine) #if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func sendPublisher(request: URLRequest) -> AnyPublisher<Output, Error> { public func sendPublisher(request: URLRequest) -> AnyPublisher<Output, Error> {
dataTaskPublisher(for: request) dataTaskPublisher(for: request)
.mapError { $0 as Error } .mapError { $0 as Error }
.eraseToAnyPublisher() .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, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func send(request: URLRequest) async throws -> Output { public func send(request: URLRequest) async throws -> Output {
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { guard #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) else {
return try await data(for: request)
} else {
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
let task = dataTask(with: request) { data, response, error in let task = dataTask(with: request) { data, response, error in
if let error = error { if let error = error {
@ -76,8 +74,9 @@ extension URLSession: Transport {
} }
guard let data = data, guard let data = data,
let response = response else { let response = response
continuation.resume(throwing: TransportError.noResponse) else {
continuation.resume(throwing: TransportError.noResponse)
return return
} }
@ -86,6 +85,7 @@ extension URLSession: Transport {
task.resume() 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 { } else if items.count == 1 {
appendIfNeeded(items.first, forKey: key) appendIfNeeded(items.first, forKey: key)
} else { } else {
append(contentsOf: items append(
.enumerated() contentsOf:
.map { items
URLQueryItem(name: "\(key)\(arrayFormat.format(for: $0.offset))", .enumerated()
value: $0.element) .map {
}) URLQueryItem(
name: "\(key)\(arrayFormat.format(for: $0.offset))",
value: $0.element
)
}
)
} }
} }
mutating func append(_ items: [String: String], forKey key: String) { mutating func append(_ items: [String: String], forKey key: String) {
append(contentsOf: items.map { append(
URLQueryItem(name: "\(key)[\($0.key)]", contentsOf: items.map {
value: $0.value) URLQueryItem(
}) name: "\(key)[\($0.key)]",
value: $0.value
)
}
)
} }
} }
extension Date: LosslessStringConvertible { extension Date: LosslessStringConvertible {
public init?(_ description: String) { public init?(
_ description: String
) {
guard let date = Formatters.dateIfPossible(fromISO8601: description) else { guard let date = Formatters.dateIfPossible(fromISO8601: description) else {
return nil return nil
} }
@ -67,7 +78,9 @@ extension Date: LosslessStringConvertible {
} }
extension UUID: LosslessStringConvertible { extension UUID: LosslessStringConvertible {
public init?(_ description: String) { public init?(
_ description: String
) {
guard let id = UUID(uuidString: description) else { guard let id = UUID(uuidString: description) else {
return nil return nil
} }

View File

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

View File

@ -33,7 +33,11 @@ extension Annotation.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/annotations" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build

View File

@ -33,7 +33,11 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/artifacts" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -58,7 +62,12 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -84,7 +93,13 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts/\(artifactId)" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -113,7 +128,13 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts/\(artifactId)/download" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -141,7 +162,13 @@ extension Artifact.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(jobId)/artifacts/\(artifactId)" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -174,13 +201,25 @@ extension Resource where Self == Artifact.Resources.Get {
} }
extension Resource where Self == Artifact.Resources.Download { 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) Self(organization: organization, pipeline: pipeline, build: build, jobId: job, artifactId: id)
} }
} }
extension Resource where Self == Artifact.Resources.Delete { 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) Self(organization: organization, pipeline: pipeline, build: build, jobId: job, artifactId: id)
} }
} }

View File

@ -27,14 +27,17 @@ extension Build.Resources {
public var queryOptions: QueryOptions? public var queryOptions: QueryOptions?
public init(queryOptions: Build.Resources.QueryOptions? = nil) { public init(
queryOptions: Build.Resources.QueryOptions? = nil
) {
self.queryOptions = queryOptions self.queryOptions = queryOptions
} }
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url, guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
return else {
return
} }
var queryItems: [URLQueryItem] = [] var queryItems: [URLQueryItem] = []
if let options = queryOptions { if let options = queryOptions {
@ -60,15 +63,19 @@ extension Build.Resources {
"organizations/\(organization)/builds" "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.organization = organization
self.queryOptions = queryOptions self.queryOptions = queryOptions
} }
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url, guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
return else {
return
} }
var queryItems: [URLQueryItem] = [] var queryItems: [URLQueryItem] = []
if let options = queryOptions { if let options = queryOptions {
@ -95,7 +102,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.queryOptions = queryOptions self.queryOptions = queryOptions
@ -103,8 +114,9 @@ extension Build.Resources {
public func transformRequest(_ request: inout URLRequest) { public func transformRequest(_ request: inout URLRequest) {
guard let url = request.url, guard let url = request.url,
var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
return else {
return
} }
var queryItems: [URLQueryItem] = [] var queryItems: [URLQueryItem] = []
if let options = queryOptions { if let options = queryOptions {
@ -129,7 +141,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -151,7 +167,10 @@ extension Build.Resources {
public var name: String public var name: String
public var email: String public var email: String
public init(name: String, email: String) { public init(
name: String,
email: String
) {
self.name = name self.name = name
self.email = email self.email = email
} }
@ -181,7 +200,19 @@ extension Build.Resources {
/// For a pull request build, the git repository of the pull request. /// For a pull request build, the git repository of the pull request.
public var pullRequestRepository: String? 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.commit = commit
self.branch = branch self.branch = branch
self.author = author self.author = author
@ -201,7 +232,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.body = body self.body = body
@ -228,7 +263,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/cancel" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -255,7 +294,11 @@ extension Build.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/rebuild" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build 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. /// 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 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.branches = branches
self.commit = commit self.commit = commit
self.createdFrom = createdFrom self.createdFrom = createdFrom
@ -300,8 +353,10 @@ extension Build.Resources {
} }
} }
private extension Array where Element == URLQueryItem { extension Array where Element == URLQueryItem {
init(queryOptions: Build.Resources.QueryOptions) { fileprivate init(
queryOptions: Build.Resources.QueryOptions
) {
self.init() self.init()
append(queryOptions.branches, forKey: "branch") append(queryOptions.branches, forKey: "branch")
appendIfNeeded(queryOptions.commit, forKey: "commit") appendIfNeeded(queryOptions.commit, forKey: "commit")
@ -322,13 +377,18 @@ extension Resource where Self == Build.Resources.ListAll {
} }
extension Resource where Self == Build.Resources.ListForOrganization { 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) Self(organization: organization, queryOptions: options)
} }
} }
extension Resource where Self == Build.Resources.ListForPipeline { 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) Self(organization: organization, pipeline: pipeline, queryOptions: options)
} }
} }

View File

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

View File

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

View File

@ -27,7 +27,9 @@ public struct GraphQL<T: Decodable>: Resource {
case data(T) case data(T)
case errors(Errors) case errors(Errors)
public init(from decoder: Decoder) throws { public init(
from decoder: Decoder
) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
if let errors = try container.decodeIfPresent([Error].self, forKey: .errors) { if let errors = try container.decodeIfPresent([Error].self, forKey: .errors) {
let type = try container.decodeIfPresent(String.self, forKey: .type) let type = try container.decodeIfPresent(String.self, forKey: .type)
@ -38,7 +40,8 @@ public struct GraphQL<T: Decodable>: Resource {
throw DecodingError.dataCorrupted( throw DecodingError.dataCorrupted(
DecodingError.Context( DecodingError.Context(
codingPath: decoder.codingPath, 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 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)) 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" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -67,7 +72,10 @@ extension Job.Resources {
public var unblocker: UUID? public var unblocker: UUID?
public var fields: [String: String] 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.unblocker = unblocker
self.fields = fields self.fields = fields
} }
@ -77,7 +85,13 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/unblock" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -106,7 +120,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -129,7 +148,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -157,7 +181,12 @@ extension Job.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/env" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -189,7 +218,13 @@ extension Job.Resources.LogOutput {
"organizations/\(organization)/pipelines/\(pipeline)/builds/\(build)/jobs/\(job)/log.\(format)" "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.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.build = build self.build = build
@ -206,7 +241,13 @@ extension Resource where Self == Job.Resources.Retry {
} }
extension Resource where Self == Job.Resources.Unblock { 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) 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 { 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) Self(organization: organization, pipeline: pipeline, build: build, job: job)
} }
} }
extension Resource where Self == Job.Resources.LogOutput.Alternative { 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) 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)" "organizations/\(organization)"
} }
public init(organization: String) { public init(
organization: String
) {
self.organization = organization self.organization = organization
} }
} }

View File

@ -30,7 +30,9 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines" "organizations/\(organization)/pipelines"
} }
public init(organization: String) { public init(
organization: String
) {
self.organization = organization self.organization = organization
} }
} }
@ -47,7 +49,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)" "organizations/\(organization)/pipelines/\(pipeline)"
} }
public init(organization: String, pipeline: String) { public init(
organization: String,
pipeline: String
) {
self.organization = organization self.organization = organization
self.pipeline = pipeline 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. /// 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 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.name = name
self.repository = repository self.repository = repository
self.configuration = configuration self.configuration = configuration
@ -108,7 +126,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines" "organizations/\(organization)/pipelines"
} }
public init(organization: String, body: Body) { public init(
organization: String,
body: Body
) {
self.organization = organization self.organization = organization
self.body = body 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. /// 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 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.name = name
self.repository = repository self.repository = repository
self.steps = steps self.steps = steps
@ -176,7 +211,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines" "organizations/\(organization)/pipelines"
} }
public init(organization: String, body: Body) { public init(
organization: String,
body: Body
) {
self.organization = organization self.organization = organization
self.body = body self.body = body
} }
@ -226,7 +264,21 @@ extension Pipeline.Resources {
/// Whether the pipeline is visible to everyone, including users outside this organization. /// Whether the pipeline is visible to everyone, including users outside this organization.
public var visibility: String? 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.branchConfiguration = branchConfiguration
self.cancelRunningBranchBuilds = cancelRunningBranchBuilds self.cancelRunningBranchBuilds = cancelRunningBranchBuilds
self.cancelRunningBranchBuildsFilter = cancelRunningBranchBuildsFilter self.cancelRunningBranchBuildsFilter = cancelRunningBranchBuildsFilter
@ -247,7 +299,11 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)" "organizations/\(organization)/pipelines/\(pipeline)"
} }
public init(organization: String, pipeline: String, body: Body) { public init(
organization: String,
pipeline: String,
body: Body
) {
self.organization = organization self.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
self.body = body self.body = body
@ -266,7 +322,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/archive" "organizations/\(organization)/pipelines/\(pipeline)/archive"
} }
public init(organization: String, pipeline: String) { public init(
organization: String,
pipeline: String
) {
self.organization = organization self.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
} }
@ -288,7 +347,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/unarchive" "organizations/\(organization)/pipelines/\(pipeline)/unarchive"
} }
public init(organization: String, pipeline: String) { public init(
organization: String,
pipeline: String
) {
self.organization = organization self.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
} }
@ -309,7 +371,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)" "organizations/\(organization)/pipelines/\(pipeline)"
} }
public init(organization: String, pipeline: String) { public init(
organization: String,
pipeline: String
) {
self.organization = organization self.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
} }
@ -326,7 +391,10 @@ extension Pipeline.Resources {
"organizations/\(organization)/pipelines/\(pipeline)/webhook" "organizations/\(organization)/pipelines/\(pipeline)/webhook"
} }
public init(organization: String, pipeline: String) { public init(
organization: String,
pipeline: String
) {
self.organization = organization self.organization = organization
self.pipeline = pipeline self.pipeline = pipeline
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,99 +17,76 @@ import FoundationNetworking
extension Agent { extension Agent {
init() { init() {
let job = Job.script(Job.Command(id: UUID(), let job = Job.script(
name: "📦", Job.Command(
state: "passed", id: UUID(),
command: nil, name: "📦",
stepKey: nil, state: "passed",
buildUrl: URL(), command: nil,
webUrl: URL(), stepKey: nil,
logUrl: Followable(), buildUrl: URL(),
rawLogUrl: Followable(), webUrl: URL(),
artifactsUrl: URL(), logUrl: Followable(),
softFailed: false, rawLogUrl: Followable(),
exitStatus: 0, artifactsUrl: URL(),
artifactPaths: nil, softFailed: false,
agentQueryRules: [], exitStatus: 0,
agent: nil, artifactPaths: nil,
createdAt: Date(timeIntervalSince1970: 1000), agentQueryRules: [],
scheduledAt: Date(timeIntervalSince1970: 1000), agent: nil,
runnableAt: nil, createdAt: Date(timeIntervalSince1970: 1000),
startedAt: nil, scheduledAt: Date(timeIntervalSince1970: 1000),
finishedAt: nil, runnableAt: nil,
retried: false, startedAt: nil,
retriedInJobId: nil, finishedAt: nil,
retriesCount: nil, retried: false,
parallelGroupIndex: nil, retriedInJobId: nil,
parallelGroupTotal: nil)) retriesCount: nil,
parallelGroupIndex: nil,
parallelGroupTotal: nil
)
)
self.init(id: UUID(), self.init(
url: Followable(), id: UUID(),
webUrl: URL(), url: Followable(),
name: "jeffrey", webUrl: URL(),
connectionState: "connected", name: "jeffrey",
hostname: "jeffrey", connectionState: "connected",
ipAddress: "192.168.1.1", hostname: "jeffrey",
userAgent: "buildkite/host", ipAddress: "192.168.1.1",
version: "3.20.0", userAgent: "buildkite/host",
creator: User(), version: "3.20.0",
createdAt: Date(timeIntervalSince1970: 1000), creator: User(),
job: job, createdAt: Date(timeIntervalSince1970: 1000),
lastJobFinishedAt: nil, job: job,
priority: nil, lastJobFinishedAt: nil,
metaData: []) priority: nil,
metaData: []
)
} }
} }
class AgentsTests: XCTestCase { class AgentsTests: XCTestCase {
func testAgentsList() throws { func testAgentsList() async throws {
let expected = [Agent(), Agent()] let expected = [Agent(), Agent()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Agent()
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() let response = try await context.client.send(.agent(UUID(), in: "buildkite"))
context.client.send(.agent(UUID(), in: "buildkite")) { result in XCTAssertEqual(expected, response.content)
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
} }
func testAgentsStop() throws { func testAgentsStop() async throws {
let context = MockContext() let context = MockContext()
_ = try await context.client.send(.stopAgent(UUID(), in: "buildkite", force: true))
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])
} }
} }

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,31 +17,24 @@ import FoundationNetworking
extension Annotation { extension Annotation {
init() { init() {
self.init(id: UUID(), self.init(
context: "message", id: UUID(),
style: .info, context: "message",
bodyHtml: "<div></div>", style: .info,
createdAt: Date(timeIntervalSince1970: 1000), bodyHtml: "<div></div>",
updatedAt: Date(timeIntervalSince1970: 1001)) createdAt: Date(timeIntervalSince1970: 1000),
updatedAt: Date(timeIntervalSince1970: 1001)
)
} }
} }
class AnnotationsTests: XCTestCase { class AnnotationsTests: XCTestCase {
func testAnnotationsList() throws { func testAnnotationsList() async throws {
let expected = [Annotation(), Annotation()] let expected = [Annotation(), Annotation()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,106 +17,70 @@ import FoundationNetworking
extension Artifact { extension Artifact {
init() { init() {
self.init(id: UUID(), self.init(
jobId: UUID(), id: UUID(),
url: Followable(), jobId: UUID(),
downloadUrl: Followable(), url: Followable(),
state: .new, downloadUrl: Followable(),
path: "", state: .new,
dirname: "", path: "",
filename: "", dirname: "",
mimeType: "", filename: "",
fileSize: 0, mimeType: "",
sha1sum: "") fileSize: 0,
sha1sum: ""
)
} }
} }
class ArtifactsTests: XCTestCase { class ArtifactsTests: XCTestCase {
func testArtifactsListByBuild() throws { func testArtifactsListByBuild() async throws {
let expected = [Artifact(), Artifact()] let expected = [Artifact(), Artifact()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = [Artifact(), Artifact()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Artifact()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Artifact.URLs(url: URL())
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 context = MockContext()
let expectation = XCTestExpectation() _ = try await context.client.send(
.deleteArtifact(UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1, job: UUID())
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])
} }
} }

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,189 +17,135 @@ import FoundationNetworking
extension Build { extension Build {
init() { init() {
self.init(id: UUID(), self.init(
url: Followable(), id: UUID(),
webUrl: URL(), url: Followable(),
number: 1, webUrl: URL(),
state: .passed, number: 1,
blocked: false, state: .passed,
message: "a commit", blocked: false,
commit: "HEAD", message: "a commit",
branch: "master", commit: "HEAD",
env: [:], branch: "master",
source: "webhook", env: [:],
creator: User(), source: "webhook",
jobs: [], creator: User(),
createdAt: Date(timeIntervalSince1970: 1000), jobs: [],
scheduledAt: Date(timeIntervalSince1970: 1000), createdAt: Date(timeIntervalSince1970: 1000),
startedAt: Date(timeIntervalSince1970: 1000), scheduledAt: Date(timeIntervalSince1970: 1000),
finishedAt: Date(timeIntervalSince1970: 1001), startedAt: Date(timeIntervalSince1970: 1000),
metaData: [:], finishedAt: Date(timeIntervalSince1970: 1001),
pullRequest: [:], metaData: [:],
pipeline: Pipeline()) pullRequest: [:],
pipeline: Pipeline()
)
} }
} }
class BuildsTests: XCTestCase { class BuildsTests: XCTestCase {
func testBuildsListAllDefaultQuery() throws { func testBuildsListAllDefaultQuery() async throws {
let expected = [Build(), Build()] let expected = [Build(), Build()]
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() let response = try await context.client.send(.builds())
context.client.send(.builds()) { result in XCTAssertEqual(expected, response.content)
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 expected = [Build(), Build()]
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() let response = try await context.client.send(
.builds(
options: .init(
branches: ["master"],
commit: "HEAD",
createdFrom: Date(timeIntervalSince1970: 1000),
createdTo: Date(timeIntervalSince1970: 1000),
creator: UUID(),
finishedFrom: Date(timeIntervalSince1970: 1000),
includeRetriedJobs: true,
metadata: ["buildkite": "is cool"],
state: [.passed, .blocked, .failed]
)
)
)
context.client.send(.builds(options: .init(branches: ["master"], XCTAssertEqual(expected, response.content)
commit: "HEAD",
createdFrom: Date(timeIntervalSince1970: 1000),
createdTo: Date(timeIntervalSince1970: 1000),
creator: UUID(),
finishedFrom: Date(timeIntervalSince1970: 1000),
includeRetriedJobs: true,
metadata: ["buildkite": "is cool"],
state: [.passed, .blocked, .failed]))) { result in
do {
let response = try result.get()
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 expected = [Build(), Build()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = [Build(), Build()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Build()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Build()
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() 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,
env: nil,
ignorePipelineBranchFilters: nil,
message: nil,
metaData: nil,
pullRequestBaseBranch: nil,
pullRequestId: nil,
pullRequestRepository: nil
)
)
)
context.client.send(.createBuild(in: "buildkite", pipeline: "my-pipeline", with: .init(commit: "HEAD", XCTAssertEqual(expected, response.content)
branch: "master",
author: Build.Resources.Create.Body.Author(name: "", email: ""),
cleanCheckout: nil,
env: nil,
ignorePipelineBranchFilters: nil,
message: nil,
metaData: nil,
pullRequestBaseBranch: nil,
pullRequestId: nil,
pullRequestRepository: nil))) { result in
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
} }
func testBuildsCancel() throws { func testBuildsCancel() async throws {
let expected = Build() let expected = Build()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Build()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,27 +17,20 @@ import FoundationNetworking
extension Emoji { extension Emoji {
init() { init() {
self.init(name: "jeff", self.init(
url: URL()) name: "jeff",
url: URL()
)
} }
} }
class EmojisTests: XCTestCase { class EmojisTests: XCTestCase {
func testEmojisList() throws { func testEmojisList() async throws {
let expected = [Emoji(), Emoji()] let expected = [Emoji(), Emoji()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -21,29 +22,13 @@ extension Followable {
} }
class FollowableTests: XCTestCase { class FollowableTests: XCTestCase {
func testFollowable() throws { func testFollowable() async throws {
let expected = Organization() let expected = Organization()
let context = try MockContext(content: Organization(), expected) 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 XCTAssertEqual(expected, followableResponse.content)
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])
} }
} }

View File

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

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -15,172 +16,127 @@ import FoundationNetworking
#endif #endif
class JobsTests: XCTestCase { class JobsTests: XCTestCase {
func testJobsRetryWaiter() throws { func testJobsRetryWaiter() async throws {
let expected: Job = .waiter(Job.Wait(id: UUID())) let expected: Job = .waiter(Job.Wait(id: UUID()))
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
} }
func testJobsRetryTrigger() throws { func testJobsRetryTrigger() async throws {
let expected: Job = .trigger(Job.Trigger(name: nil, let expected: Job = .trigger(
state: nil, Job.Trigger(
buildUrl: URL(), name: nil,
webUrl: URL(), state: nil,
createdAt: Date(timeIntervalSince1970: 1000), buildUrl: URL(),
scheduledAt: nil, webUrl: URL(),
finishedAt: nil, createdAt: Date(timeIntervalSince1970: 1000),
runnableAt: nil, scheduledAt: nil,
triggeredBuild: Job.Trigger.TriggeredBuild(id: UUID(), finishedAt: nil,
number: 0, runnableAt: nil,
url: URL(), triggeredBuild: Job.Trigger.TriggeredBuild(
webUrl: URL()))) id: UUID(),
number: 0,
url: URL(),
webUrl: URL()
)
)
)
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
} }
func testJobsUnblock() throws { func testJobsUnblock() async throws {
let expected: Job = .manual(Job.Block(id: UUID(), let expected: Job = .manual(
label: "", Job.Block(
state: "", id: UUID(),
webUrl: nil, label: "",
unblockedBy: User(), state: "",
unblockedAt: Date(timeIntervalSince1970: 1000), webUrl: nil,
unblockable: true, unblockedBy: User(),
unblockUrl: URL())) unblockedAt: Date(timeIntervalSince1970: 1000),
unblockable: true,
unblockUrl: URL()
)
)
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Job.LogOutput()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = "hello friends"
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = "hello friends"
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 context = MockContext()
let expectation = XCTestExpectation() _ = try await context.client.send(
.deleteLogOutput(for: UUID(), in: "buildkite", pipeline: "my-pipeline", build: 1)
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])
} }
func testJobsEnvironmentVariables() throws { func testJobsEnvironmentVariables() async throws {
let expected = Job.EnvironmentVariables(env: [:]) let expected = Job.EnvironmentVariables(env: [:])
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
do {
let response = try result.get()
XCTAssertEqual(expected, response.content)
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
} }
} }
extension Job.LogOutput { extension Job.LogOutput {
init() { init() {
self.init(url: Followable(), self.init(
content: "hello friends", url: Followable(),
size: 13, content: "hello friends",
headerTimes: []) size: 13,
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 Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,52 +17,36 @@ import FoundationNetworking
extension Organization { extension Organization {
init() { init() {
self.init(id: UUID(), self.init(
url: Followable(), id: UUID(),
webUrl: URL(), url: Followable(),
name: "Buildkite", webUrl: URL(),
slug: "buildkite", name: "Buildkite",
pipelinesUrl: Followable(), slug: "buildkite",
agentsUrl: Followable(), pipelinesUrl: Followable(),
emojisUrl: Followable(), agentsUrl: Followable(),
createdAt: Date(timeIntervalSince1970: 1000)) emojisUrl: Followable(),
createdAt: Date(timeIntervalSince1970: 1000)
)
} }
} }
class OrganizationsTests: XCTestCase { class OrganizationsTests: XCTestCase {
func testOrganizationsList() throws { func testOrganizationsList() async throws {
let expected = [Organization()] let expected = [Organization()]
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() let response = try await context.client.send(.organizations)
context.client.send(.organizations) { result in XCTAssertEqual(expected, response.content)
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 expected = Organization()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -15,207 +16,194 @@ import FoundationNetworking
#endif #endif
extension Pipeline { extension Pipeline {
init(steps: [Step] = []) { init(
self.init(id: UUID(), steps: [Step] = []
url: Followable(), ) {
webUrl: URL(), self.init(
name: "My Pipeline", id: UUID(),
slug: "my-pipeline", url: Followable(),
repository: "git@github.com:buildkite/agent.git", webUrl: URL(),
branchConfiguration: nil, name: "My Pipeline",
defaultBranch: "master", slug: "my-pipeline",
provider: Provider(id: "github", repository: "git@github.com:buildkite/agent.git",
webhookUrl: URL(), branchConfiguration: nil,
settings: Provider.Settings(repository: nil, defaultBranch: "master",
buildPullRequests: nil, provider: Provider(
pullRequestBranchFilterEnabled: nil, id: "github",
pullRequestBranchFilterConfiguration: nil, webhookUrl: URL(),
skipPullRequestBuildsForExistingCommits: nil, settings: Provider.Settings(
buildTags: nil, repository: nil,
publishCommitStatus: nil, buildPullRequests: nil,
publishCommitStatusPerStep: nil, pullRequestBranchFilterEnabled: nil,
triggerMode: nil, pullRequestBranchFilterConfiguration: nil,
filterEnabled: nil, skipPullRequestBuildsForExistingCommits: nil,
filterCondition: nil, buildTags: nil,
buildPullRequestForks: nil, publishCommitStatus: nil,
prefixPullRequestForkBranchNames: nil, publishCommitStatusPerStep: nil,
separatePullRequestStatuses: nil, triggerMode: nil,
publishBlockedAsPending: nil)), filterEnabled: nil,
skipQueuedBranchBuilds: false, filterCondition: nil,
skipQueuedBranchBuildsFilter: nil, buildPullRequestForks: nil,
cancelRunningBranchBuilds: false, prefixPullRequestForkBranchNames: nil,
cancelRunningBranchBuildsFilter: nil, separatePullRequestStatuses: nil,
buildsUrl: Followable(), publishBlockedAsPending: nil
badgeUrl: URL(), )
createdAt: Date(timeIntervalSince1970: 1000), ),
scheduledBuildsCount: 0, skipQueuedBranchBuilds: false,
runningBuildsCount: 0, skipQueuedBranchBuildsFilter: nil,
scheduledJobsCount: 0, cancelRunningBranchBuilds: false,
runningJobsCount: 0, cancelRunningBranchBuildsFilter: nil,
waitingJobsCount: 0, buildsUrl: Followable(),
visibility: "private", badgeUrl: URL(),
steps: steps, createdAt: Date(timeIntervalSince1970: 1000),
env: [:]) scheduledBuildsCount: 0,
runningBuildsCount: 0,
scheduledJobsCount: 0,
runningJobsCount: 0,
waitingJobsCount: 0,
visibility: "private",
steps: steps,
env: [:]
)
} }
} }
class PipelinesTests: XCTestCase { class PipelinesTests: XCTestCase {
func testPipelinesList() throws { func testPipelinesList() async throws {
let expected = [Pipeline(), Pipeline()] let expected = [Pipeline(), Pipeline()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 expected = Pipeline()
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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] = [ let steps: [Pipeline.Step] = [
.script(Pipeline.Step.Command(name: "📦", .script(
command: "echo true", Pipeline.Step.Command(
label: "📦", name: "📦",
env: [:], command: "echo true",
agentQueryRules: [])) label: "📦",
env: [:],
agentQueryRules: []
)
)
] ]
let expected = Pipeline(steps: steps) let expected = Pipeline(steps: steps)
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() _ = try await context.client.send(
context.client.send(.createPipeline(.init(name: "My Pipeline", .createPipeline(
repository: URL(), .init(
configuration: "steps:\n\t- label: \"📦\"\n\t command: \"echo true\""), name: "My Pipeline",
in: "buildkite")) { result in repository: URL(),
do { configuration: "steps:\n\t- label: \"📦\"\n\t command: \"echo true\""
_ = try result.get() ),
} catch { in: "buildkite"
XCTFail(error.localizedDescription) )
} )
expectation.fulfill()
}
wait(for: [expectation])
} }
func testPipelinesCreateVisualSteps() throws { func testPipelinesCreateVisualSteps() async throws {
let steps: [Pipeline.Step] = [ let steps: [Pipeline.Step] = [
.script(Pipeline.Step.Command(name: "📦", .script(
command: "echo true", Pipeline.Step.Command(
label: "📦", name: "📦",
artifactPaths: "*", command: "echo true",
branchConfiguration: nil, label: "📦",
env: [:], artifactPaths: "*",
timeoutInMinutes: nil, branchConfiguration: nil,
agentQueryRules: [], env: [:],
async: nil, timeoutInMinutes: nil,
concurrency: nil, agentQueryRules: [],
parallelism: nil)), async: nil,
.waiter(Pipeline.Step.Wait(label: "wait", concurrency: nil,
continueAfterFailure: true)), parallelism: nil
)
),
.waiter(
Pipeline.Step.Wait(
label: "wait",
continueAfterFailure: true
)
),
.manual(Pipeline.Step.Block(label: "manual")), .manual(Pipeline.Step.Block(label: "manual")),
.trigger(Pipeline.Step.Trigger(triggerProjectSlug: "my-other-pipeline", .trigger(
label: "trigger", Pipeline.Step.Trigger(
triggerCommit: nil, triggerProjectSlug: "my-other-pipeline",
triggerBranch: nil, label: "trigger",
triggerAsync: nil)) triggerCommit: nil,
triggerBranch: nil,
triggerAsync: nil
)
),
] ]
let expected = Pipeline(steps: steps) let expected = Pipeline(steps: steps)
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() _ = try await context.client.send(
context.client.send(.createVisualStepsPipeline(.init(name: "My Pipeline", .createVisualStepsPipeline(
repository: URL(), .init(
steps: steps, name: "My Pipeline",
branchConfiguration: nil, repository: URL(),
cancelRunningBranchBuilds: nil, steps: steps,
cancelRunningBranchBuildsFilter: nil, branchConfiguration: nil,
defaultBranch: nil, cancelRunningBranchBuilds: nil,
description: nil, cancelRunningBranchBuildsFilter: nil,
env: nil, defaultBranch: nil,
providerSettings: nil, description: nil,
skipQueuedBranchBuilds: nil, env: nil,
skipQueuedBranchBuildsFilter: nil, providerSettings: nil,
teamUUIDs: nil), skipQueuedBranchBuilds: nil,
in: "buildkite")) { result in skipQueuedBranchBuildsFilter: nil,
do { teamUUIDs: nil
_ = try result.get() ),
} catch { in: "buildkite"
XCTFail(error.localizedDescription) )
} )
expectation.fulfill()
}
wait(for: [expectation])
} }
func testPipelinesUpdate() throws { func testPipelinesUpdate() async throws {
let expected = Pipeline() let expected = Pipeline()
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() _ = try await context.client.send(
context.client.send(.updatePipeline("my-pipeline", .updatePipeline(
in: "buildkite", "my-pipeline",
with: .init(branchConfiguration: nil, in: "buildkite",
cancelRunningBranchBuilds: nil, with: .init(
cancelRunningBranchBuildsFilter: nil, branchConfiguration: nil,
defaultBranch: nil, cancelRunningBranchBuilds: nil,
description: nil, cancelRunningBranchBuildsFilter: nil,
env: nil, defaultBranch: nil,
name: nil, description: nil,
providerSettings: nil, env: nil,
repository: nil, name: nil,
steps: nil, providerSettings: nil,
skipQueuedBranchBuilds: nil, repository: nil,
skipQueuedBranchBuildsFilter: nil, steps: nil,
visibility: nil))) { result in skipQueuedBranchBuilds: nil,
do { skipQueuedBranchBuildsFilter: nil,
_ = try result.get() visibility: nil
} catch { )
XCTFail(error.localizedDescription) )
} )
expectation.fulfill()
}
wait(for: [expectation])
} }
func testPipelinesDelete() throws { func testPipelinesDelete() async throws {
let context = MockContext() let context = MockContext()
let expectation = XCTestExpectation() _ = try await context.client.send(.deletePipeline("my-pipeline", in: "buildkite"))
context.client.send(.deletePipeline("my-pipeline", in: "buildkite")) { result in
do {
_ = try result.get()
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}
wait(for: [expectation])
} }
} }

View File

@ -8,41 +8,35 @@
import Foundation import Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
import FoundationNetworking import FoundationNetworking
#endif #endif
private extension Team { extension Team {
init() { fileprivate init() {
self.init(id: UUID(), self.init(
name: "", id: UUID(),
slug: "", name: "",
description: "", slug: "",
privacy: .visible, description: "",
default: true, privacy: .visible,
createdAt: Date(timeIntervalSince1970: 1000), default: true,
createdBy: User()) createdAt: Date(timeIntervalSince1970: 1000),
createdBy: User()
)
} }
} }
class TeamsTests: XCTestCase { class TeamsTests: XCTestCase {
func testTeamsList() throws { func testTeamsList() async throws {
let expected = [Team(), Team()] let expected = [Team(), Team()]
let context = try MockContext(content: expected) 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 XCTAssertEqual(expected, response.content)
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 Foundation
import XCTest import XCTest
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -16,30 +17,23 @@ import FoundationNetworking
extension User { extension User {
init() { init() {
self.init(id: UUID(), self.init(
name: "Jeff", id: UUID(),
email: "jeff@buildkite.com", name: "Jeff",
avatarUrl: URL(), email: "jeff@buildkite.com",
createdAt: Date(timeIntervalSince1970: 1000)) avatarUrl: URL(),
createdAt: Date(timeIntervalSince1970: 1000)
)
} }
} }
class UsersTests: XCTestCase { class UsersTests: XCTestCase {
func testUserMe() throws { func testUserMe() async throws {
let expected = User() let expected = User()
let context = try MockContext(content: expected) let context = try MockContext(content: expected)
let expectation = XCTestExpectation() let response = try await context.client.send(.me)
context.client.send(.me) { result in XCTAssertEqual(expected, response.content)
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. // Copyright © 2020 Aaron Sky. All rights reserved.
// //
import Foundation
import Buildkite import Buildkite
import Foundation
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
import FoundationNetworking import FoundationNetworking
@ -19,21 +19,34 @@ struct MockContext {
init() { init() {
let configuration = Configuration.default let configuration = Configuration.default
self.init(configuration: configuration, responses: [ self.init(
MockData.mockingSuccessNoContent(url: configuration.version.baseURL) 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 let configuration = Configuration.default
try self.init(configuration: configuration, responses: content.map { try self.init(
try MockData.mockingSuccess(with: $0, url: configuration.version.baseURL) 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) let transport = MockTransport(responses: responses)
client = BuildkiteClient(configuration: configuration, client = BuildkiteClient(
transport: transport) configuration: configuration,
transport: transport
)
} }
} }

View File

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
@testable import Buildkite @testable import Buildkite
#if canImport(FoundationNetworking) #if canImport(FoundationNetworking)
@ -143,9 +144,14 @@ extension MockData {
} }
private static func urlResponse(for url: URL, rawStatus status: Int) -> URLResponse { private static func urlResponse(for url: URL, rawStatus status: Int) -> URLResponse {
HTTPURLResponse(url: url, HTTPURLResponse(
statusCode: status, url: url,
httpVersion: "HTTP/1.1", statusCode: status,
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""#])! 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""#
]
)!
} }
} }

View File

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