Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.5
// swift-tools-version:6.2
import PackageDescription

let package = Package(
Expand All @@ -15,7 +15,26 @@ let package = Package(
],
targets: [
.target(
name: "RevoHttp"
name: "RevoHttp",
swiftSettings: [
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_DYNAMIC_ACTOR_ISOLATION"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION"),
.enableUpcomingFeature("SWIFT_ENABLE_BARE_SLASH_REGEX"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE"),
.enableUpcomingFeature("SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN"),

.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "RevoHttpTests",
Expand Down
1 change: 0 additions & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ target 'RevoHttp' do
#use_frameworks!

# Pods for RevoHttp
pod 'RevoFoundation'

target 'RevoHttpTests' do
inherit! :search_paths
Expand Down
15 changes: 1 addition & 14 deletions Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
PODS:
- RevoFoundation (0.2.0)

DEPENDENCIES:
- RevoFoundation

SPEC REPOS:
trunk:
- RevoFoundation

SPEC CHECKSUMS:
RevoFoundation: f00513750bfbbb9a07476450728063e921bdf5db

PODFILE CHECKSUM: 9cb5cf14b95b566960199e173b82560ca9dcf6e6
PODFILE CHECKSUM: 3c4539269f16d249c8a415f7262c3b68209f9f46

COCOAPODS: 1.16.2
21 changes: 21 additions & 0 deletions Sources/RevoHttp/Enums/HttpError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

public enum HttpError : LocalizedError {

case invalidUrl
case invalidParams
case responseError
case reponseStatusError(response:HttpResponse)
case undecodableResponse

public var errorDescription: String? {
switch self {
case .invalidUrl: "Malformed Url"
case .invalidParams: "Invalid input params"
case .responseError: "Response returned and error"
case .reponseStatusError: "Response returned a non 200 status"
case .undecodableResponse: "Undecodable response"

}
}
}
8 changes: 8 additions & 0 deletions Sources/RevoHttp/Enums/HttpOption.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

public enum HttpOption {
case hmacSHA256(header: String, privateKey: String)
case timeout(seconds: Int)
case session(URLSession)
case allowUnsecureUrls
}
133 changes: 77 additions & 56 deletions Sources/RevoHttp/Fake/HttpFake.swift
Original file line number Diff line number Diff line change
@@ -1,81 +1,102 @@
import Foundation


public class HttpFake : NSObject {
actor HttpFakeState {
var calls: [HttpRequest] = []
var responses: [String: HttpResponse] = [:]
var globalResponses: [HttpResponse] = []

public static var calls:[HttpRequest] = []
static var responses:[String:HttpResponse] = [:]
static var globalResponses:[HttpResponse] = []
func reset() {
calls = []
responses = [:]
globalResponses = []
}

static var swizzled = false
func addCall(_ request: HttpRequest) {
calls.append(request)
}

public static func enable(){
Self.responses = [:]
Self.globalResponses = []
Self.calls = []
if (swizzled) { return }


guard let originalMethod = class_getInstanceMethod(Http.self, #selector(call(_:then:))),
let newMethod = class_getInstanceMethod(HttpFake.self, #selector(call(_:then:))) else {
return
}

method_exchangeImplementations(originalMethod, newMethod)
swizzled = true
func getResponse(for url: String) -> HttpResponse? {
responses[url]
}

public static func disable(){
if (!swizzled) { return }
swizzled = false
Self.enable()
swizzled = false
func getGlobalResponse() -> HttpResponse? {
guard let first = globalResponses.first else { return nil }
if globalResponses.count > 1 {
globalResponses.removeFirst()
}
return first
}

@objc dynamic public func call(_ request:HttpRequest, then:@escaping(_ response:HttpResponse)->Void) {
Self.calls.append(request)

if let toRespond = Self.responses[request.url] {
return then(toRespond)
func addResponse(_ response: HttpResponse, for url: String?) {
if let url {
responses[url] = response
} else {
globalResponses.append(response)
}
}
}

public class HttpFake : Http, @unchecked Sendable {

if (Self.globalResponses.count == 1) {
return then(Self.globalResponses.first!)
public var calls: [HttpRequest] {
get async {
await state.calls
}

if let toRespond = Self.globalResponses.first {
if Self.globalResponses.count > 1 {
Self.globalResponses.removeFirst()
}
return then(toRespond)
}

public var globalResponses: [HttpResponse] {
get async {
await state.globalResponses
}

then(HttpResponse(data: nil, response: nil, error: nil))
}

public static func addResponse(_ response:String, status:Int = 200) {
let httpResponse = HTTPURLResponse(url: URL(string:"http://fakeUrl.com")!, statusCode: status, httpVersion: "1.0", headerFields: nil)
let globalResponse = HttpResponse(data:response.data(using: .utf8), response:httpResponse , error: nil)
Self.globalResponses.append(globalResponse)
public var responses: [String: HttpResponse] {
get async {
await state.responses
}
}

private let state = HttpFakeState()
var swizzled = false

public func enable() async {
await state.reset()
guard !swizzled else { return }

public static func addResponse<T:Codable>(encoded response:T, status:Int = 200) {
let data = try! JSONEncoder().encode(response)
let httpResponse = HTTPURLResponse(url: URL(string:"http://fakeUrl.com")!, statusCode: status, httpVersion: "1.0", headerFields: nil)
let globalResponse = HttpResponse(data:data, response:httpResponse , error: nil)
Self.globalResponses.append(globalResponse)
await ThreadSafeContainer.shared.bind(instance: Http.self, self)
swizzled = true
}

public static func addResponse(for url:String, _ response:String, status:Int = 200) {
let httpResponse = HTTPURLResponse(url: URL(string:"http://fakeUrl.com")!, statusCode: status, httpVersion: "1.0", headerFields: nil)
let concreteResponse = HttpResponse(data:response.data(using: .utf8), response:httpResponse , error: nil)
Self.responses[url] = concreteResponse
public func disable() async {
guard swizzled else { return }
await state.reset()

await ThreadSafeContainer.shared.unbind(Http.self)
swizzled = false
}

public static func addResponse<T:Codable>(for url:String, encoded response:T, status:Int = 200) {
public override func makeCall(_ request:HttpRequest) async -> HttpResponse {
await state.addCall(request)

if let urlResponse = await state.getResponse(for: request.url) {
return urlResponse
}
if let globalResponse = await state.getGlobalResponse() {
return globalResponse
}
return HttpResponse(data: nil, response: nil, error: nil)
}

public func addResponse(for url:String? = nil, _ response:String, status:Int = 200) async {
let httpResponse = HTTPURLResponse(url: URL(string:"http://fakeUrl.com")!, statusCode: status, httpVersion: "1.0", headerFields: nil)
let httpResponseObj = HttpResponse(data:response.data(using: .utf8), response:httpResponse , error: nil)
await state.addResponse(httpResponseObj, for: url)
}

public func addResponse<T:Codable>(for url:String? = nil, encoded response:T, status:Int = 200) async {
let data = try! JSONEncoder().encode(response)
let httpResponse = HTTPURLResponse(url: URL(string:"http://fakeUrl.com")!, statusCode: status, httpVersion: "1.0", headerFields: nil)
let concreteResponse = HttpResponse(data:data, response:httpResponse , error: nil)
Self.responses[url] = concreteResponse
let httpResponseObj = HttpResponse(data:data, response:httpResponse , error: nil)
await state.addResponse(httpResponseObj, for: url)
}
}
41 changes: 41 additions & 0 deletions Sources/RevoHttp/Fake/ThreadSafeContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
protocol Resolvable{
init()
}

actor ThreadSafeContainer {

nonisolated static let shared = ThreadSafeContainer()

private var resolvers: [String: Any] = [:]

func bind<T, Z>(instance type: T.Type, _ resolver: Z) {
resolvers[String(describing: type)] = resolver
}

func unbind<T>(_ type: T.Type) {
resolvers.removeValue(forKey: String(describing: type))
}

func resolve<T>(_ type: T.Type) -> T? {
resolve(withoutExtension: type)
}

func resolve<T>(withoutExtension type: T.Type) -> T? {
guard let resolver = resolvers[String(describing: type)] else {
if type.self is Resolvable.Type {
return (type as! Resolvable.Type).init() as? T
}
return nil
}
if let resolvable = resolver as? Resolvable.Type {
return resolvable.init() as? T
}
if let resolvable = resolver as? T {
return resolvable
}
if let resolvable = resolver as? (()->T) {
return resolvable()
}
return nil
}
}
37 changes: 0 additions & 37 deletions Sources/RevoHttp/Http+Async.swift

This file was deleted.

Loading