From e761fbcd0284e3cfec6dfb4744ffca016a8f1c6b Mon Sep 17 00:00:00 2001 From: Daniel Koster Date: Thu, 18 Dec 2025 16:37:15 -0300 Subject: [PATCH 1/4] added docs --- Docs/AuthenticationDoc.md | 25 +++++++ Docs/CertificatePinning.md | 106 ++++++++++++++++++++++++++ Docs/CodableExtensions.md | 26 +++++++ Docs/CombineSupport.md | 37 +++++++++ Docs/DispatcherExtension.md | 41 ++++++++++ Docs/Error.md | 47 ++++++++++++ Docs/GettingStarted.md | 115 ++++++++++++++++++++++++++++ Docs/HostEnvironment.md | 145 ++++++++++++++++++++++++++++++++++++ Docs/ImageExtension.md | 39 ++++++++++ Docs/JSONEncoding.md | 31 ++++++++ Docs/Responses.md | 100 +++++++++++++++++++++++++ Docs/StringEncoding.md | 33 ++++++++ Docs/URLEncoding.md | 33 ++++++++ Docs/URLRequestExtension.md | 38 ++++++++++ README.md | 52 +++++++++++++ diagram.png | Bin 0 -> 29842 bytes 16 files changed, 868 insertions(+) create mode 100644 Docs/AuthenticationDoc.md create mode 100644 Docs/CertificatePinning.md create mode 100644 Docs/CodableExtensions.md create mode 100644 Docs/CombineSupport.md create mode 100644 Docs/DispatcherExtension.md create mode 100644 Docs/Error.md create mode 100644 Docs/GettingStarted.md create mode 100644 Docs/HostEnvironment.md create mode 100644 Docs/ImageExtension.md create mode 100644 Docs/JSONEncoding.md create mode 100644 Docs/Responses.md create mode 100644 Docs/StringEncoding.md create mode 100644 Docs/URLEncoding.md create mode 100644 Docs/URLRequestExtension.md create mode 100644 README.md create mode 100644 diagram.png diff --git a/Docs/AuthenticationDoc.md b/Docs/AuthenticationDoc.md new file mode 100644 index 0000000..7e72eaf --- /dev/null +++ b/Docs/AuthenticationDoc.md @@ -0,0 +1,25 @@ +## **Authentication Protocol** +- This is a very useful protocol when you are working with Authenticated API's and you need to authorize requests + +```swift +public protocol Authentication { + var isAuthenticated: Bool { get } + func autenticate(params: [String: Any], completionHandler completion: @escaping (Result) -> Void) + func authorize(request: URLRequest) -> URLRequest + func clearCredentials() +} + +public protocol RefreshableAuthentication { + func refresh(params: [String: Any], completionHandler completion: @escaping (Result) -> Void) +} + +public protocol RevokableAuthentication { + func revoke(params: [String: Any], completionHandler completion: @escaping (Result) -> Void) +} +``` + +QuickHatch provides these 3 protocols for Authentication management, some Auths are revokables and refreshables. + +Usually you have an endpoint to fetch the set of tokens, (in your implementation you should use the authenticate method for that) and the you inject the auth token in the Authorization header for each request. + + diff --git a/Docs/CertificatePinning.md b/Docs/CertificatePinning.md new file mode 100644 index 0000000..c55f868 --- /dev/null +++ b/Docs/CertificatePinning.md @@ -0,0 +1,106 @@ +## **iOS Security: Certificate Pinning** +As developers we need our apps to: work properly (obviously), be easy to mantain, testable, scalable and **SECURE**. +Users will trust their confidential data to our apps and we need to handle that carefully and prevent leaks or flaws that can cause data loss or exposure. + +I always recommend to follow OWASP and their Mobile Apps Top 10 security threats and the solutions for it +(https://owasp.org/www-project-mobile-top-10/) + +The threat that is treated by QuickHatch is Top 3 Insecure Communication (https://owasp.org/www-project-mobile-top-10/2016-risks/m3-insecure-communication). + +The Certificate Pinning Module is made to prevent Man in the middle attacks (you can find more detailed information in the OWASP links) but basically what we are preventing is that an attacker is able to see all your network traffic. + +Certificate Pinning works on top of HTTPS requests and what is gonna do is to verify the authenticity of the SSL Certificate used by the server and the connection between your device and server. + +For more information about TLS/SSL handshake and how it works check OWASP links. +https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning + +The module has 2 strategies to validate the certificate: Pin the certificate and pin the public key. +which one to choose ? Its up to you, if you choose to pin the certificate you will have to update the app bundle every time the certificate is updated, however if you pin the public key you wont have this problem because the public key is not going to change, but you may violate the key rotation standard (its always good to change the keys of your app). +Lets jump right to the Code: + +```swift + public protocol CertificatePinner { + func isServerTrusted(challenge: URLAuthenticationChallenge) -> Bool + } +``` + +First of all we have a protocol CertificatePinner with only one function that is to check wether the server we are receiving is trusted by our app or not. **URLAuthenticationChallenge** is the swift class that contains information about the server certficate. +```swift +public class QHCertificatePinner: CertificatePinner { + private let pinningStrategies: [String: [PinningStrategy]] + + public init(pinningStrategies: [String: [PinningStrategy]]) { + self.pinningStrategies = pinningStrategies + } + .... + ... +``` + +Here is the QuickHatch implementation: you initialize your pinner with a dictionary of hosts (string) with pinning strategies for those hosts. + +The pinning strategy protocol: +```swift +public protocol PinningStrategy { + func validate(serverTrust: SecTrust) -> Bool +} +``` + +CertificatePinning strategy: +```swift +public class CertificatePinningStrategy: PinningStrategy { + private let certificates: [SecCertificate] + + public init(certificates: [SecCertificate]) +``` +its initialized with an array of Certificates (included in your bundle) and the validate function will use those certificates. + +The public key strategy: +```swift +public class PublicKeyPinningStrategy: PinningStrategy { + private let publicKeys: [String] + private let hasher: Hasher + + public init(publicKeys: [String], hasher: Hasher = CCSHA256Hasher.shared) +``` +This implementation is initialized with an array of public keys (hash) and with a hasher class that is gonna apply the hashing to the public key coming in the **URLAuthenticationChallenge** and is gonna compare 2 strings to validate. + + +Real Example: +```swift + +let pinner = QHCertificatePinner(pinningStrategies: ["quickhatch.com": [CertificatePinningStrategy(certificates: [certificate], + PublicKeyPinningStrategy(publicKeys: ["your public key has"], + hasher: youHasher)) + ] + ]) +``` + +In this case we set 2 pinning strategies for **quickhatch.com** one public key pinning with one public key and your own hasher implementation and one Certificate pinning strategy with one certificate. + +By default if the host doesnt have a pinning strategy set it will return as trusted. + +All of this look great and fancy but where do we use our pinner ??? + +The answer is URLSession method **urlSession(_ session: URLSession,didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)** so There is a class implementing URLSessionDelegate, you can use this class to initialize your urlsession or you can subclass it you choice. +```swift +public class URLSessionPinningDelegate: NSObject, URLSessionDelegate { + private let certificatePinner: CertificatePinner + + public init(certificatePinner: CertificatePinner) { + self.certificatePinner = certificatePinner + } + + public func urlSession(_ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + if certificatePinner.isServerTrusted(challenge: challenge) { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + return + } + completionHandler(.cancelAuthenticationChallenge, nil) + } +``` \ No newline at end of file diff --git a/Docs/CodableExtensions.md b/Docs/CodableExtensions.md new file mode 100644 index 0000000..908ac05 --- /dev/null +++ b/Docs/CodableExtensions.md @@ -0,0 +1,26 @@ +## **Using Codable extension** + +Codable (Encodable & Decodable) is a protocol launched by Apple to encode and decode JSONs very easily, +QuickHatch provides an extension for mapping responses to an object or an array. + +This is a sample for a **response** mapping using QuickHatch: + +```swift + struct User: Codable { + var name: String + var age: Int + } + + let request = networkRequestFactory.response(request: yourRequest) { (result: Result, Error>) in + switch result { + case .success(let response): + //do something + case .failure(let error): + //handle error + } + } + request.resume() +``` + + +--- diff --git a/Docs/CombineSupport.md b/Docs/CombineSupport.md new file mode 100644 index 0000000..8690067 --- /dev/null +++ b/Docs/CombineSupport.md @@ -0,0 +1,37 @@ +## **Using Combine extension** + +The network Request Factory also has support for swift Combine if you decide to go full Reactive programming. + +```swift + func image(urlRequest: URLRequest, + dispatchQueue: DispatchQueue = .main, + quality: CGFloat) -> AnyPublisher + + func response(urlRequest: URLRequest, + dispatchQueue: DispatchQueue = .main, + jsonDecoder: JSONDecoder = JSONDecoder()) -> AnyPublisher + + func string(urlRequest: URLRequest, + dispatchQueue: DispatchQueue = .main, + jsonDecoder: JSONDecoder = JSONDecoder())-> AnyPublisher + +``` +This is a sample for a **response** mapping using QuickHatch Combine function: + +```swift + struct User: Codable { + var name: String + var age: Int + } + var subscriptions: Set = [] + + let request = networkRequestFactory.response(request: yourRequest) + .sink(receiveCompletion: { receiveCompletion in + + }, + receiveValue: { (value: User) in + }).store(in: &subscriptions) +``` + + +--- \ No newline at end of file diff --git a/Docs/DispatcherExtension.md b/Docs/DispatcherExtension.md new file mode 100644 index 0000000..f75c971 --- /dev/null +++ b/Docs/DispatcherExtension.md @@ -0,0 +1,41 @@ +## **Dispatcher Extension** +- QuickHatch allows you to handle the thread you want to perform your results + + +QuickHatch has a parameter in each of the methods that is the dispatchQueue where the results will be dispatched, the default value is main thread but you can configure it easily +```swift +public extension NetworkRequestFactory { + func data(request: URLRequest, dispatchQueue: DispatchQueue = .main, completionHandler completion: @escaping DataCompletionHandler) -> Request +} +``` +If we want to handle our results in a background thread... + +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private weak var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + let backgroundThread = DispatchQueue.global(qos: .background) + networkRequestFactory.data(urlRequest: request, dispatchQueue: backgroundThread) { result in + switch result { + case .success(let response): + // show error messsage + view?.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + view?.showErrorMessage("Error downloading profile image") + } + } +} +``` + +The results of the login service will run on a background thread now. Super easy! diff --git a/Docs/Error.md b/Docs/Error.md new file mode 100644 index 0000000..4e8b303 --- /dev/null +++ b/Docs/Error.md @@ -0,0 +1,47 @@ +## **Errors** +- If an error is returned in a network request QuickHatch provides an enum with errors +```swift +public enum RequestError: Error, Equatable { + case unauthorized + case unknownError(statusCode: Int) + case cancelled + case noResponse + case requestWithError(statusCode:HTTPStatusCode) + case serializationError + case invalidParameters + case noInternetConnection + case malformedRequest +} +``` +Now if we want to check what error is.. +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private weak var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.data(urlRequest: request) { result in + switch result { + case .success(let response): + // show error messsage + view?.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + if let requestError = error as? RequestError, + requestError == .unauthorized { + //show auth error + } + view?.showErrorMessage("Error downloading profile image") + } + } +} +``` +In this sample we are checking whether the error is unauthorized type or not. diff --git a/Docs/GettingStarted.md b/Docs/GettingStarted.md new file mode 100644 index 0000000..fbef2cb --- /dev/null +++ b/Docs/GettingStarted.md @@ -0,0 +1,115 @@ +## **Getting Started** + +- When you develop an app usually you have to fetch data from server/s. +- due to this you will want to use a Networking framework but you want to keep the code clean, independent and mantainable. +- You may want to use libraries like Alamofire, Malibu or your own implementation using URLSession or URLConnection. +- What QuickHatch provides is a set of protocols that allows you to follow clean code standards, SOLID principles and allows you to be flexible when it comes to Networking. +- First of all, your app will use a NetworkClient (this can be Alamofire, etc), and QuickHatch provides a protocol called NetworkRequestFactory that sets what a network client should respond when making a request. + +![](https://github.com/dkoster95/QuickHatchSwift/blob/master/diagram.png) + +The name of this networkLayer interface in QuickHatch is NetworkRequestFactory :). +```swift + +public protocol NetworkRequestFactory { + func data(request: URLRequest, dispatchQueue: DispatchQueue, completionHandler completion: @escaping DataCompletionHandler) -> Request + func data(request: URLRequest, dispatchQueue: DispatchQueue) -> AnyPublisher +} +``` + +The very base protocol of the RequestFactory provides 2 functions to get Data out of a network request, one is using a ***completionHandler*** with a ***Request*** type return and the other one is using ***Combine's AnyPublisher*** if you choose to use Reactive Programming + +Now lets say your app is using a MVP (Model-View-Presenter) pattern and you want to use QuickHatch to map your networking layer. +Lets show a sample of how that would be: + +First we have our presenter, class in charge of connecting View and Models. +Keep in mind these its just a sample, you should always use clean code principles such as SOLID or KISS. + + +```swift + class ViewPresenter: Presenter { + private var requestFactory: NetworkRequestFactory + var view: View? + + init(requestFactory: NetworkRequestFactory) { + self.requestFactory = requestFactory + } + + func fetchData(username: String) { + view?.showLoading() + // Here you initialize the URLRequest object, for now we will use a quickHatch get request + let yourRequest = URLRequest.get(url: URL("www.google.com", + parameters: ["username": username], + parameterEncoding: URLEncoding.queryString) + requestFactory.data(request: yourRequest, dispatch: .main) { result in + view?.stopLoading() + switch result { + case .success(let data): + view?.showSuccessAlert(data) + case .failure(let error): + view?.showErrorAlert(error) + } + + }.resume() + } + } +``` +Here we have a View type (protocol), a fetchData method that is going to use the NetworkFactory to get the data from somewhere using a data response and we have the initializer where we are injecting the networkFactory. +Now previously if we used some NetworkClient framework here we would be using the implementation class instead of the protocol and we would be attached to that framework, +but with QuickHatch you are attaching yourself to a protocol, and the implementation can change anytime. + +And now we have the code of our view and the dependency injector: + +```swift + struct DependencyInjector { + func initializeSampleView() { + let presenter = ViewPresenter(requestFactory: QHRequestFactory(urlSession: URLSession.shared) + let sampleView = SampleView(presenter: presenter) + application.rootView = sampleView + } + } +``` + +```swift + class SampleView: View { + + private var presenter: ViewPresenter + private var textField: TextField + + init(presenter: Presenter) { + self.presenter = presenter + self.presenter.view = self + } + + @IBAction buttonTapped() { + presenter.fetchData(textField.text) + } + + func stopLoading() { + // stop spinner + } + + func showLoading() { + // start spinner + } + + func showSuccessAlert(data: Data) { + // show alert for success case + } + + func showErrorAlert(error: Error) { + // show alert for error case + } + } +``` + +Great! now we have a presenter that uses an abstract networking layer, and we can switch the implementation by changing the code in the dependencyInjector, +in this case we used the NetworkFactory implementation that QuickHatch provides, this one is initizalized with a URLSession object. +If we want to use an Alamofire Implementation: +you go to the dependency injector and set the new implementation. + +```swift + let presenter = ViewPresenter(requestFactory: AlamofireRequestFactory()) +``` + +--- diff --git a/Docs/HostEnvironment.md b/Docs/HostEnvironment.md new file mode 100644 index 0000000..6b4208d --- /dev/null +++ b/Docs/HostEnvironment.md @@ -0,0 +1,145 @@ +## **Host Environment classes** + + - QuickHatch also provides a set of protocols to make your API specification easy to make. + - Usually our app consume many services from a server(sometimes more than one) + - That server usually has 3 environments (QA, Staging and Production) + + ```swift +public protocol HostEnvironment { + var baseURL: String { get } + var headers: [String: String] { get } +} + ``` + + This is the base protocol for a Host, baseURL and headers. + + For a sophisticated implementation we have the next protocols: + ```swift + public protocol ServerEnvironmentConfiguration: Any { + var headers: [String: String] { get } + var qa: HostEnvironment { get } + var staging: HostEnvironment { get } + var production: HostEnvironment { get } +} + + +public enum Environment: String { + case qa = "QA" + case staging = "Staging" + case production = "Prod" +} + +public extension Environment { + func getCurrentEnvironment(server: ServerEnvironmentConfiguration) -> HostEnvironment { + switch self { + case .qa: + return server.qa + case .staging: + return server.staging + case .production: + return server.production + } + } +} + +public protocol GenericAPI { + var hostEnvironment: HostEnvironment { get set } + var path: String { get } +} +``` + + + For example lets say we are building an app with a server called QuickHatchServer and we have a User API with getUsers, fetchUserById and deleteUser. + + Also the server has 3 environments with 3 diferent URLs and headers. + + First lets create a class for our server specification with 3 environments +```swift + class QuickHatchServer: ServerEnvironmentConfiguration { + var headers: [String: Any] { + return ["User-Agent":"QuickHatch"] + } + var qa: HostEnvironment { + return GenericHostEnvironment(headers: headers, baseURL: "quickhatch-qa.com") + } + var staging: HostEnvironment { + return GenericHostEnvironment(headers: headers, baseURL: "quickhatch-stg.com") + } + var production: HostEnvironment { + return GenericHostEnvironment(headers: headers, baseURL: "quickhatch-prod.com") + } + } +``` +(GenericHostEnvironment is an implementation of HostEnvironment initalized with headers and baseURL) +Now we have our server class there with all the environments and headers configured (Keep in mind this is just an example to show how it works) + +Lets say we have a config class where we decide which server we re gonna use (QA, Staging or Prod) +```swift + class AppConfig { + static func getHost(environment: Environment) -> HostEnvironment { + let quickhatchServer = QuickHatchServer() + return environment.getCurrentEnvironment(server: quickhatchServer) + } + } +``` + +And now we are gonna create our API struct + +```swift + struct UserAPI: GenericAPI { + var hostEnvironment: HostEnvironment + var path: String { return "/api/" } + + init(hostEnvironment: HostEnvironment) { + self.hostEnvironment = hostEnvironment + } + + func getUsers(authentication: Authentication) -> URLRequest { + let stringURL = "\(hostEnvironment.baseURL)\(path)getusers.com" + let request = URLRequest.post(url: URL(stringURL), + encoding: URLEncoding.default) + return authentication.authorize(request: request) + } + + func fetchUser(authentication: Authentication, id: String) -> URLRequest { + let stringURL = "\(hostEnvironment.baseURL)\(path)getusers.com/{id}" + let request = URLRequest.get(url: URL(stringURL), + params: ["id": id], + encoding: StringEncoding.urlEncoding) + return authentication.authorize(request: request) + } + + func deleteUser(authentication: Authentication, id: String) -> URLRequest { + let stringURL = "\(hostEnvironment.baseURL)\(path)getusers.com" + let request = URLRequest.delete(url: URL(stringURL), + params: ["id":id] + encoding: URLEncoding.default) + return authentication.authorize(request: request) + } + } +``` + +And thats it for your API specification! +Keep in mind that in this example we used an authentication that inject a field in the Authorization header, dont freak out! + +Now the only thing you need is instantiate the API injecting the host + +```swift + let host = AppConfig.getHost(environment: .qa) + let userApi = UserAPI(hostEnvironment: host) + + let getUsersRequest = userApi.getUsers(authentication: yourAuthImplementation) + // Now we handle the response with QuickHatch implementation + requestFactory.array(request: getUsersRequest) { (result: Result) in + switch result { + case .success(let response): + print(response.data) + //array printed + case .failure(let error): + print(error) + //error printed + } + } +``` + +How Simple is to make your requests with QuickHatch! diff --git a/Docs/ImageExtension.md b/Docs/ImageExtension.md new file mode 100644 index 0000000..95e165d --- /dev/null +++ b/Docs/ImageExtension.md @@ -0,0 +1,39 @@ +## **Downloading Images** +- When you develop an app, you will probably use images to show content and design your UI +- QuickHatch provides an extension for downloading images from the net simple + +```swift +public extension NetworkRequestFactory { + func image(urlRequest: URLRequest, quality: CGFloat = 1, dispatchQueue: DispatchQueue = .main, completion completionHandler: @escaping (Result) -> Void) -> Request +} +``` +Now a sample of its use would be... + +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func showProfileImage() { + let request = URLRequest("getimage.com") + networkRequestFactory.image(urlRequest: request) { result in + switch result { + case .success(let image): + // update UI or cache image + view.showProfileImage(image) + case .failure(let error): + //show message or nothing + view.showErrorMessage("Error downloading profile image") + } + } +} +``` + +And just like that you can use The image extension. \ No newline at end of file diff --git a/Docs/JSONEncoding.md b/Docs/JSONEncoding.md new file mode 100644 index 0000000..20259b7 --- /dev/null +++ b/Docs/JSONEncoding.md @@ -0,0 +1,31 @@ +## **JSON Parameter Encoding** +- Another important feature of the framework is the **parameter encoding** for a URLRequest + + +Here you see the parameter encoding protocol, simple right ? +the framework provides 3 implementations for this protocol URL, JSON and String encodings but you can create your own implementation and then inject it into the URLRequest initializer. + +```swift +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} +``` + +The JSON encoding will escape and encode the parameters in the body. + +It will look like this +params: name and password +```swift +{ + "name": "quickhatch", + "password": "1234" +} +``` + +Real Sample: +```swift +let getUsers = URLRequest.post(url: URL("getusers.com"), + params: ["page":1], + encoding: JSONEncoding.default, + headers: ["Authorization": "token 1232"]) +``` \ No newline at end of file diff --git a/Docs/Responses.md b/Docs/Responses.md new file mode 100644 index 0000000..b9026ed --- /dev/null +++ b/Docs/Responses.md @@ -0,0 +1,100 @@ +## **Data and String responses** +- Use QuickHatch to parse your request responses into the standard types +- Handle the response however you want to do it + +Remember the Base protocol.... +```swift +public protocol NetworkRequestFactory { + func data(request: URLRequest, dispatchQueue: DispatchQueue, completionHandler completion: @escaping DataCompletionHandler) -> Request + ... + .. +} + +extension NetworkRequestFactory { + func string(request: URLRequest,dispatchQueue: DispatchQueue, completionHandler completion: @escaping StringCompletionHandler) -> Request +} +``` +If we want to handle our login response back as **Data** Type.... + +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.data(request: request) { result in + switch result { + case .success(let response): + // show error messsage + view.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + view.showErrorMessage("Error downloading profile image") + } + } +} +``` + +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.data(request: request) { result in + switch result { + case .success(let response): + // show error messsage + view.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + view.showErrorMessage("Error downloading profile image") + } + } +} +``` +If you want to use **String** response type.... +```swift +import QuickHatch + +class LoginViewPresenter { + private let networkRequestFactory: NetworkRequestFactory + private var view: LoginView + + init(requestFactory: NetworkRequestFactory, view: LoginView) { + self.networkRequestFactory = requestFactory + self.view = view + } + + func login(user: String, password: String) { + let request = URLRequest("getimage.com/user=\(user)&password=\(password)") + networkRequestFactory.string(request: request) { result in + switch result { + case .success(let response): + // show error messsage + view.showSuccessMessage(response.data) + case .failure(let error): + //show message or nothing + view.showErrorMessage("Error downloading profile image") + } + } +} +``` + +This is an alternative for handling the responses, you can also check the Codable Extension to parse the response into a Codable Object of your own diff --git a/Docs/StringEncoding.md b/Docs/StringEncoding.md new file mode 100644 index 0000000..e8e1f7e --- /dev/null +++ b/Docs/StringEncoding.md @@ -0,0 +1,33 @@ +## **String Parameter Encoding** +- Another important feature of the framework is the **parameter encoding** for a URLRequest + + +Here you see the parameter encoding protocol, simple right ? +the framework provides 3 implementations for this protocol URL, JSON and String encodings but you can create your own implementation and then inject it into the URLRequest initializer. + +```swift +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} +``` + +The String encoding will escape and encode the parameters in the body or the URL, you can configure that. + +It will look like this if you choose the body: +params: name and password +```swift +httpBody = "quickhatch&1234" +``` + +But if you choose the URL as destination: +There is an interesting mapping provided by the Encoder +you have page and max(per page) as parameters +```swift + let getUsers = URLRequest.get(url: URL("getusers.com/{page}/{max}"), + params: ["page":1,"maxperpage":30], + encoding: StringEncoding.urlEncoding, + headers: ["Authorization": "token 1232"]) + yourURL will be "getusers.com/1/30" +``` + + diff --git a/Docs/URLEncoding.md b/Docs/URLEncoding.md new file mode 100644 index 0000000..b486417 --- /dev/null +++ b/Docs/URLEncoding.md @@ -0,0 +1,33 @@ +## **Url Parameter Encoding** +- Another important feature of the framework is the **parameter encoding** for a URLRequest. + + +Here you see the parameter encoding protocol, simple right ? +the framework provides 3 implementations for this protocol: URL, JSON and String encoders but you can create your own implementation and then inject it into the URLRequest initializer. + +```swift +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} +``` + +The **Url encoding** will escape and encode the parameters in the url of the request or in the body depending the destination you choose. + +If you choose the body for a post request the property httpBody of the URLRequest will look like this: +params: name and password +```swift +"name=quickhatch&password=1232" +``` + +If you choose the URL it will look like this: +```swift +"www.theURL.com/api?name=quickhatch&password=1232" +``` + +Real Sample: +```swift +let getUsers = URLRequest.post(url: URL("getusers.com"), + params: ["page":1], + encoding: URLEncoding.bodyEncoding, + headers: ["Authorization": "token 1232"]) +``` diff --git a/Docs/URLRequestExtension.md b/Docs/URLRequestExtension.md new file mode 100644 index 0000000..d881d27 --- /dev/null +++ b/Docs/URLRequestExtension.md @@ -0,0 +1,38 @@ +## **URLRequest Extension** +- QuickHatch provides an extension of URLRequest for you to build the requests easier and faster +```swift +public extension URLRequest { + + static func get(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func post(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func put(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func delete(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func patch(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func connect(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func head(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func trace(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest + static func options(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest +} +``` + +Now Some samples... + + +```swift +let getUsers = URLRequest.get(url: URL("getusers.com"), + params: ["page":1], + encoding: URLEncoding.default, + headers: ["Authorization": "token 1232"]) + +let login = URLRequest.post(url: URL("lgin.com"), + params: ["user":"QuickHatch", "password": "quickhatch"], + encoding: URLEncoding.default, + headers: ["Authorization": "token 1232"]) + +let updateProfile = URLRequest.put(url: URL("profile.com"), + params: ["name":"QuickHatch"], + encoding: JSONEncoding.default, + headers: ["Authorization": "token 1232"]) +``` + +Building a request is so easy now! diff --git a/README.md b/README.md new file mode 100644 index 0000000..6951394 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ + + +# QuickHatchHTTP + +[![Swift](https://img.shields.io/badge/Swift-6.0)](https://img.shields.io/badge/Swift-6.0) +[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_visionOS_Linux_Windows_Android-yellowgreen)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_vision_OS_Linux_Windows_Android-Green) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-green)](https://img.shields.io/badge/Swift_Package_Manager-compatible-green) +- **Use an abstract networking layer** +- **Request/Response Pattern** +- **Keep networking simple** +- **Swift Concurrency support** +- **Combine support** +--- + + +## Features + +- **NetworkRequestFactory protocol**: + - [Getting started](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/GettingStarted.md) + - [Codable extension](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/CodableExtensions.md) + - [Data, String responses](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/Responses.md) + - [Combine Support](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/CombineSupport.md) + - [Errors](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/Error.md) +- **URLRequest Additions**: + - [HTTP Methods oriented](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/URLRequestExtension.md) +- **Parameter Encoding** + - [URLEncoding](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/URLEncoding.md) + - [JSONEncoding](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/JSONEncoding.md) + - [StringEncoding](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/StringEncoding.md) +- **Request/Response Pattern** + - [Index](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/HostEnvironment.md) +- **Certificate Pinning** + - [Index](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/CertificatePinning.md) +--- + +## Requirements + +- iOS 15.0+ +- WatchOS 7.0+ +- TvOS 14.0+ +- MacOS 12.14+ +- Xcode 10.2+ +- Swift 6+ + +--- + +## Installation + +## Swift Package Manager +QuickHatch has support for SPM, you just need to go to Xcode in the menu File/Swift Packages/Add package dependency +and you select the version of QuickHatchHTTP. + diff --git a/diagram.png b/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..e799bec52f15633d540d2fb34f575624c33d4ffe GIT binary patch literal 29842 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 TKJX)>ZzM!yg)2UP@&Eq-ra$J+ literal 0 HcmV?d00001 From 0f89176bf76d571ac56175ee7d231f612234094b Mon Sep 17 00:00:00 2001 From: Daniel Koster Date: Thu, 18 Dec 2025 16:38:42 -0300 Subject: [PATCH 2/4] updated readme file --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6951394..bdf2647 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ # QuickHatchHTTP -[![Swift](https://img.shields.io/badge/Swift-6.0)](https://img.shields.io/badge/Swift-6.0) -[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_visionOS_Linux_Windows_Android-yellowgreen)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_vision_OS_Linux_Windows_Android-Green) +[![Swift](https://img.shields.io/badge/Swift-6.0-green)](https://img.shields.io/badge/Swift-6.0-green) +[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_visionOS_Linux_Windows_Android-green)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_vision_OS_Linux_Windows_Android-Green) [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-green)](https://img.shields.io/badge/Swift_Package_Manager-compatible-green) - **Use an abstract networking layer** - **Request/Response Pattern** From 66feadf82fe6d03ab93c335549792631030a2682 Mon Sep 17 00:00:00 2001 From: Daniel Koster Date: Thu, 18 Dec 2025 16:56:20 -0300 Subject: [PATCH 3/4] Updated some docs --- Docs/AuthenticationDoc.md | 25 --- Docs/CodableExtensions.md | 2 +- Docs/CombineSupport.md | 18 +-- Docs/DispatcherExtension.md | 41 ----- Docs/Error.md | 8 +- Docs/GettingStarted.md | 11 +- Docs/HostEnvironment.md | 145 ------------------ Docs/ImageExtension.md | 39 ----- ...equestExtension.md => Request-Response.md} | 4 +- Docs/Responses.md | 42 +---- README.md | 22 +-- 11 files changed, 35 insertions(+), 322 deletions(-) delete mode 100644 Docs/AuthenticationDoc.md delete mode 100644 Docs/DispatcherExtension.md delete mode 100644 Docs/HostEnvironment.md delete mode 100644 Docs/ImageExtension.md rename Docs/{URLRequestExtension.md => Request-Response.md} (94%) diff --git a/Docs/AuthenticationDoc.md b/Docs/AuthenticationDoc.md deleted file mode 100644 index 7e72eaf..0000000 --- a/Docs/AuthenticationDoc.md +++ /dev/null @@ -1,25 +0,0 @@ -## **Authentication Protocol** -- This is a very useful protocol when you are working with Authenticated API's and you need to authorize requests - -```swift -public protocol Authentication { - var isAuthenticated: Bool { get } - func autenticate(params: [String: Any], completionHandler completion: @escaping (Result) -> Void) - func authorize(request: URLRequest) -> URLRequest - func clearCredentials() -} - -public protocol RefreshableAuthentication { - func refresh(params: [String: Any], completionHandler completion: @escaping (Result) -> Void) -} - -public protocol RevokableAuthentication { - func revoke(params: [String: Any], completionHandler completion: @escaping (Result) -> Void) -} -``` - -QuickHatch provides these 3 protocols for Authentication management, some Auths are revokables and refreshables. - -Usually you have an endpoint to fetch the set of tokens, (in your implementation you should use the authenticate method for that) and the you inject the auth token in the Authorization header for each request. - - diff --git a/Docs/CodableExtensions.md b/Docs/CodableExtensions.md index 908ac05..9c14121 100644 --- a/Docs/CodableExtensions.md +++ b/Docs/CodableExtensions.md @@ -3,7 +3,7 @@ Codable (Encodable & Decodable) is a protocol launched by Apple to encode and decode JSONs very easily, QuickHatch provides an extension for mapping responses to an object or an array. -This is a sample for a **response** mapping using QuickHatch: +This is a sample for a **response** mapping using QuickHatchHTTP: ```swift struct User: Codable { diff --git a/Docs/CombineSupport.md b/Docs/CombineSupport.md index 8690067..ce1bfcc 100644 --- a/Docs/CombineSupport.md +++ b/Docs/CombineSupport.md @@ -2,19 +2,9 @@ The network Request Factory also has support for swift Combine if you decide to go full Reactive programming. -```swift - func image(urlRequest: URLRequest, - dispatchQueue: DispatchQueue = .main, - quality: CGFloat) -> AnyPublisher - - func response(urlRequest: URLRequest, - dispatchQueue: DispatchQueue = .main, - jsonDecoder: JSONDecoder = JSONDecoder()) -> AnyPublisher - - func string(urlRequest: URLRequest, - dispatchQueue: DispatchQueue = .main, - jsonDecoder: JSONDecoder = JSONDecoder())-> AnyPublisher - +```swift + func response(urlRequest: URLRequest, + jsonDecoder: JSONDecoder = JSONDecoder()) -> AnyPublisher, Error> ``` This is a sample for a **response** mapping using QuickHatch Combine function: @@ -34,4 +24,4 @@ This is a sample for a **response** mapping using QuickHatch Combine function: ``` ---- \ No newline at end of file +--- diff --git a/Docs/DispatcherExtension.md b/Docs/DispatcherExtension.md deleted file mode 100644 index f75c971..0000000 --- a/Docs/DispatcherExtension.md +++ /dev/null @@ -1,41 +0,0 @@ -## **Dispatcher Extension** -- QuickHatch allows you to handle the thread you want to perform your results - - -QuickHatch has a parameter in each of the methods that is the dispatchQueue where the results will be dispatched, the default value is main thread but you can configure it easily -```swift -public extension NetworkRequestFactory { - func data(request: URLRequest, dispatchQueue: DispatchQueue = .main, completionHandler completion: @escaping DataCompletionHandler) -> Request -} -``` -If we want to handle our results in a background thread... - -```swift -import QuickHatch - -class LoginViewPresenter { - private let networkRequestFactory: NetworkRequestFactory - private weak var view: LoginView - - init(requestFactory: NetworkRequestFactory, view: LoginView) { - self.networkRequestFactory = requestFactory - self.view = view - } - - func login(user: String, password: String) { - let request = URLRequest("getimage.com/user=\(user)&password=\(password)") - let backgroundThread = DispatchQueue.global(qos: .background) - networkRequestFactory.data(urlRequest: request, dispatchQueue: backgroundThread) { result in - switch result { - case .success(let response): - // show error messsage - view?.showSuccessMessage(response.data) - case .failure(let error): - //show message or nothing - view?.showErrorMessage("Error downloading profile image") - } - } -} -``` - -The results of the login service will run on a background thread now. Super easy! diff --git a/Docs/Error.md b/Docs/Error.md index 4e8b303..6ac3313 100644 --- a/Docs/Error.md +++ b/Docs/Error.md @@ -1,16 +1,16 @@ ## **Errors** -- If an error is returned in a network request QuickHatch provides an enum with errors +- If an error is returned in a network request QuickHatchHTTP provides an enum with errors ```swift public enum RequestError: Error, Equatable { case unauthorized case unknownError(statusCode: Int) case cancelled case noResponse - case requestWithError(statusCode:HTTPStatusCode) - case serializationError + case requestWithError(statusCode: HTTPStatusCode) + case serializationError(error: Error) case invalidParameters - case noInternetConnection case malformedRequest + case other(error: Error) } ``` Now if we want to check what error is.. diff --git a/Docs/GettingStarted.md b/Docs/GettingStarted.md index fbef2cb..81a52dc 100644 --- a/Docs/GettingStarted.md +++ b/Docs/GettingStarted.md @@ -6,14 +6,17 @@ - What QuickHatch provides is a set of protocols that allows you to follow clean code standards, SOLID principles and allows you to be flexible when it comes to Networking. - First of all, your app will use a NetworkClient (this can be Alamofire, etc), and QuickHatch provides a protocol called NetworkRequestFactory that sets what a network client should respond when making a request. -![](https://github.com/dkoster95/QuickHatchSwift/blob/master/diagram.png) +![](https://github.com/dkoster95/QuickHatchHTTP/blob/main/diagram.png) -The name of this networkLayer interface in QuickHatch is NetworkRequestFactory :). +The name of this networkLayer interface in QuickHatchHTTP is NetworkRequestFactory :). ```swift public protocol NetworkRequestFactory { - func data(request: URLRequest, dispatchQueue: DispatchQueue, completionHandler completion: @escaping DataCompletionHandler) -> Request - func data(request: URLRequest, dispatchQueue: DispatchQueue) -> AnyPublisher + func data(request: URLRequest, + dispatchQueue: DispatchQueue, + completionHandler completion: @Sendable @escaping (Result) -> Void) -> DataTask + func dataPublisher(request: URLRequest) -> AnyPublisher + func data(request: URLRequest) async throws -> HTTPResponse } ``` diff --git a/Docs/HostEnvironment.md b/Docs/HostEnvironment.md deleted file mode 100644 index 6b4208d..0000000 --- a/Docs/HostEnvironment.md +++ /dev/null @@ -1,145 +0,0 @@ -## **Host Environment classes** - - - QuickHatch also provides a set of protocols to make your API specification easy to make. - - Usually our app consume many services from a server(sometimes more than one) - - That server usually has 3 environments (QA, Staging and Production) - - ```swift -public protocol HostEnvironment { - var baseURL: String { get } - var headers: [String: String] { get } -} - ``` - - This is the base protocol for a Host, baseURL and headers. - - For a sophisticated implementation we have the next protocols: - ```swift - public protocol ServerEnvironmentConfiguration: Any { - var headers: [String: String] { get } - var qa: HostEnvironment { get } - var staging: HostEnvironment { get } - var production: HostEnvironment { get } -} - - -public enum Environment: String { - case qa = "QA" - case staging = "Staging" - case production = "Prod" -} - -public extension Environment { - func getCurrentEnvironment(server: ServerEnvironmentConfiguration) -> HostEnvironment { - switch self { - case .qa: - return server.qa - case .staging: - return server.staging - case .production: - return server.production - } - } -} - -public protocol GenericAPI { - var hostEnvironment: HostEnvironment { get set } - var path: String { get } -} -``` - - - For example lets say we are building an app with a server called QuickHatchServer and we have a User API with getUsers, fetchUserById and deleteUser. - - Also the server has 3 environments with 3 diferent URLs and headers. - - First lets create a class for our server specification with 3 environments -```swift - class QuickHatchServer: ServerEnvironmentConfiguration { - var headers: [String: Any] { - return ["User-Agent":"QuickHatch"] - } - var qa: HostEnvironment { - return GenericHostEnvironment(headers: headers, baseURL: "quickhatch-qa.com") - } - var staging: HostEnvironment { - return GenericHostEnvironment(headers: headers, baseURL: "quickhatch-stg.com") - } - var production: HostEnvironment { - return GenericHostEnvironment(headers: headers, baseURL: "quickhatch-prod.com") - } - } -``` -(GenericHostEnvironment is an implementation of HostEnvironment initalized with headers and baseURL) -Now we have our server class there with all the environments and headers configured (Keep in mind this is just an example to show how it works) - -Lets say we have a config class where we decide which server we re gonna use (QA, Staging or Prod) -```swift - class AppConfig { - static func getHost(environment: Environment) -> HostEnvironment { - let quickhatchServer = QuickHatchServer() - return environment.getCurrentEnvironment(server: quickhatchServer) - } - } -``` - -And now we are gonna create our API struct - -```swift - struct UserAPI: GenericAPI { - var hostEnvironment: HostEnvironment - var path: String { return "/api/" } - - init(hostEnvironment: HostEnvironment) { - self.hostEnvironment = hostEnvironment - } - - func getUsers(authentication: Authentication) -> URLRequest { - let stringURL = "\(hostEnvironment.baseURL)\(path)getusers.com" - let request = URLRequest.post(url: URL(stringURL), - encoding: URLEncoding.default) - return authentication.authorize(request: request) - } - - func fetchUser(authentication: Authentication, id: String) -> URLRequest { - let stringURL = "\(hostEnvironment.baseURL)\(path)getusers.com/{id}" - let request = URLRequest.get(url: URL(stringURL), - params: ["id": id], - encoding: StringEncoding.urlEncoding) - return authentication.authorize(request: request) - } - - func deleteUser(authentication: Authentication, id: String) -> URLRequest { - let stringURL = "\(hostEnvironment.baseURL)\(path)getusers.com" - let request = URLRequest.delete(url: URL(stringURL), - params: ["id":id] - encoding: URLEncoding.default) - return authentication.authorize(request: request) - } - } -``` - -And thats it for your API specification! -Keep in mind that in this example we used an authentication that inject a field in the Authorization header, dont freak out! - -Now the only thing you need is instantiate the API injecting the host - -```swift - let host = AppConfig.getHost(environment: .qa) - let userApi = UserAPI(hostEnvironment: host) - - let getUsersRequest = userApi.getUsers(authentication: yourAuthImplementation) - // Now we handle the response with QuickHatch implementation - requestFactory.array(request: getUsersRequest) { (result: Result) in - switch result { - case .success(let response): - print(response.data) - //array printed - case .failure(let error): - print(error) - //error printed - } - } -``` - -How Simple is to make your requests with QuickHatch! diff --git a/Docs/ImageExtension.md b/Docs/ImageExtension.md deleted file mode 100644 index 95e165d..0000000 --- a/Docs/ImageExtension.md +++ /dev/null @@ -1,39 +0,0 @@ -## **Downloading Images** -- When you develop an app, you will probably use images to show content and design your UI -- QuickHatch provides an extension for downloading images from the net simple - -```swift -public extension NetworkRequestFactory { - func image(urlRequest: URLRequest, quality: CGFloat = 1, dispatchQueue: DispatchQueue = .main, completion completionHandler: @escaping (Result) -> Void) -> Request -} -``` -Now a sample of its use would be... - -```swift -import QuickHatch - -class LoginViewPresenter { - private let networkRequestFactory: NetworkRequestFactory - private var view: LoginView - - init(requestFactory: NetworkRequestFactory, view: LoginView) { - self.networkRequestFactory = requestFactory - self.view = view - } - - func showProfileImage() { - let request = URLRequest("getimage.com") - networkRequestFactory.image(urlRequest: request) { result in - switch result { - case .success(let image): - // update UI or cache image - view.showProfileImage(image) - case .failure(let error): - //show message or nothing - view.showErrorMessage("Error downloading profile image") - } - } -} -``` - -And just like that you can use The image extension. \ No newline at end of file diff --git a/Docs/URLRequestExtension.md b/Docs/Request-Response.md similarity index 94% rename from Docs/URLRequestExtension.md rename to Docs/Request-Response.md index d881d27..6b37fba 100644 --- a/Docs/URLRequestExtension.md +++ b/Docs/Request-Response.md @@ -1,5 +1,5 @@ -## **URLRequest Extension** -- QuickHatch provides an extension of URLRequest for you to build the requests easier and faster +## **Request/Response Pattern** +- QuickHatchHTTP provides an easy way to create a request and fetch the response like Python Requests does ```swift public extension URLRequest { diff --git a/Docs/Responses.md b/Docs/Responses.md index b9026ed..ae7b3f3 100644 --- a/Docs/Responses.md +++ b/Docs/Responses.md @@ -1,18 +1,16 @@ -## **Data and String responses** -- Use QuickHatch to parse your request responses into the standard types +## **Data responses** +- Use QuickHatchHTTP to parse your request responses into the standard types - Handle the response however you want to do it Remember the Base protocol.... ```swift public protocol NetworkRequestFactory { - func data(request: URLRequest, dispatchQueue: DispatchQueue, completionHandler completion: @escaping DataCompletionHandler) -> Request - ... - .. + func data(request: URLRequest, + dispatchQueue: DispatchQueue, + completionHandler completion: @Sendable @escaping (Result) -> Void) -> DataTask + func data(request: URLRequest) async throws -> HTTPResponse } -extension NetworkRequestFactory { - func string(request: URLRequest,dispatchQueue: DispatchQueue, completionHandler completion: @escaping StringCompletionHandler) -> Request -} ``` If we want to handle our login response back as **Data** Type.... @@ -69,32 +67,4 @@ class LoginViewPresenter { } } ``` -If you want to use **String** response type.... -```swift -import QuickHatch - -class LoginViewPresenter { - private let networkRequestFactory: NetworkRequestFactory - private var view: LoginView - - init(requestFactory: NetworkRequestFactory, view: LoginView) { - self.networkRequestFactory = requestFactory - self.view = view - } - - func login(user: String, password: String) { - let request = URLRequest("getimage.com/user=\(user)&password=\(password)") - networkRequestFactory.string(request: request) { result in - switch result { - case .success(let response): - // show error messsage - view.showSuccessMessage(response.data) - case .failure(let error): - //show message or nothing - view.showErrorMessage("Error downloading profile image") - } - } -} -``` - This is an alternative for handling the responses, you can also check the Codable Extension to parse the response into a Codable Object of your own diff --git a/README.md b/README.md index bdf2647..3d12d40 100644 --- a/README.md +++ b/README.md @@ -16,21 +16,21 @@ ## Features - **NetworkRequestFactory protocol**: - - [Getting started](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/GettingStarted.md) - - [Codable extension](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/CodableExtensions.md) - - [Data, String responses](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/Responses.md) - - [Combine Support](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/CombineSupport.md) - - [Errors](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/Error.md) + - [Getting started](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/GettingStarted.md) + - [Codable extension](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/CodableExtensions.md) + - [Data, String responses](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/Responses.md) + - [Combine Support](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/CombineSupport.md) + - [Errors](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/Error.md) - **URLRequest Additions**: - - [HTTP Methods oriented](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/URLRequestExtension.md) + - [HTTP Methods oriented](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/URLRequestExtension.md) - **Parameter Encoding** - - [URLEncoding](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/URLEncoding.md) - - [JSONEncoding](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/JSONEncoding.md) - - [StringEncoding](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/StringEncoding.md) + - [URLEncoding](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/URLEncoding.md) + - [JSONEncoding](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/JSONEncoding.md) + - [StringEncoding](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/StringEncoding.md) - **Request/Response Pattern** - - [Index](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/HostEnvironment.md) + - [Index](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/HostEnvironment.md) - **Certificate Pinning** - - [Index](https://github.com/dkoster95/QuickHatchSwift/blob/master/Docs/CertificatePinning.md) + - [Index](https://github.com/dkoster95/QuickHatchHTTP/blob/main/Docs/CertificatePinning.md) --- ## Requirements From 4d13aa0f332701a244237f8aa097d20dd90cef6c Mon Sep 17 00:00:00 2001 From: Daniel Koster Date: Thu, 18 Dec 2025 17:01:23 -0300 Subject: [PATCH 4/4] finish updating docs --- Docs/Request-Response.md | 55 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/Docs/Request-Response.md b/Docs/Request-Response.md index 6b37fba..0c09603 100644 --- a/Docs/Request-Response.md +++ b/Docs/Request-Response.md @@ -1,38 +1,41 @@ ## **Request/Response Pattern** - QuickHatchHTTP provides an easy way to create a request and fetch the response like Python Requests does ```swift -public extension URLRequest { - - static func get(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func post(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func put(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func delete(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func patch(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func connect(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func head(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func trace(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest - static func options(url: URL, params: [String: Any] = [:], encoding: ParameterEncoding, headers: [String: String] = [:]) throws -> URLRequest -} +public protocol HTTPRequest { + var headers: [String: String] { get } + var body: Data? { get } + var url: String { get } + var method: HTTPMethod { get } +} + +public protocol HTTPRequestActionable { + func response() async throws -> HTTPResponse + var responsePublisher: any Publisher { get } +} + +public protocol HTTPRequestDecodedActionable { + associatedtype ResponseType: Codable + func responseDecoded() async throws -> Response + var responseDecodedPublisher: any Publisher { get } +} + +public protocol HTTPResponse { + var statusCode: HTTPStatusCode { get } + var headers: [AnyHashable: Any] { get } + var body: Data? { get } +} ``` Now Some samples... ```swift -let getUsers = URLRequest.get(url: URL("getusers.com"), - params: ["page":1], - encoding: URLEncoding.default, - headers: ["Authorization": "token 1232"]) - -let login = URLRequest.post(url: URL("lgin.com"), - params: ["user":"QuickHatch", "password": "quickhatch"], - encoding: URLEncoding.default, - headers: ["Authorization": "token 1232"]) - -let updateProfile = URLRequest.put(url: URL("profile.com"), - params: ["name":"QuickHatch"], - encoding: JSONEncoding.default, - headers: ["Authorization": "token 1232"]) +let getUsersRequest = QHHTTPRequest(url: "quickhatch", method: .get, headers: ["Authorization": "token1234"]) +do { +let response = try await getUsers.responseDecoded() +} catch let error as RequestError { + //do something with error +} ``` Building a request is so easy now!