Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonHLawrence committed Jan 22, 2024
1 parent a3cdfa6 commit 9841878
Show file tree
Hide file tree
Showing 17 changed files with 877 additions and 38 deletions.
102 changes: 102 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/SimpleRESTClient.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SimpleRESTClient"
BuildableName = "SimpleRESTClient"
BlueprintName = "SimpleRESTClient"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SimpleRESTClientTests"
BuildableName = "SimpleRESTClientTests"
BlueprintName = "SimpleRESTClientTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SimpleRESTClient"
BuildableName = "SimpleRESTClient"
BlueprintName = "SimpleRESTClient"
ReferencedContainer = "container:">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SimpleRESTClientTests"
BuildableName = "SimpleRESTClientTests"
BlueprintName = "SimpleRESTClientTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SimpleRESTClient"
BuildableName = "SimpleRESTClient"
BlueprintName = "SimpleRESTClient"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
45 changes: 24 additions & 21 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@
import PackageDescription

let package = Package(
name: "SimpleRESTClient",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SimpleRESTClient",
targets: ["SimpleRESTClient"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SimpleRESTClient",
dependencies: []),
.testTarget(
name: "SimpleRESTClientTests",
dependencies: ["SimpleRESTClient"]),
]
name: "SimpleRESTClient",
platforms: [
.iOS(.v15)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SimpleRESTClient",
targets: ["SimpleRESTClient"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SimpleRESTClient",
dependencies: []),
.testTarget(
name: "SimpleRESTClientTests",
dependencies: ["SimpleRESTClient"]),
]
)
122 changes: 122 additions & 0 deletions Sources/SimpleRESTClient/APIClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// APIClient.swift
// SimpleRESTClient
//
// Created by Simon Lawrence on 20/01/2024.
//

import Foundation

/// A type providing JSON coding for the specific API in use.
public protocol APICoding {
/// Create a JSON decoder compatible with the API.
/// - Returns: the decoder.
func makeDecoder() -> JSONDecoder
/// Create a JSON encoder compatible with the API.
/// - Returns: the encoder.
func makeEncoder() -> JSONEncoder
}

/// Default implementation of ``APICoding``.
public struct DefaultAPICoding: APICoding {

public init() { }

public func makeDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}

public func makeEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}
}

/// A general API client providing REST operations.
public struct APIClient {

private var transport: Transport
private var coding: APICoding

/// Initialize an API client with the supplied transport and coding.
/// - Parameters:
/// - transport: a transport to perform network operations.
/// - coding: an instance providing encoding and decoding of JSON data for the particular API.
public init(transport: Transport, coding: APICoding = DefaultAPICoding()) {
self.transport = transport
self.coding = coding
}

/// GET an item from the specified endpoint.
/// - Parameter endpoint: the endpoint to retrieve.
/// - Returns: the retrieved item.
public func get<Response: Decodable>(endpoint: Endpoint) async throws -> Response {
let response = try await transport.get(endpoint: endpoint)
let jsonDecoder = coding.makeDecoder()
return try jsonDecoder.decode(Response.self, from: response)
}

/// PUT an item to the specified endpoint, retrieving a response.
/// - Parameters:
/// - endpoint: the endpoint to put.
/// - value: the value to put.
/// - Returns: the decoded response.
public func put<Request: Encodable, Response: Decodable>(endpoint: Endpoint, value: Request) async throws -> Response {
let jsonEncoder = coding.makeEncoder()
let request = try jsonEncoder.encode(value)
let response = try await transport.put(endpoint: endpoint, data: request)
let jsonDecoder = coding.makeDecoder()
return try jsonDecoder.decode(Response.self, from: response)
}

//// PUT an item to the specified endpoint.
/// - Parameters:
/// - endpoint: the endpoint to put.
/// - value: the value to put.
public func put<Request: Encodable>(endpoint: Endpoint, value: Request) async throws {
let jsonEncoder = coding.makeEncoder()
let request = try jsonEncoder.encode(value)
_ = try await transport.put(endpoint: endpoint, data: request)
}

/// POST an item to the specified endpoint, retrieving a response.
/// - Parameters:
/// - endpoint: the endpoint to post.
/// - value: the value to put.
/// - Returns: the decoded response.
public func post<Request: Encodable, Response: Decodable>(endpoint: Endpoint, value: Request) async throws -> Response {
let jsonEncoder = coding.makeEncoder()
let request = try jsonEncoder.encode(value)
let response = try await transport.post(endpoint: endpoint, data: request)
let jsonDecoder = coding.makeDecoder()
return try jsonDecoder.decode(Response.self, from: response)
}

/// POST an item to the specified endpoint.
/// - Parameters:
/// - endpoint: the endpoint to post.
/// - value: the value to put.
public func post<Request: Encodable>(endpoint: Endpoint, value: Request) async throws {
let jsonEncoder = coding.makeEncoder()
let request = try jsonEncoder.encode(value)
_ = try await transport.post(endpoint: endpoint, data: request)
}

/// DELETE the specified item, retrieving a response.
/// - Parameter endpoint: the endpoint to delete.
/// - Returns: the decoded response.
public func delete<Response: Decodable>(endpoint: Endpoint) async throws -> Response {
let response = try await transport.delete(endpoint: endpoint)
let jsonDecoder = coding.makeDecoder()
return try jsonDecoder.decode(Response.self, from: response)
}

/// DELETE the specified item.
/// - Parameter endpoint: the endpoint to delete.
public func delete(endpoint: Endpoint) async throws {
_ = try await transport.delete(endpoint: endpoint)
}
}
64 changes: 64 additions & 0 deletions Sources/SimpleRESTClient/HTTPDefnitions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// HTTPDefinitions.swift
// SimpleRESTClient
//
// Created by Simon Lawrence on 20/01/2024.
//

import Foundation

public struct HTTPMethod {

static var get = "GET"
static var delete = "DELETE"
static var post = "POST"
static var put = "PUT"
}

public struct HTTPStatus {

static var ok = 200
static var created = 201
static var noContent = 204
}

public struct HTTPHeader {

static var contentType = "Content-Type"
static var contentLength = "Content-Length"
static var accept = "Accept"
static var location = "Location"
static var authorization = "Authorization"
}

public struct HTTPContentType {

static var applicationJSON = "application/json"
}

public extension URLRequest {

func updating(headerFields: [String: String]) -> URLRequest {
var updatedHeaderFields = allHTTPHeaderFields ?? [:]
headerFields.forEach { updatedHeaderFields[$0.key] = $0.value }
var updatedRequest = self
updatedRequest.allHTTPHeaderFields = updatedHeaderFields
return updatedRequest
}
}

public struct HTTPError: Error {

public let code: Int

public init(code: Int) {
self.code = code
}
}

extension HTTPError: LocalizedError {

public var errorDescription: String? {
HTTPURLResponse.localizedString(forStatusCode: code)
}
}
38 changes: 38 additions & 0 deletions Sources/SimpleRESTClient/NetworkTransport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// NetworkTransport.swift
// SimpleRESTClient
//
// Created by Simon Lawrence on 20/01/2024.
//

import Foundation

public struct NetworkTransport: Transport {

public let environment: Environment
public let session: URLSession
public let requestProcessors: [RequestProcessor]

public init(environment: Environment, session: URLSession = .shared, requestProcessors: [RequestProcessor] = []) {
self.environment = environment
self.session = session
self.requestProcessors = requestProcessors
}

public func execute(request: URLRequest, expectedStatusCodes: [Int]) async throws -> Data {

let (data, response) = try await session.data(for: request)

guard let httpURLResponse = response as? HTTPURLResponse else {
throw URLError(.unsupportedURL)
}

let statusCode = httpURLResponse.statusCode

guard expectedStatusCodes.contains(statusCode) else {
throw HTTPError(code: statusCode)
}

return data
}
}
6 changes: 0 additions & 6 deletions Sources/SimpleRESTClient/SimpleRESTClient.swift

This file was deleted.

Loading

0 comments on commit 9841878

Please sign in to comment.