Skip to content

Commit

Permalink
Change to a protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
hallee committed Jan 29, 2024
1 parent f02de59 commit a758a08
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ disabled_rules:
- closure_parameter_position
- identifier_name
- multiple_closures_with_trailing_closure
- type_name
opt_in_rules:
- attributes
- closure_end_indentation
Expand All @@ -11,7 +12,6 @@ opt_in_rules:
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- convenience_type
- empty_count
- empty_string
- explicit_init
Expand Down
8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
.visionOS(.v1)
],
products: [
.library(name: "EndpointBuilderURLSession", targets: ["EndpointBuilderURLSession"]),
.library(name: "EndpointBuilderURLSession", targets: ["EndpointBuilderURLSession"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"),
Expand All @@ -24,6 +24,12 @@ let package = Package(
.product(name: "EndpointBuilder", package: "endpoint-builder"),
.product(name: "HTTPTypes", package: "swift-http-types")
]
),
.testTarget(
name: "EndpointBuilderURLSessionTests",
dependencies: [
.byName(name: "EndpointBuilderURLSession")
]
)
]
)
62 changes: 23 additions & 39 deletions Sources/EndpointBuilderURLSession/EndpointBuilderURLSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,35 @@ import FoundationNetworking
import HTTPTypes

/// An API endpoint client that uses `URLSession` to create requests
public struct EndpointBuilderURLSession: Sendable {
public protocol EndpointBuilderURLSession: Sendable {

/// The base server URL on which endpoint paths will be appended
public let serverBaseURL: URL
var serverBaseURL: URL { get }

/// The URLSession object that will make requests
public let urlSession: @Sendable () -> URLSession
var urlSession: @Sendable () -> URLRequestHandler { get }

/// JSON encoder
public let encoder: @Sendable () -> JSONEncoder
var encoder: @Sendable () -> JSONEncoder { get }

/// JSON decoder
public let decoder: @Sendable () -> JSONDecoder

/// Creates a new `EndpointBuilderURLSession`.
public init(
serverBaseURL: URL,
urlSession: @Sendable @escaping () -> URLSession = { URLSession.shared },
encoder: @Sendable @escaping () -> JSONEncoder = { JSONEncoder() },
decoder: @Sendable @escaping () -> JSONDecoder = { JSONDecoder() }
) {
self.serverBaseURL = serverBaseURL
self.urlSession = urlSession
self.encoder = encoder
self.decoder = decoder
var decoder: @Sendable () -> JSONDecoder { get }

}

// Default values
extension EndpointBuilderURLSession {

public var urlSession: @Sendable () -> URLSession {
{ URLSession.shared }
}

public var encoder: @Sendable () -> JSONEncoder {
{ JSONEncoder() }
}

var decoder: @Sendable () -> JSONDecoder {
{ JSONDecoder() }
}

}
Expand Down Expand Up @@ -70,7 +74,8 @@ extension EndpointBuilderURLSession {
}

// perform request
return try await urlSession().responseData(for: request)
let (data, _) = try await urlSession().data(for: request)
return data
}

}
Expand All @@ -90,24 +95,3 @@ extension URL {
}

}

extension URLSession {

func responseData(for request: URLRequest) async throws -> Data {
#if canImport(FoundationNetworking)
await withCheckedContinuation { continuation in
self.dataTask(with: request) { data, _, _ in
guard let data = data else {
continuation.resume(returning: Data())
return
}
continuation.resume(returning: data)
}.resume()
}
#else
let (data, _) = try await self.data(for: request)
return data
#endif
}

}
34 changes: 34 additions & 0 deletions Sources/EndpointBuilderURLSession/URLRequestHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public protocol URLRequestHandler: Sendable {

func data(for request: URLRequest) async throws -> (Data, URLResponse)

}

extension URLSession: URLRequestHandler {}

#if canImport(FoundationNetworking)
enum URLRequestHandlerError: Error {
case noResponse
}

extension URLSession {

public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
self.dataTask(with: request) { data, response, error in
guard let data = data, let response = response else {
continuation.resume(throwing: error ?? URLRequestHandlerError.noResponse)
return
}
continuation.resume(returning: (data, response))
}.resume()
}
}

}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import EndpointBuilder
@testable import EndpointBuilderURLSession
import HTTPTypes
import RoutingKit
import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

struct MockURLSession: URLRequestHandler {

let requestHandler: @Sendable (URLRequest) -> Data

func data(for request: URLRequest) async throws -> (Data, URLResponse) {
let data = requestHandler(request)
return (
data,
URLResponse()
)
}

}

final class EndpointBuilderURLSessionTests: XCTestCase {

@Endpoint
struct EndpointWithNoResponse {
static let path: [PathComponent] = ["blank"]
static let httpMethod = HTTPRequest.Method.get
}

@Endpoint
struct EndpointWithStringResponseAndPathComponent {
static let path: [PathComponent] = ["echo", ":id"]
static let httpMethod = HTTPRequest.Method.post
static let responseType = String.self
let body: String
}

struct APIClient: EndpointBuilderURLSession {

let serverBaseURL = URL(string: "https://api.shipyard.studio")!
let urlSession: @Sendable () -> URLRequestHandler

init(mockSession: MockURLSession) {
let session: @Sendable () -> URLRequestHandler = { mockSession }
self.urlSession = session
}

}

func testEndpointWithNoResponse() async throws {
let endpoint = EndpointWithNoResponse()
let client = APIClient(mockSession: MockURLSession(requestHandler: { urlRequest in
XCTAssertEqual(urlRequest.url?.pathComponents, ["/", "blank"])
XCTAssertEqual(urlRequest.httpMethod, "GET")
return Data()
}))
try await client.request(endpoint)
}

func testEndpointWithStringResponse() async throws {
let endpoint = EndpointWithStringResponseAndPathComponent(
body: "hello",
pathParameters: EndpointWithStringResponseAndPathComponent.PathParameters(id: "my-ids")
)
let client = APIClient(mockSession: MockURLSession(requestHandler: { urlRequest in
XCTAssertEqual(urlRequest.url?.pathComponents, ["/", "echo", "my-ids"])
XCTAssertEqual(urlRequest.httpMethod, "POST")
XCTAssertEqual(urlRequest.httpBody, try? JSONEncoder().encode("hello"))
return (try? JSONEncoder().encode("world")) ?? Data()
}))
let response = try await client.request(endpoint)
XCTAssertEqual(response, "world")
}

}

0 comments on commit a758a08

Please sign in to comment.