diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata index 706eede..919434a 100644 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..3cadc13 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "PublisherKit", + "repositoryURL": "https://github.com/ragzy15/PublisherKit", + "state": { + "branch": null, + "revision": "ad211780f0f532a473e3d5d1cef2a2644024d7c4", + "version": "1.0.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index de035f5..68f8439 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,12 @@ import PackageDescription let package = Package( name: "NetworkKit", + platforms: [ + .iOS(.v8), + .macOS(.v10_10), + .tvOS(.v10), + .watchOS(.v4) + ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( @@ -12,6 +18,7 @@ let package = Package( targets: ["NetworkKit"]), ], dependencies: [ + .package(url: "https://github.com/ragzy15/PublisherKit", from: .init(1, 0, 0)) // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], @@ -20,7 +27,7 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "NetworkKit", - dependencies: []), + dependencies: ["PublisherKit"]), .testTarget( name: "NetworkKitTests", dependencies: ["NetworkKit"]), diff --git a/Sources/NetworkKit/Connection/Connection Representable.swift b/Sources/NetworkKit/Connection/Connection Representable.swift new file mode 100644 index 0000000..35f09ea --- /dev/null +++ b/Sources/NetworkKit/Connection/Connection Representable.swift @@ -0,0 +1,60 @@ +// +// ConnectionRepresentable.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +/** + A type that represents as a connection or an endpoint. + + ``` + let url = "https://api.example.com/users/all" + // `/users/all` is a connection. + ``` + */ +public protocol ConnectionRepresentable { + + /** + The path subcomponent. It is the connection endpoint for the url. + + ``` + let url = "https://api.example.com/users/all" + // `/users/all` is the path for this connection + ``` + + Setting this property assumes the subcomponent or component string is not percent encoded and will add percent encoding (if the component allows percent encoding). + */ + var path: String { get } + + /// Connection name if any. Use for console logging. Defaults to connection url if provided `nil`. + var name: String? { get } // for console logging purposes only + + /// HTTP Method for the connection request. + var method: HTTPMethod { get } + + /// Default Headers attached to connection request. Example: ["User-Agent": "iOS_13_0"] + var httpHeaders: HTTPHeaderParameters { get } + + /// The scheme subcomponent of the URL. Default value is `.https` + var scheme: Scheme { get } + + /// Host for the connection. + var host: HostRepresentable { get } + + /// Default URL Query for connection. Example: ["client": "ios"] + var defaultQuery: URLQuery? { get } + + /// API Type for connection. Default value is `host.defaultAPIType`. + var apiType: APIRepresentable? { get } +} + +public extension ConnectionRepresentable { + + var scheme: Scheme { .https } + + var apiType: APIRepresentable? { host.defaultAPIType } +} diff --git a/Sources/NetworkKit/Create Request/Create Request.swift b/Sources/NetworkKit/Create Request/Create Request.swift new file mode 100644 index 0000000..4a9841e --- /dev/null +++ b/Sources/NetworkKit/Create Request/Create Request.swift @@ -0,0 +1,61 @@ +// +// CreateRequest.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public typealias URLQuery = [String: String?] +public typealias HTTPHeaderParameters = [String: String] + +public struct CreateRequest { + + public let request: URLRequest + + public init?(with connection: ConnectionRepresentable, query urlQuery: Set, body: Data?, headers: HTTPHeaderParameters) { + + var components = URLComponents() + components.scheme = connection.scheme.rawValue + + let subURL = connection.apiType?.subUrl ?? "" + let endPoint = connection.apiType?.endPoint ?? "" + + components.host = (subURL.isEmpty ? subURL : subURL + ".") + connection.host.host + components.path = endPoint + connection.path + + var queryItems = Set() + queryItems.addURLQuery(query: connection.defaultQuery) + queryItems.addURLQuery(query: connection.host.defaultUrlQuery) + queryItems = queryItems.union(urlQuery) + + let method = connection.method + + if !queryItems.isEmpty { + components.queryItems = Array(queryItems) + } + + guard let url = components.url else { + return nil + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = method.rawValue + + let defaultHeaderFields = connection.host.defaultHeaders + let connectionHeaderFields = connection.httpHeaders + + var headerFields = defaultHeaderFields.merging(connectionHeaderFields) { (_, new) in new } + headerFields.merge(headers) { (_, new) in new } + + if !headerFields.isEmpty { + urlRequest.allHTTPHeaderFields = headerFields + } + + urlRequest.httpBody = body + request = urlRequest + + } +} diff --git a/Sources/NetworkKit/Debug Loging/NKLogger.swift b/Sources/NetworkKit/Debug Loging/NKLogger.swift new file mode 100644 index 0000000..9bcb0e5 --- /dev/null +++ b/Sources/NetworkKit/Debug Loging/NKLogger.swift @@ -0,0 +1,226 @@ +// +// NKLogger.swift +// NetworkKit +// +// Created by Raghav Ahuja on 18/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +final public class NKLogger { + + /// Allows Logs to be Printed in Debug Console. + /// Default value is `true` + public var isLoggingEnabled: Bool = true + + public static let `default` = NKLogger() + + /** + Creates a `NKLogger`. + */ + public init() { } + + /** + Writes the textual representations of the given items into the standard output. + + - parameter items: Zero or more items to print.. + - parameter separator: A string to print between each item. The default is a single space (" "). + - parameter terminator: The string to print after all items have been printed. The default is a newline ("\n"). + + */ + func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { + #if DEBUG + guard NKConfiguration.allowLoggingOnAllSessions, isLoggingEnabled else { return } + Swift.print(items, separator: separator, terminator: terminator) + #endif + } + + /** + Writes the textual representations of the given items most suitable for debugging into the standard output. + + - parameter items: Zero or more items to print. + - parameter separator: A string to print between each item. The default is a single space (" "). + - parameter terminator: The string to print after all items have been printed. The default is a newline ("\n"). + + */ + func debugPrint(_ items: Any..., separator: String = " ", terminator: String = "\n") { + #if DEBUG + guard NKConfiguration.allowLoggingOnAllSessions, isLoggingEnabled else { return } + Swift.debugPrint(items, separator: separator, terminator: terminator) + #endif + } + + /** + Handles APIRequest logging sent by the `NetworkKit`. + + - parameter request: URLRequest + - parameter apiName: API name. + */ + func logAPIRequest(request: URLRequest?, apiName: String?) { + #if DEBUG + guard NKConfiguration.allowLoggingOnAllSessions, isLoggingEnabled else { return } + + Swift.print( + """ + ------------------------------------------------------------ + API Call Request for: + Name: \(apiName ?? "nil") + \(request?.debugDescription ?? "") + + """ + ) + #endif + } + + /** + Print JSON sent by the `NetworkKit`. + + - parameter data: Input Type to be printed + - parameter apiName: API name. + */ + func printJSON(data: Input, apiName: String) { + #if DEBUG + guard NKConfiguration.allowLoggingOnAllSessions, isLoggingEnabled else { return } + guard let data = data as? Data else { + return + } + + do { + let object = try JSONSerialization.jsonObject(with: data, options: []) + let newData = try JSONSerialization.data(withJSONObject: object, options: .prettyPrinted) + +// Swift.print( +// """ +// ------------------------------------------------------------ +// Printing JSON for: +// API Name: \(apiName) +// JSON: +// + // """) + Swift.print(""" + ------------------------------------------------------------ + JSON: + + """) + Swift.print(String(data: newData, encoding: .utf8) ?? "nil") + Swift.print("------------------------------------------------------------") + + } catch { + + } + #endif + } + + /** + Handles errors sent by the `NetworkKit`. + + - parameter error: Error occurred. + - parameter file: Source file name. + - parameter line: Source line number. + - parameter function: Source function name. + */ + @inline(__always) + func log(error: Error, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) { + #if DEBUG + guard NKConfiguration.allowLoggingOnAllSessions, isLoggingEnabled else { return } + + Swift.print("⚠️ [NetworkKit: Error] \((String(describing: file) as NSString).lastPathComponent):\(line) \(function)\n ↪︎ \(error as NSError)\n") + #endif + } + + /** + Handles assertions made throughout the `NetworkKit`. + + - parameter condition: Assertion condition. + - parameter message: Assertion failure message. + - parameter file: Source file name. + - parameter line: Source line number. + - parameter function: Source function name. + */ + @inline(__always) + func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) { + + #if DEBUG + let condition = condition() + + if condition { return } + + let message = message() + + Swift.print("❗ [NetworkKit: Assertion Failure] \((String(describing: file) as NSString).lastPathComponent):\(line) \(function)\n ↪︎ \(message)\n") + Swift.assert(condition, message, file: file, line: line) + #endif + } + + /** + Handles assertion failures made throughout the `NetworkKit`. + + - parameter message: Assertion failure message. + - parameter file: Source file name. + - parameter line: Source line number. + - parameter function: Source function name. + */ + @inlinable public func assertionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line, function: StaticString = #function) { + let message = message() + Swift.print("❗ [NetworkKit: Assertion Failure] \((String(describing: file) as NSString).lastPathComponent):\(line) \(function)\n ↪︎ \(message)\n") + Swift.assertionFailure(message, file: file, line: line) + } + + /** + Handles precondition failures made throughout the `NetworkKit`. + + - parameter message: Assertion failure message. + - parameter file: Source file name. + - parameter line: Source line number. + - parameter function: Source function name. + */ + @inlinable public func preconditionFailure(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line, function: StaticString = #function) -> Never { + let message = message() + Swift.print("❗ [NetworkKit: Assertion Failure] \((String(describing: file) as NSString).lastPathComponent):\(line) \(function)\n ↪︎ \(message)\n") + Swift.preconditionFailure(message, file: file, line: line) + } + + /** + Handles preconditions made throughout the `NetworkKit`. + + - parameter condition: Precondition to be satisfied. + - parameter message: Precondition failure message. + - parameter file: Source file name. + - parameter line: Source line number. + - parameter function: Source function name. + */ + @inline(__always) + func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) { + + #if DEBUG + let condition = condition() + + if condition { return } + + let message = message() + + Swift.print("❗ [NetworkKit: Precondition Failure] \((String(describing: file) as NSString).lastPathComponent):\(line) \(function)\n ↪︎ \(message)\n") + Swift.preconditionFailure(message, file: file, line: line) + #endif + } + + /** + Handles fatal errors made throughout the `NetworkKit`. + - Important: Implementers should guarantee that this function doesn't return, either by calling another `Never` function such as `fatalError()` or `abort()`, or by raising an exception. + + - parameter message: Fatal error message. + - parameter file: Source file name. + - parameter line: Source line number. + - parameter function: Source function name. + */ + @inline(__always) + func fatalError(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) -> Never { + + #if DEBUG + let message = message() + Swift.print("❗ [NetworkKit: Fatal Error] \((String(describing: file) as NSString).lastPathComponent):\(line) \(function)\n ↪︎ \(message)\n") + Swift.fatalError(message, file: file, line: line) + #endif + } +} diff --git a/Sources/NetworkKit/Errors/NSError.swift b/Sources/NetworkKit/Errors/NSError.swift new file mode 100644 index 0000000..4708a1f --- /dev/null +++ b/Sources/NetworkKit/Errors/NSError.swift @@ -0,0 +1,109 @@ +// +// NSError.swift +// NetworkKit +// +// Created by Raghav Ahuja on 18/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +extension NSError { + + public static var networkKitErrorDomain: String { "NKErrorDomain" } + + static func cancelled(for url: URL?) -> NSError { + var userInfo: [String: Any] = [NSLocalizedDescriptionKey: "User cancelled the task for url: \(url?.absoluteString ?? "nil")."] + if let url = url { + userInfo[NSURLErrorFailingURLErrorKey] = url + } + + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorCancelled, userInfo: userInfo) + + return error + } + + static func badServerResponse(for url: URL) -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorBadServerResponse, userInfo: [ + NSURLErrorFailingURLErrorKey: url, + NSLocalizedDescriptionKey: "Bad server response for request : \(url.absoluteString)" + ]) + + return error + } + + static func badURL(for urlString: String?) -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorBadURL, userInfo: [ + NSURLErrorFailingURLStringErrorKey: urlString ?? "nil", + NSLocalizedDescriptionKey: "Invalid URL provied." + ]) + + return error + } + + static func resourceUnavailable(for url: URL) -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorResourceUnavailable, userInfo: [ + NSURLErrorFailingURLErrorKey: url, + NSLocalizedDescriptionKey: "A requested resource couldn’t be retrieved from url: \(url.absoluteString)." + ]) + + return error + } + + static func unsupportedURL(for url: URL?) -> NSError { + var userInfo: [String: Any] = [NSLocalizedDescriptionKey: "A requested resource couldn’t be retrieved from url: \(url?.absoluteString ?? "nil")."] + if let url = url { + userInfo[NSURLErrorFailingURLErrorKey] = url + } + + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorUnsupportedURL, userInfo: userInfo) + + return error + } + + static func zeroByteResource(for url: URL) -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorZeroByteResource, userInfo: [ + NSURLErrorFailingURLErrorKey: url, + NSLocalizedDescriptionKey: "A server reported that a URL has a non-zero content length, but terminated the network connection gracefully without sending any data." + ]) + + return error + } + + static func cannotDecodeContentData(for url: URL) -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorCannotDecodeContentData, userInfo: [ + NSURLErrorFailingURLErrorKey: url, + NSLocalizedDescriptionKey: "Content data received during a connection request had an unknown content encoding." + ]) + + return error + } + + static func cannotDecodeRawData(for url: URL) -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorCannotDecodeRawData, userInfo: [ + NSURLErrorFailingURLErrorKey: url, + NSLocalizedDescriptionKey: "Content data received during a connection request had an unknown content encoding." + ]) + + return error + } + + static func notStarted(for url: URL?) -> NSError { + var userInfo: [String: Any] = [NSLocalizedDescriptionKey: "An asynchronous load has been canceled or not started."] + if let url = url { + userInfo[NSURLErrorFailingURLErrorKey] = url + } + + let error = NSError(domain: networkKitErrorDomain, code: NSUserCancelledError, userInfo: userInfo) + + return error + } + + static func unkown() -> NSError { + let error = NSError(domain: networkKitErrorDomain, code: NSURLErrorUnknown, userInfo: [ + NSLocalizedDescriptionKey: "An Unknown Occurred." + ]) + + return error + } +} diff --git a/Sources/NetworkKit/Extensions/Data+extension.swift b/Sources/NetworkKit/Extensions/Data+extension.swift new file mode 100644 index 0000000..db4d4ad --- /dev/null +++ b/Sources/NetworkKit/Extensions/Data+extension.swift @@ -0,0 +1,16 @@ +// +// Data+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +extension Data { + + var debugDescription: String { + return String(data: self, encoding: .utf8) ?? "nil" + } +} diff --git a/Sources/NetworkKit/Extensions/NotificationName+Extension.swift b/Sources/NetworkKit/Extensions/NotificationName+Extension.swift new file mode 100644 index 0000000..134c125 --- /dev/null +++ b/Sources/NetworkKit/Extensions/NotificationName+Extension.swift @@ -0,0 +1,14 @@ +// +// NotificationName+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public extension Notification.Name { + + static let logAPIAnalytics = Notification.Name(rawValue: "logAPIAnalytics") +} diff --git a/Sources/NetworkKit/Extensions/Result+Extension.swift b/Sources/NetworkKit/Extensions/Result+Extension.swift new file mode 100644 index 0000000..d407ada --- /dev/null +++ b/Sources/NetworkKit/Extensions/Result+Extension.swift @@ -0,0 +1,22 @@ +// +// Result+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +extension Result where Failure: Error { + + func getError() -> Failure? { + switch self { + case .success: + return nil + + case .failure(let error): + return error + } + } +} diff --git a/Sources/NetworkKit/Extensions/Set+Extension.swift b/Sources/NetworkKit/Extensions/Set+Extension.swift new file mode 100644 index 0000000..63b79dc --- /dev/null +++ b/Sources/NetworkKit/Extensions/Set+Extension.swift @@ -0,0 +1,28 @@ +// +// Set+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +extension Set where Element == URLQueryItem { + + mutating func addURLQuery(query urlQuery: URLQuery?) { + if let urlQuery = urlQuery { + for query in urlQuery { + let queryItem = URLQueryItem(name: query.key, value: query.value) + if !self.contains(queryItem) { + insert(queryItem) + } + } + } + } + + var toDictionary: URLQuery { + let params = Dictionary(uniqueKeysWithValues: self.map { ($0.name, $0.value) }) + return params + } +} diff --git a/Sources/NetworkKit/Extensions/String+Extension.swift b/Sources/NetworkKit/Extensions/String+Extension.swift new file mode 100644 index 0000000..4702dbe --- /dev/null +++ b/Sources/NetworkKit/Extensions/String+Extension.swift @@ -0,0 +1,16 @@ +// +// String+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +extension String.StringInterpolation { + + mutating func appendInterpolation(_ value: Environment) { + appendInterpolation(value.value) + } +} diff --git a/Sources/NetworkKit/Extensions/UIImage+NSImage+Extension.swift b/Sources/NetworkKit/Extensions/UIImage+NSImage+Extension.swift new file mode 100644 index 0000000..5d30d96 --- /dev/null +++ b/Sources/NetworkKit/Extensions/UIImage+NSImage+Extension.swift @@ -0,0 +1,42 @@ +// +// UIImage+NSImage+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +#if canImport(WatchKit) + +import UIKit.UIImage + +extension UIImage { + + static func initialize(using data: inout Data) -> UIImage? { + UIImage(data: data) + } +} + +#elseif canImport(UIKit) + +import UIKit.UIImage + +extension UIImage { + + static func initialize(using data: inout Data) -> UIImage? { + UIImage(data: data, scale: UIScreen.main.scale) + } +} + +#elseif canImport(AppKit) + +import AppKit.NSImage + +extension NSImage { + + static func initialize(using data: inout Data) -> NSImage? { + NSImage(data: data) + } +} + +#endif diff --git a/Sources/NetworkKit/Extensions/UIImageView+NSImageView+WKInterfaceImage+Extension.swift b/Sources/NetworkKit/Extensions/UIImageView+NSImageView+WKInterfaceImage+Extension.swift new file mode 100644 index 0000000..649683a --- /dev/null +++ b/Sources/NetworkKit/Extensions/UIImageView+NSImageView+WKInterfaceImage+Extension.swift @@ -0,0 +1,47 @@ +// +// UIImageView+NSImageView+WKInterfaceImage+Extension.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +#if canImport(WatchKit) + +import WatchKit + +extension WKInterfaceImage: NKImageSessionDelegate { + + public var image: ImageType? { + get { nil } + set { setImage(newValue) } + } + + open func prepareForReuse(_ placeholder: ImageType? = nil) { + setImage(placeholder) + } +} + +#elseif canImport(UIKit) + +import UIKit.UIImage + +extension UIImageView: NKImageSessionDelegate { + + open func prepareForReuse(_ placeholder: ImageType? = nil) { + image = placeholder + } +} + +#elseif canImport(AppKit) + +import AppKit.NSImage + +extension NSImageView: NKImageSessionDelegate { + + open func prepareForReuse(_ placeholder: ImageType? = nil) { + image = placeholder + } +} + +#endif diff --git a/Sources/NetworkKit/NetworkKit.swift b/Sources/NetworkKit/NetworkKit.swift deleted file mode 100644 index 4361043..0000000 --- a/Sources/NetworkKit/NetworkKit.swift +++ /dev/null @@ -1,3 +0,0 @@ -struct NetworkKit { - var text = "Hello, World!" -} diff --git a/Sources/NetworkKit/Networking/Configuration/Configuration+Notification.swift b/Sources/NetworkKit/Networking/Configuration/Configuration+Notification.swift new file mode 100644 index 0000000..df088ad --- /dev/null +++ b/Sources/NetworkKit/Networking/Configuration/Configuration+Notification.swift @@ -0,0 +1,28 @@ +// +// NKConfiguration+Notification.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +#if canImport(AppKit) +import AppKit.NSApplication + +// MARK: - NOTIFICATION OBSERVERS +extension NKConfiguration { + var notification: Notification.Name { NSApplication.willTerminateNotification } + +} + +#elseif canImport(WatchKit) + +#elseif canImport(UIKit) +import UIKit.UIApplication + +// MARK: - NOTIFICATION OBSERVERS +extension NKConfiguration { + var notification: Notification.Name { UIApplication.willTerminateNotification } +} + +#endif diff --git a/Sources/NetworkKit/Networking/Configuration/Configuration.swift b/Sources/NetworkKit/Networking/Configuration/Configuration.swift new file mode 100644 index 0000000..65db5a8 --- /dev/null +++ b/Sources/NetworkKit/Networking/Configuration/Configuration.swift @@ -0,0 +1,163 @@ +// +// NKConfiguration.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +// MARK: - NKCONFIGURATION + +/// A Class that coordinates a group of related network data transfer tasks. +open class NKConfiguration { + + /// Enables Logging for this session. + /// Default value is `true`. + open var enableLogs: Bool { + get { + logger.isLoggingEnabled + } set { + logger.isLoggingEnabled = newValue + } + } + + var logger = NKLogger() + + /// Should allow logging for all sessions. + /// Default value is `false`. + public static var allowLoggingOnAllSessions: Bool = true + + /// Should empty cache before application terminates. + /// Default value is `true`. + open var emptyCacheOnAppTerminate: Bool + + /// Should empty cache before application terminates. + /// Default value is `false`. + public static var emptyCacheOnAppTerminateOnAllSessions: Bool = false + + /// URL Cache for a URLSession + public let urlCache: URLCache? + + /// An object that coordinates a group of related network data transfer tasks. + public let session: URLSession + + /// ACCEPTABLE STATUS CODES + open var acceptableStatusCodes: [Int] = Array(200 ..< 300) + + /// A default session configuration object. + public static var defaultConfiguration: URLSessionConfiguration = { + let configuration: URLSessionConfiguration = .default + + configuration.requestCachePolicy = .useProtocolCachePolicy + if #available(iOS 11.0, watchOS 4.0, tvOS 11.0, macOS 10.13, *) { + configuration.waitsForConnectivity = true + } + configuration.networkServiceType = .responsiveData + configuration.timeoutIntervalForRequest = TimeInterval(integerLiteral: 20) + configuration.timeoutIntervalForResource = TimeInterval(integerLiteral: 20) + + return configuration + }() + + /// Inititalises Manager with `defaultURLSessionConfiguration` Configuration. + public init(useDefaultCache: Bool, requestCachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, cacheDiskPath diskPath: String? = nil) { + let configuration = NKConfiguration.defaultConfiguration + configuration.requestCachePolicy = requestCachePolicy + + if useDefaultCache { + if #available(iOS 13.0, watchOS 6.0, tvOS 13.0, macOS 10.15, *) { + let cache = URLCache(memoryCapacity: 5242880, diskCapacity: 52428800) + urlCache = cache + } else { + let cache = URLCache(memoryCapacity: 5242880, diskCapacity: 52428800, diskPath: diskPath) + urlCache = cache + } + configuration.urlCache = urlCache + } else { + urlCache = nil + } + + session = URLSession(configuration: configuration) +// session.nkLogger = .init() + emptyCacheOnAppTerminate = true + setNotificationObservers() + } + + public init(urlCache cache: URLCache? = nil, configuration config: URLSessionConfiguration) { + urlCache = cache + session = URLSession(configuration: config) +// session.nkLogger = .init() + emptyCacheOnAppTerminate = true + setNotificationObservers() + } + + deinit { + NotificationCenter.default.removeObserver(self) + session.invalidateAndCancel() + } + + // MARK: - NOTIFICATION OBSERVERS + private func setNotificationObservers() { + #if !canImport(WatchKit) + NotificationCenter.default.addObserver(forName: notification, object: nil, queue: .init()) { [weak self] (_) in + if let `self` = self, self.emptyCacheOnAppTerminate || NKConfiguration.emptyCacheOnAppTerminateOnAllSessions { + self.removeAllCachedResponses() + } + } + #endif + } +} + + +// MARK: - URL CACHE MANAGER +extension NKConfiguration { + + /// Remove All Cached Responses from this session. + open func removeAllCachedResponses() { + session.configuration.urlCache?.removeAllCachedResponses() + } + + #if DEBUG + public func printURLCacheDetails() { + guard let cache = session.configuration.urlCache else { + logger.print( + "Cannot Print Cache Memory And Disk Capacity And Usage", + "Error - No URL Cache Found") + return + } + let byteToMb: Double = 1048576 + + let memoryCapacity = Double(cache.memoryCapacity) / byteToMb + let memoryUsage = Double(cache.currentMemoryUsage) / byteToMb + + let diskCapacity = Double(cache.diskCapacity) / byteToMb + let diskUsage = Double(cache.currentDiskUsage) / byteToMb + + logger.print( + "Current URL Cache Memory And Disk Capacity And Usage", + "Memory Capacity: \(String(format: "%.2f", memoryCapacity)) Mb", + "Memory Usage: \(String(format: "%.3f", memoryUsage)) Mb", + "Disk Capacity: \(String(format: "%.2f", diskCapacity)) Mb", + "Disk Usage: \(String(format: "%.3f", diskUsage)) Mb" + ) + } + #endif +} + +// MARK: - SERVER ENVIRONMENT +public extension NKConfiguration { + + /// Updates the current environment. + /// - Parameter newEnvironment: New Server Environment to be set. + static func updateEnvironment(_ newEnvironment: Environment) { + Environment.current = newEnvironment + UserDefaults.standard.set(newEnvironment.value, forKey: "api_environment") + } + + /// Returns the current environment. + static var currentEnvironment: Environment? { + return Environment.current + } +} diff --git a/Sources/NetworkKit/Networking/ImageSession/ImageSession+ImageType.swift b/Sources/NetworkKit/Networking/ImageSession/ImageSession+ImageType.swift new file mode 100644 index 0000000..4ce2d89 --- /dev/null +++ b/Sources/NetworkKit/Networking/ImageSession/ImageSession+ImageType.swift @@ -0,0 +1,30 @@ +// +// ImageSession+AppKit.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +#if canImport(AppKit) +import AppKit.NSImage + +public extension NKImageSession { + typealias ImageType = NSImage +} + +#elseif canImport(WatchKit) +import UIKit.UIImage + +public extension NKImageSession { + typealias ImageType = UIImage +} + +#elseif canImport(UIKit) +import UIKit.UIImage + +public extension NKImageSession { + typealias ImageType = UIImage +} + +#endif diff --git a/Sources/NetworkKit/Networking/ImageSession/ImageSession.swift b/Sources/NetworkKit/Networking/ImageSession/ImageSession.swift new file mode 100644 index 0000000..f039708 --- /dev/null +++ b/Sources/NetworkKit/Networking/ImageSession/ImageSession.swift @@ -0,0 +1,95 @@ +// +// NKImageSession.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public final class NKImageSession: NKConfiguration { + + private typealias ImageValidationResult = (response: HTTPURLResponse, data: Data, image: ImageType) + + public static let shared = NKImageSession() + + public init(useCache: Bool = true, cacheDiskPath: String? = "cachedImages") { + let requestCachePolicy: NSURLRequest.CachePolicy = useCache ? .returnCacheDataElseLoad : .useProtocolCachePolicy + super.init(useDefaultCache: useCache, requestCachePolicy: requestCachePolicy, cacheDiskPath: cacheDiskPath) + emptyCacheOnAppTerminate = false + } +} + +public extension NKImageSession { + + /// Creates a task that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion. + /// - Parameter url: URL from where image has to be fetched. + /// - Parameter useCache: Flag which allows Response and Data to be cached. + /// - Parameter completion: The completion handler to call when the load request is complete. This handler is executed on the main queue. This completion handler takes the Result as parameter. On Success, it returns the image. On Failure, returns URLError. + /// - Returns: **URLSessionDataTask** for further operations. + @discardableResult + func fetch(from url: URL, cacheImage useCache: Bool = true, completion: @escaping (Result) -> ()) -> URLSessionDataTask { + + let requestCachePolicy: NSURLRequest.CachePolicy = useCache ? .returnCacheDataElseLoad : .reloadIgnoringLocalCacheData + let request = URLRequest(url: url, cachePolicy: requestCachePolicy, timeoutInterval: session.configuration.timeoutIntervalForRequest) + + let task = session.dataTask(with: request) { [weak self] (data, response, error) in + + guard let `self` = self else { + return + } + + if let error = error as NSError? { + self.logger.log(error: error) + DispatchQueue.main.async { + completion(.failure(error)) + } + return + } + + let result = self.validateImageResponse(url: url, response: response, data: data) + + switch result { + case .success(let value): + DispatchQueue.main.async { + completion(.success(value.image)) + } + + case .failure(let error): + self.logger.log(error: error) + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + + task.resume() + return task + } +} + +private extension NKImageSession { + + /// Validates URL Request's HTTP Response. + /// - Parameter response: HTTP URL Response for the provided request. + /// - Parameter data: Response Data containing Image Data sent by server. + /// - Returns: Result Containing Image on success or URL Error if validation fails. + private func validateImageResponse(url: URL, response: URLResponse?, data: Data?) -> Result { + + guard let httpURLResponse = response as? HTTPURLResponse, acceptableStatusCodes.contains(httpURLResponse.statusCode), + let mimeType = httpURLResponse.mimeType, mimeType.hasPrefix("image") else { + return .failure(.badServerResponse(for: url)) + } + + guard var data = data, !data.isEmpty else { + return .failure(.zeroByteResource(for: url)) + } + + guard let image = ImageType.initialize(using: &data) else { + return .failure(.cannotDecodeRawData(for: url)) + } + + return .success((httpURLResponse, data, image)) + } +} diff --git a/Sources/NetworkKit/Networking/Session.swift b/Sources/NetworkKit/Networking/Session.swift new file mode 100644 index 0000000..1136c22 --- /dev/null +++ b/Sources/NetworkKit/Networking/Session.swift @@ -0,0 +1,82 @@ +// +// NKSession.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation +import PublisherKit + +extension URLSession.NKDataTaskPublisher { + + public func validate() -> NKPublishers.Validate { + NKPublishers.Validate(upstream: self, shouldCheckForErrorModel: true, acceptableStatusCodes: NKSession.shared.acceptableStatusCodes) + } +} + +public typealias NKTask = URLSession.NKDataTaskPublisher +public typealias NKAnyCancellable = PublisherKit.NKAnyCancellable +public typealias NKPublishers = PublisherKit.NKPublishers + +final public class NKSession: NKConfiguration { + + public static let shared = NKSession() + + private let queue = DispatchQueue(label: "com.networkkit.task-thread", qos: .utility, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) + + private init() { + super.init(configuration: NKConfiguration.defaultConfiguration) + + #if DEBUG + if let environmentValue = UserDefaults.standard.value(forKey: "api_environment") as? String, !environmentValue.isEmpty { + Environment.current = Environment(value: environmentValue) + } else { + Environment.current = .none + } + #else + Environment.current = .none + #endif + } + + /// Returns a publisher that wraps a URL session data task for a given Network request. + /// + /// The publisher publishes data when the task completes, or terminates if the task fails with an error. + /// - Parameter builder: The block which returns a `NetworkRequest` to create a URL session data task. + /// - Parameter apiName: API Name for debug console logging. + /// - Returns: A publisher that wraps a data task for the URL request. + public func dataTask(file: StaticString = #file, line: UInt = #line, function: StaticString = #function, _ builder: () -> NKRequest) -> URLSession.NKDataTaskPublisher { + let creator = builder() + let urlRequest: URLRequest? = queue.sync { + creator.create() + return creator.request + } + + guard let request = urlRequest else { + NKLogger.default.preconditionFailure("Invalid Request Created", file: file, line: line, function: function) + } + + return session.nkTaskPublisher(for: request, apiName: creator.apiName) + } + + /// Returns a publisher that wraps a URL session data task for a given URL request. + /// + /// The publisher publishes data when the task completes, or terminates if the task fails with an error. + /// - Parameter request: The URL request for which to create a data task. + /// - Parameter apiName: API Name for debug console logging. + /// - Returns: A publisher that wraps a data task for the URL request. + public func dataTask(for request: URLRequest, apiName: String? = nil) -> URLSession.NKDataTaskPublisher { + session.nkTaskPublisher(for: request) + } + + /// Returns a publisher that wraps a URL session data task for a given URL. + /// + /// The publisher publishes data when the task completes, or terminates if the task fails with an error. + /// - Parameter url: The URL for which to create a data task. + /// - Parameter apiName: API Name for debug console logging. + /// - Returns: A publisher that wraps a data task for the URL. + public func dataTask(for url: URL, apiName: String? = nil) -> URLSession.NKDataTaskPublisher { + session.nkTaskPublisher(for: url) + } +} diff --git a/Sources/NetworkKit/Protocols/ImageSessionDelegate.swift b/Sources/NetworkKit/Protocols/ImageSessionDelegate.swift new file mode 100644 index 0000000..639df1e --- /dev/null +++ b/Sources/NetworkKit/Protocols/ImageSessionDelegate.swift @@ -0,0 +1,105 @@ +// +// NKImageSessionDelegate.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// +import Foundation + +public protocol NKImageSessionDelegate: class { + + typealias ImageType = NKImageSession.ImageType + + var image: ImageType? { get set } + + @discardableResult + func fetch(from url: URL, setImageAutomatically flag: Bool, placeholder: ImageType?, completion: ((ImageType?) -> ())?) -> URLSessionDataTask + + @discardableResult + func fetch(fromUrlString urlString: String?, setImageAutomatically flag: Bool, placeholder: ImageType?, completion: ((ImageType?) -> ())?) -> URLSessionDataTask? + + func prepareForReuse(_ placeholder: ImageType?) +} + +public extension NKImageSessionDelegate { + + + /// Fetches Image from provided URL String and sets it on this UIImageView. + /// - Parameter urlString: URL String from where image has to be fetched. + /// - Parameter flag: Bool to set image automatically after downloading. Default value is `true`. + /// - Parameter placeholder: Place holder image to be displayed until image is downloaded. Default value is `nil`. + /// - Parameter completion: Completion Block which provides image if downloaded successfully. + /// - Returns: URLSessionDataTask if URL is correctly formed else returns `nil`. + @discardableResult + func fetch(from url: URL, setImageAutomatically flag: Bool, placeholder: ImageType?, completion: ((ImageType?) -> ())?) -> URLSessionDataTask { + _fetch(from: url, setImageAutomatically: flag, placeholder: placeholder, completion: completion) + } + + + /// Fetches Image from provided URL and sets it on this UIImageView. + /// - Parameter url: URL from where image has to be fetched. + /// - Parameter flag: Bool to set image automatically after downloading. Default value is `true`. + /// - Parameter placeholder: Place holder image to be displayed until image is downloaded. Default value is `nil`. + /// - Parameter completion: Completion Block which provides image if downloaded successfully. + /// - Returns: URLSessionDataTask. + @discardableResult + func fetch(fromUrlString urlString: String?, setImageAutomatically flag: Bool, placeholder: ImageType?, completion: ((ImageType?) -> ())?) -> URLSessionDataTask? { + _fetch(fromUrlString: urlString, setImageAutomatically: flag, placeholder: placeholder, completion: completion) + } +} + + +private extension NKImageSessionDelegate { + + @inline(__always) + func _fetch(fromUrlString urlString: String?, + setImageAutomatically flag: Bool = true, placeholder: ImageType? = nil, + completion: ((ImageType?) -> ())? = nil) -> URLSessionDataTask? { + + if let placeholder = placeholder { + image = placeholder + } + + guard let urlStringValue = urlString, let url = URL(string: urlStringValue) else { + #if DEBUG + NKImageSession.shared.logger.log(error: NSError.badURL(for: urlString)) + #endif + + if flag { + image = nil + } + + completion?(nil) + return nil + } + + return _fetch(from: url, setImageAutomatically: flag, completion: completion) + } + + @inline(__always) + func _fetch(from url: URL, + setImageAutomatically flag: Bool = true, placeholder: ImageType? = nil, + completion: ((ImageType?) -> ())? = nil) -> URLSessionDataTask { + + if let placeholder = placeholder { + image = placeholder + } + + return NKImageSession.shared.fetch(from: url) { [weak self] (result) in + switch result { + case .success(let newImage): + if flag { + self?.image = newImage + } + completion?(newImage) + + case .failure: + if flag { + self?.image = nil + } + completion?(nil) + } + } + } +} diff --git a/Sources/NetworkKit/Protocols/Server/API Representable.swift b/Sources/NetworkKit/Protocols/Server/API Representable.swift new file mode 100644 index 0000000..3d0a7ae --- /dev/null +++ b/Sources/NetworkKit/Protocols/Server/API Representable.swift @@ -0,0 +1,43 @@ +// +// API Representable.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +/** + A type that represents server api. It can also be used for managing server environment in URL. + + ``` + let url = "https://api-staging.example.com/v1/users/all" + // `api` is a Server API. + // `staging` is Server Environment. + ``` + */ +public protocol APIRepresentable { + + /** + Sub URL for API. + + It may include server environment for the api. + Use **Environment.current** to maintain environment. + ``` + let url = "https://api-staging.example.com/users/all" + // `api-staging` is sub url. + ``` + */ + var subUrl: String { get } + + /** + EndPoint for API. + + ``` + let url = "https://api-staging.example.com/v1/users/all" + // `/v1` is api endpoint. + ``` + */ + var endPoint: String { get } +} diff --git a/Sources/NetworkKit/Protocols/Server/Environment.swift b/Sources/NetworkKit/Protocols/Server/Environment.swift new file mode 100644 index 0000000..4bd4f0c --- /dev/null +++ b/Sources/NetworkKit/Protocols/Server/Environment.swift @@ -0,0 +1,39 @@ +// +// Environment.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +/** + Server Environment. + + ``` + let url = "https://api-staging.example.com/v1/users/all" + // `staging` is Server Environment. + ``` + + It has a `current` property for maintaining the server environment. + + To update the `current` environment, use `NKConfiguration.updateEnvironment(:_)`. + + In `DEBUG` mode, it persists the `current` value in `UserDefaults`. + */ +public struct Environment: Hashable, Equatable { + + /// String value of the environment + public let value: String + + public init(value: String) { + self.value = value + } + + public internal(set) static var current: Environment = .none + + public static let none = Environment(value: "") + public static let staging = Environment(value: "staging") + public static let dev = Environment(value: "dev") +} diff --git a/Sources/NetworkKit/Protocols/Server/Host Representable.swift b/Sources/NetworkKit/Protocols/Server/Host Representable.swift new file mode 100644 index 0000000..8c50c9a --- /dev/null +++ b/Sources/NetworkKit/Protocols/Server/Host Representable.swift @@ -0,0 +1,32 @@ +// +// Host Representable.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +/** +A type that represents a URL host. + +``` +let url = "https://api.example.com/users/all" +// `example.com` is a host. +``` + +*/ +public protocol HostRepresentable { + + var host: String { get } + + /// Default Headers attached to every request with this host. + var defaultHeaders: HTTPHeaderParameters { get } + + /// Default URL Query for particular host. + var defaultUrlQuery: URLQuery? { get } + + /// Default API Type for particular host. + var defaultAPIType: APIRepresentable? { get } +} diff --git a/Sources/NetworkKit/Request/Request.swift b/Sources/NetworkKit/Request/Request.swift new file mode 100644 index 0000000..b22d261 --- /dev/null +++ b/Sources/NetworkKit/Request/Request.swift @@ -0,0 +1,82 @@ +// +// NetworkRequest.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public typealias NetworkRequest = NKRequest + +public class NKRequest { + + var bodyData: Data? = nil + var headers: HTTPHeaderParameters = [:] + + var queryItems = Set() + + private var _request: URLRequest? = nil + + public var request: URLRequest? { + _request + } + + public let apiName: String + + public let connection: ConnectionRepresentable + + public init(to endPoint: ConnectionRepresentable) { + connection = endPoint + apiName = endPoint.name ?? "Nil" + } + + public func create() { + let creator = CreateRequest(with: connection, query: queryItems, body: bodyData, headers: headers) + _request = creator?.request + } + + public func urlQuery(_ query: URLQuery) -> Self { + queryItems.addURLQuery(query: query) + return self + } + + public func requestBody(_ body: T, strategy: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys) -> Self { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = strategy + encoder.outputFormatting = .prettyPrinted + + do { + let bodyData = try encoder.encode(body) + self.bodyData = bodyData + } catch { + bodyData = nil + } + + headers = headers.merging(HTTPBodyEncodingType.json.headers) { (_, new) in new } + + return self + } + + public func requestBody(_ body: [String: Any], encoding: HTTPBodyEncodingType) -> Self { + bodyData = encoding.encode(body: body) + + headers = headers.merging(encoding.headers) { (_, new) in new } + + return self + } + + public func requestBody(_ body: Data?, httpHeaders: HTTPHeaderParameters) -> Self { + bodyData = body + headers = headers.merging(httpHeaders) { (_, new) in new } + + return self + } + + public func httpHeaders(_ httpHeaders: HTTPHeaderParameters) -> Self { + headers = headers.merging(httpHeaders) { (_, new) in new } + + return self + } +} diff --git a/Sources/NetworkKit/Resources/HTTP Body Encoding.swift b/Sources/NetworkKit/Resources/HTTP Body Encoding.swift new file mode 100644 index 0000000..112d154 --- /dev/null +++ b/Sources/NetworkKit/Resources/HTTP Body Encoding.swift @@ -0,0 +1,165 @@ +// +// HTTP Body Encoding.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public enum HTTPBodyEncodingType { + case formURLEncoded + case json + + var headers: HTTPHeaderParameters { + switch self { + case .formURLEncoded: + return ["Content-Type": "application/x-www-form-urlencoded"] + case .json: + return ["Content-Type": "application/json"] + } + } + + func encode(body: [String: Any]) -> Data? { + switch self { + case .formURLEncoded: + if let bodyData = query(body).data(using: .utf8, allowLossyConversion: false) { + + #if DEBUG + print(""" + Request Body: + \(String(data: bodyData, encoding: .utf8) ?? "nil") + --------------------------------------------- + + """) + #endif + + return bodyData + + } else { + #if DEBUG + print(""" + Request Body: nil + Error Encoding: - Unknown + --------------------------------------------- + + """) + #endif + + return nil + } + + + case .json: + do { + let bodyData = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + + #if DEBUG + print(""" + Request Body: + \(String(data: bodyData, encoding: .utf8) ?? "nil") + --------------------------------------------- + + """) + #endif + + return bodyData + + } catch { + #if DEBUG + print(""" + Request Body: nil + Error Encoding: - + \(error) + --------------------------------------------- + + """) + #endif + + return nil + } + } + } +} + +private extension HTTPBodyEncodingType { + + func query(_ parameters: [String: Any]) -> String { + var components: [(String, String)] = [] + + for key in parameters.keys.sorted(by: <) { + let value = parameters[key]! + components += queryComponents(fromKey: key, value: value) + } + return components.map { "\($0)=\($1)" }.joined(separator: "&") + } + + func queryComponents(fromKey key: String, + value: Any, arrayEncoding: ArrayEncoding = .brackets, + boolEncoding: BoolEncoding = .numeric) -> [(String, String)] { + + var components: [(String, String)] = [] + + if let dictionary = value as? [String: Any] { + for (nestedKey, value) in dictionary { + components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) + } + } else if let array = value as? [Any] { + for value in array { + components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value) + } + } else if let value = value as? NSNumber { + if value.boolValue { + components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue)))) + } else { + components.append((escape(key), escape("\(value)"))) + } + } else if let bool = value as? Bool { + components.append((escape(key), escape(boolEncoding.encode(value: bool)))) + } else { + components.append((escape(key), escape("\(value)"))) + } + + return components + } + + func escape(_ string: String) -> String { + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + + var allowedCharacterSet = CharacterSet.urlQueryAllowed + allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + + return string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? "" + } +} + + +enum BoolEncoding { + case numeric + case literal + + func encode(value: Bool) -> String { + switch self { + case .numeric: + return value ? "1" : "0" + case .literal: + return value ? "true" : "false" + } + } +} + +enum ArrayEncoding { + case brackets + case noBrackets + + func encode(key: String) -> String { + switch self { + case .brackets: + return "\(key)[]" + case .noBrackets: + return key + } + } +} diff --git a/Sources/NetworkKit/Resources/HTTP Method.swift b/Sources/NetworkKit/Resources/HTTP Method.swift new file mode 100644 index 0000000..c169139 --- /dev/null +++ b/Sources/NetworkKit/Resources/HTTP Method.swift @@ -0,0 +1,21 @@ +// +// HTTP Method.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public enum HTTPMethod: String { + case connect = "CONNECT" + case delete = "DELETE" + case get = "GET" + case head = "HEAD" + case options = "OPTIONS" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case trace = "TRACE" +} diff --git a/Sources/NetworkKit/Resources/Scheme.swift b/Sources/NetworkKit/Resources/Scheme.swift new file mode 100644 index 0000000..1bed048 --- /dev/null +++ b/Sources/NetworkKit/Resources/Scheme.swift @@ -0,0 +1,14 @@ +// +// Scheme.swift +// NetworkKit +// +// Created by Raghav Ahuja on 15/10/19. +// Copyright © 2019 Raghav Ahuja. All rights reserved. +// + +import Foundation + +public enum Scheme: String { + case http + case https +} diff --git a/Tests/NetworkKitTests/NetworkKitTests.swift b/Tests/NetworkKitTests/NetworkKitTests.swift index 2b91138..2e6af01 100644 --- a/Tests/NetworkKitTests/NetworkKitTests.swift +++ b/Tests/NetworkKitTests/NetworkKitTests.swift @@ -1,15 +1,154 @@ import XCTest @testable import NetworkKit +enum APIType: String, APIRepresentable { + case v1 = "5da1e9ae76c28f0014bbe25f" + + var subUrl: String { + rawValue + } + + var endPoint: String { + switch self { + case .v1: return "" + } + } +} + +enum Host: String, HostRepresentable { + + case server = "mockapi.io" + + var host: String { rawValue } + + var defaultHeaders: HTTPHeaderParameters { [:] } + + var defaultAPIType: APIRepresentable? { APIType.v1 } + + var defaultUrlQuery: URLQuery? { nil } +} + +extension Environment { + static let production = Environment(value: "") +} + +enum MockPoint: ConnectionRepresentable { + + case allUsers + + var path: String { "/users" } + + var name: String? { "Users" } + + var method: HTTPMethod { .get } + + var httpHeaders: HTTPHeaderParameters { [:] } + + var host: HostRepresentable { Host.server } + + var defaultQuery: URLQuery? { nil } + +} + +enum MockPointError: ConnectionRepresentable { + + case allUsers + + var path: String { "/users1" } + + var name: String? { "Users" } + + var method: HTTPMethod { .get } + + var httpHeaders: HTTPHeaderParameters { [:] } + + var host: HostRepresentable { Host.server } + + var defaultQuery: URLQuery? { nil } + +} + +struct User: Codable, Equatable { + let id, createdAt, name: String? + let avatar: String? +} + +typealias Users = [User] + final class NetworkKitTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(NetworkKit().text, "Hello, World!") + + +// var cancel: AnyCancellable? +// +// @available(OSX 10.15, *) +// func testURLSession() { +// cancel = URLSession.shared.dataTaskPublisher(for: URL(string: "")!) +// .catch { (error) -> URLSession.DataTaskPublisher in +// print(error == URLError.network) +// return URLSession.shared.dataTaskPublisher(for: URL(string: "11")!) +// } +// .map(\.data) +// .decode(type: Users.self, decoder: JSONDecoder()) +// .sink(receiveCompletion: { (error) in +// +// }, receiveValue: { (users) in +// print(users) +// }) +// } +// +// var cancellable: NetworkCancellable? +// +// func testExampleOld() { +// URLSession.shared.dataTask(with: URL(string: "high quality")!) { (data, response, error) in +// if let error = error { +// URLSession.shared.dataTask(with: URL(string: "low quality")!) { (data, response, error) in +// if let error = error { +// // fail +// } else if let data = data { +// // completion(UIImage(data: data) +// } +// } +// +// } else if let data = data { +// // completion(UIImage(data: data) +// } +// } +// .resume() +// } + + + var users: Users = [] { + willSet { + print("Setting value: \(newValue)") + expecatation.fulfill() + } } + + var cancellable: NKAnyCancellable? + let expecatation = XCTestExpectation() + + func testExample() { + cancellable = NKSession.shared.dataTask { + NetworkRequest(to: MockPoint.allUsers) + } + .map(\.data) + .decode(type: Users.self, decoder: JSONDecoder()) + .replaceError(with: [User(id: "12", createdAt: "Today", name: "Guest User", avatar: nil)]) + .assign(to: \.users, on: self) + + cancellable?.cancel() + + wait(for: [expecatation], timeout: 60) + } + + func testPerformanceExample() { + measure { + testExample() + } + } + static var allTests = [ - ("testExample", testExample), + ("testExample", testExample, "testPerformanceExample", testPerformanceExample), ] }