-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jacob Hearst
committed
Sep 13, 2024
1 parent
3e81be0
commit 8e2d73b
Showing
7 changed files
with
227 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// swift-tools-version:5.9 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "ScryfallKit", | ||
platforms: [.macOS(.v10_13), .iOS(.v12)], | ||
products: [ | ||
.library( | ||
name: "ScryfallKit", | ||
targets: ["ScryfallKit"]) | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), | ||
], | ||
targets: [ | ||
.target( | ||
name: "ScryfallKit" | ||
), | ||
.testTarget( | ||
name: "ScryfallKitTests", | ||
dependencies: ["ScryfallKit"] | ||
) | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,103 +1,107 @@ | ||
// | ||
// NetworkService.swift | ||
// | ||
// | ||
|
||
import Foundation | ||
import OSLog | ||
|
||
/// An enum representing the two available levels of log verbosity | ||
public enum NetworkLogLevel: Sendable { | ||
/// Only log when requests are made and errors | ||
case minimal | ||
/// Log the bodies of requests and responses | ||
case verbose | ||
/// Only log when requests are made and errors | ||
case minimal | ||
/// Log the bodies of requests and responses | ||
case verbose | ||
} | ||
|
||
protocol NetworkServiceProtocol: Sendable { | ||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result<T, Error>) -> Void) | ||
@available(macOS 10.15.0, *, iOS 13.0.0, *) | ||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type) async throws -> T | ||
func request<T: Decodable>( | ||
_ request: EndpointRequest, | ||
as type: T.Type, | ||
completion: @Sendable @escaping (Result<T, Error>) -> Void | ||
) | ||
@available(macOS 10.15.0, *, iOS 13.0.0, *) | ||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type) async throws -> T | ||
} | ||
|
||
struct NetworkService: NetworkServiceProtocol, Sendable { | ||
var logLevel: NetworkLogLevel | ||
|
||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result<T, Error>) -> Void) { | ||
guard let urlRequest = request.urlRequest else { | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.error("Invalid url request") | ||
} else { | ||
print("Invalid url request") | ||
} | ||
completion(.failure(ScryfallKitError.invalidUrl)) | ||
return | ||
} | ||
var logLevel: NetworkLogLevel | ||
|
||
if logLevel == .verbose, let body = urlRequest.httpBody, let JSONString = String(data: body, encoding: String.Encoding.utf8) { | ||
print("Sending request with body:") | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.debug("\(JSONString)") | ||
} else { | ||
print(JSONString) | ||
} | ||
} | ||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type, completion: @Sendable @escaping (Result<T, Error>) -> Void) { | ||
guard let urlRequest = request.urlRequest else { | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.error("Invalid url request") | ||
} else { | ||
print("Invalid url request") | ||
} | ||
completion(.failure(ScryfallKitError.invalidUrl)) | ||
return | ||
} | ||
|
||
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in | ||
do { | ||
let result = try handle(dataType: type, data: data, response: response, error: error) | ||
completion(.success(result)) | ||
} catch { | ||
completion(.failure(error)) | ||
} | ||
} | ||
if logLevel == .verbose, let body = urlRequest.httpBody, let JSONString = String(data: body, encoding: String.Encoding.utf8) { | ||
print("Sending request with body:") | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.debug("\(JSONString)") | ||
} else { | ||
print(JSONString) | ||
} | ||
} | ||
|
||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.debug("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'") | ||
} else { | ||
print("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'") | ||
} | ||
task.resume() | ||
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in | ||
do { | ||
let result = try handle(dataType: type, data: data, response: response, error: error) | ||
completion(.success(result)) | ||
} catch { | ||
completion(.failure(error)) | ||
} | ||
} | ||
|
||
func handle<T: Decodable>(dataType: T.Type, data: Data?, response: URLResponse?, error: Error?) throws -> T { | ||
if let error = error { | ||
throw error | ||
} | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.debug("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'") | ||
} else { | ||
print("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'") | ||
} | ||
task.resume() | ||
} | ||
|
||
guard let content = data else { | ||
throw ScryfallKitError.noDataReturned | ||
} | ||
func handle<T: Decodable>(dataType: T.Type, data: Data?, response: URLResponse?, error: Error?) throws -> T { | ||
if let error = error { | ||
throw error | ||
} | ||
|
||
guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else { | ||
throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse") | ||
} | ||
guard let content = data else { | ||
throw ScryfallKitError.noDataReturned | ||
} | ||
|
||
let decoder = JSONDecoder() | ||
decoder.keyDecodingStrategy = .convertFromSnakeCase | ||
guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else { | ||
throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse") | ||
} | ||
|
||
if (200..<300).contains(httpStatus) { | ||
if logLevel == .verbose { | ||
let responseBody = String(data: content, encoding: .utf8) | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.debug("\(responseBody ?? "Couldn't represent response body as string")") | ||
} else { | ||
print(responseBody ?? "Couldn't represent response body as string") | ||
} | ||
} | ||
let decoder = JSONDecoder() | ||
decoder.keyDecodingStrategy = .convertFromSnakeCase | ||
|
||
return try decoder.decode(dataType, from: content) | ||
if (200..<300).contains(httpStatus) { | ||
if logLevel == .verbose { | ||
let responseBody = String(data: content, encoding: .utf8) | ||
if #available(macOS 11.0, iOS 14.0, *) { | ||
Logger.network.debug("\(responseBody ?? "Couldn't represent response body as string")") | ||
} else { | ||
let httpError = try decoder.decode(ScryfallError.self, from: content) | ||
throw ScryfallKitError.scryfallError(httpError) | ||
print(responseBody ?? "Couldn't represent response body as string") | ||
} | ||
} | ||
|
||
return try decoder.decode(dataType, from: content) | ||
} else { | ||
let httpError = try decoder.decode(ScryfallError.self, from: content) | ||
throw ScryfallKitError.scryfallError(httpError) | ||
} | ||
} | ||
|
||
@available(macOS 10.15.0, *, iOS 13.0.0, *) | ||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type) async throws -> T { | ||
try await withCheckedThrowingContinuation { continuation in | ||
self.request(request, as: type) { result in | ||
continuation.resume(with: result) | ||
} | ||
} | ||
@available(macOS 10.15.0, *, iOS 13.0.0, *) | ||
func request<T: Decodable>(_ request: EndpointRequest, as type: T.Type) async throws -> T where T: Sendable { | ||
try await withCheckedThrowingContinuation { continuation in | ||
self.request(request, as: type) { result in | ||
continuation.resume(with: result) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.