add TINetworking
This commit is contained in:
parent
67488af9c6
commit
fb0e1090e5
|
|
@ -1,6 +1,15 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Alamofire",
|
||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
||||
"version": "5.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Cursors",
|
||||
"repositoryURL": "https://github.com/petropavel13/Cursors",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ let package = Package(
|
|||
.library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]),
|
||||
.library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]),
|
||||
.library(name: "TITableKitUtils", targets: ["TITableKitUtils"]),
|
||||
.library(name: "TINetworking", targets: ["TINetworking"]),
|
||||
|
||||
// MARK: - Elements
|
||||
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
|
||||
|
|
@ -24,9 +25,10 @@ let package = Package(
|
|||
.library(name: "TIPagination", targets: ["TIPagination"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/maxsokolov/TableKit.git", from: "2.11.0"),
|
||||
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"),
|
||||
.package(url: "https://github.com/petropavel13/Cursors", from: "0.5.1")
|
||||
.package(url: "https://github.com/maxsokolov/TableKit.git", .upToNextMajor(from: "2.11.0")),
|
||||
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2")),
|
||||
.package(url: "https://github.com/petropavel13/Cursors", .upToNextMajor(from: "0.5.1")),
|
||||
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")),
|
||||
],
|
||||
targets: [
|
||||
|
||||
|
|
@ -39,6 +41,7 @@ let package = Package(
|
|||
.target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils/Sources"),
|
||||
.target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"),
|
||||
.target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"),
|
||||
.target(name: "TINetworking", dependencies: ["Alamofire"], path: "TINetworking/Sources"),
|
||||
|
||||
// MARK: - Elements
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
|
||||
open class ApplicationJsonBodyContent<Body>: BodyContent {
|
||||
public var mediaTypeName: String {
|
||||
MediaType.applicationJson.rawValue
|
||||
}
|
||||
|
||||
private let encodingClosure: () throws -> Data
|
||||
|
||||
public init(body: Body, jsonEncoder: JSONEncoder = JSONEncoder()) where Body: Encodable {
|
||||
self.encodingClosure = {
|
||||
try jsonEncoder.encode(body)
|
||||
}
|
||||
}
|
||||
|
||||
public init(jsonBody: Body, options: JSONSerialization.WritingOptions = .prettyPrinted) {
|
||||
self.encodingClosure = {
|
||||
try JSONSerialization.data(withJSONObject: jsonBody, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
public func encodeBody() throws -> Data {
|
||||
try encodingClosure()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import Foundation
|
||||
|
||||
public protocol BodyContent: Content {
|
||||
func encodeBody() throws -> Data
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Foundation
|
||||
|
||||
public struct EmptyBodyContent: BodyContent {
|
||||
public var mediaTypeName: String {
|
||||
MediaType.textPlain.rawValue
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func encodeBody() throws -> Data {
|
||||
Data()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
public extension KeyedDecodingContainer {
|
||||
func decode<T: Decodable>(_ type: T?.Type,
|
||||
forKey key: Key,
|
||||
required: Bool) throws -> T? {
|
||||
|
||||
required ? try decode(type, forKey: key) : try decodeIfPresent(T.self, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
public extension KeyedEncodingContainer {
|
||||
mutating func encode<T: Encodable>(_ value: T?,
|
||||
forKey key: Key,
|
||||
required: Bool) throws {
|
||||
|
||||
if let value = value {
|
||||
try encode(value, forKey: key)
|
||||
} else if required {
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
public protocol Content {
|
||||
var mediaTypeName: String { get }
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
public enum MediaType: String {
|
||||
case applicationJson = "application/json"
|
||||
case textPlain = "text/plain"
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
public struct AnyTypeMapping<R> {
|
||||
public let type: Any.Type
|
||||
private let mappingClosure: () -> Result<R, Error>
|
||||
|
||||
public init<T: Decodable>(decoder: Decoder,
|
||||
transform: @escaping (T) -> R) {
|
||||
|
||||
type = T.self
|
||||
|
||||
mappingClosure = {
|
||||
do {
|
||||
return .success(transform(try T(from: decoder)))
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func decode() -> Result<R, Error> {
|
||||
mappingClosure()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
public struct OneOfMappingError: Error, CustomDebugStringConvertible {
|
||||
public typealias MappingFailures = [KeyValueTuple<Any.Type, Error>]
|
||||
|
||||
public let codingPath: [CodingKey]
|
||||
public let mappingFailures: MappingFailures
|
||||
|
||||
public init(codingPath: [CodingKey], mappingFailures: MappingFailures) {
|
||||
self.codingPath = codingPath
|
||||
self.mappingFailures = mappingFailures
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
var formattedString = "OneOf mapping failed for codingPath \(codingPath)\nwith following errors:\n"
|
||||
|
||||
for (type, error) in mappingFailures {
|
||||
formattedString += "\(type) mapping failed with error: \(error)\n"
|
||||
}
|
||||
|
||||
return formattedString
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import Foundation
|
||||
|
||||
open class ApplicationJsonResponseContent<Model: Decodable>: ResponseContent {
|
||||
public var mediaTypeName: String {
|
||||
MediaType.applicationJson.rawValue
|
||||
}
|
||||
|
||||
public let jsonDecoder: JSONDecoder
|
||||
|
||||
public init(jsonDecoder: JSONDecoder = JSONDecoder()) {
|
||||
self.jsonDecoder = jsonDecoder
|
||||
}
|
||||
|
||||
public func decodeResponse(data: Data) throws -> Model {
|
||||
try jsonDecoder.decode(Model.self, from: data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import Foundation
|
||||
|
||||
public struct MapResponseContent<Model>: ResponseContent {
|
||||
public let mediaTypeName: String
|
||||
|
||||
private let decodeClosure: (Data) throws -> Model
|
||||
|
||||
public init<C: ResponseContent>(responseContent: C, transform: @escaping (C.Model) -> Model) {
|
||||
self.mediaTypeName = responseContent.mediaTypeName
|
||||
self.decodeClosure = {
|
||||
transform(try responseContent.decodeResponse(data: $0))
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeResponse(data: Data) throws -> Model {
|
||||
try decodeClosure(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public protocol ResponseContent: Content {
|
||||
associatedtype Model
|
||||
|
||||
func decodeResponse(data: Data) throws -> Model
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import Foundation
|
||||
|
||||
public struct TextPlainResponseContent: ResponseContent {
|
||||
struct UnableToDecodeStringError: Error {
|
||||
let data: Data
|
||||
let encoding: String.Encoding
|
||||
}
|
||||
|
||||
private let encoding: String.Encoding
|
||||
|
||||
public init(encoding: String.Encoding = .utf8) {
|
||||
self.encoding = encoding
|
||||
}
|
||||
|
||||
public let mediaTypeName = MediaType.textPlain.rawValue
|
||||
|
||||
public func decodeResponse(data: Data) throws -> String {
|
||||
guard let plainText = String(data: data, encoding: encoding) else {
|
||||
throw UnableToDecodeStringError(data: data, encoding: encoding)
|
||||
}
|
||||
|
||||
return plainText
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import Alamofire
|
||||
|
||||
open class BaseUrlParameterEncoding {
|
||||
private let encoding: URLEncoding = .queryString
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
open func encode<L: ParameterLocation>(parameters: [String: Parameter<L>]) -> [KeyValueTuple<String, String>] {
|
||||
var filteredComponents: [(String, String)] = []
|
||||
|
||||
for key in parameters.keys.sorted(by: <) {
|
||||
guard let parameter = parameters[key] else {
|
||||
continue
|
||||
}
|
||||
|
||||
let components: [KeyValueTuple<String, String>] = encoding.queryComponents(fromKey: key, value: parameter.value)
|
||||
|
||||
for component in components {
|
||||
if component.value.isEmpty && !parameter.allowEmptyValue {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredComponents.append(component)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredComponents
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
open class HeadersParameterEncoding: BaseUrlParameterEncoding, ParameterEncoding {
|
||||
public let sequenceSeparator: String
|
||||
|
||||
public init(sequenceSeparator: String = ";") {
|
||||
self.sequenceSeparator = sequenceSeparator
|
||||
}
|
||||
|
||||
open func encode(parameters: [String: Parameter<LocationHeader>]) -> [String: String] {
|
||||
Dictionary(encode(parameters: parameters)) {
|
||||
$0 + sequenceSeparator + $1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
open class PathParameterEncoding: BaseUrlParameterEncoding, ParameterEncoding {
|
||||
public let templateUrl: String
|
||||
|
||||
public init(templateUrl: String) {
|
||||
self.templateUrl = templateUrl
|
||||
}
|
||||
|
||||
open func encode(parameters: [String: Parameter<LocationPath>]) -> String {
|
||||
.render(template: templateUrl, using: encode(parameters: parameters))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
open class QueryStringParameterEncoding: BaseUrlParameterEncoding, ParameterEncoding {
|
||||
open func encode(parameters: [String: Parameter<LocationQuery>]) -> [String: Any] {
|
||||
let includedKeys = Set(super.encode(parameters: parameters).map { $0.key })
|
||||
|
||||
return parameters
|
||||
.filter { includedKeys.contains($0.key) }
|
||||
.mapValues { $0.value }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
public struct Parameter<Location: ParameterLocation> {
|
||||
public let value: Any
|
||||
public let allowEmptyValue: Bool
|
||||
|
||||
public init(value: Any, allowEmptyValue: Bool = false) {
|
||||
self.value = value
|
||||
self.allowEmptyValue = allowEmptyValue
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
protocol ParameterEncoding {
|
||||
associatedtype Location: ParameterLocation
|
||||
associatedtype Result
|
||||
|
||||
func encode(parameters: [String: Parameter<Location>]) -> Result
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
public protocol ParameterLocation {}
|
||||
|
||||
public struct LocationQuery: ParameterLocation {}
|
||||
public struct LocationPath: ParameterLocation {}
|
||||
public struct LocationHeader: ParameterLocation {}
|
||||
public struct LocationCookie: ParameterLocation {}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import Alamofire
|
||||
|
||||
public struct Request<Content: BodyContent> {
|
||||
|
||||
public var templatePath: String
|
||||
public var method: HTTPMethod
|
||||
public var requestBodyContent: Content
|
||||
public var queryParameters: [String: Parameter<LocationQuery>]
|
||||
public var pathParameters: [String: Parameter<LocationPath>]
|
||||
public var headerParameters: [String: Parameter<LocationHeader>]
|
||||
public var cookieParameters: [String: Parameter<LocationCookie>]
|
||||
public var acceptableStatusCodes: Set<Int>
|
||||
public var serverOverride: Server?
|
||||
public var serverVariablesOverride: [KeyValueTuple<String, String>]
|
||||
|
||||
public var path: String {
|
||||
PathParameterEncoding(templateUrl: templatePath).encode(parameters: pathParameters)
|
||||
}
|
||||
|
||||
public init(templatePath: String,
|
||||
method: HTTPMethod,
|
||||
requestBodyContent: Content,
|
||||
queryParameters: [String: Parameter<LocationQuery>] = [:],
|
||||
pathParameters: [String: Parameter<LocationPath>] = [:],
|
||||
headerParameters: [String: Parameter<LocationHeader>] = [:],
|
||||
cookieParameters: [String: Parameter<LocationCookie>] = [:],
|
||||
acceptableStatusCodes: Set<Int> = [200],
|
||||
serverOverride: Server? = nil,
|
||||
serverVariablesOverride: [KeyValueTuple<String, String>] = []) {
|
||||
|
||||
self.templatePath = templatePath
|
||||
self.method = method
|
||||
self.requestBodyContent = requestBodyContent
|
||||
self.queryParameters = queryParameters
|
||||
self.pathParameters = pathParameters
|
||||
self.headerParameters = headerParameters
|
||||
self.cookieParameters = cookieParameters
|
||||
self.acceptableStatusCodes = acceptableStatusCodes
|
||||
self.serverOverride = serverOverride
|
||||
self.serverVariablesOverride = serverVariablesOverride
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
open class MimeTypeUnsupportedError: Error, CustomDebugStringConvertible {
|
||||
|
||||
public let mimeType: String?
|
||||
|
||||
public init(mimeType: String?) {
|
||||
self.mimeType = mimeType
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"Mime type \(mimeType.debugDescription) isn't supported."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
public struct ResponseTypeDecodingError: Error {
|
||||
public let statusCode: Int
|
||||
public let contentType: String
|
||||
}
|
||||
|
||||
public struct ContentMapping<Content: ResponseContent> {
|
||||
public let statusCode: Int
|
||||
public let mimeType: String?
|
||||
public let responseContent: Content
|
||||
|
||||
public init(statusCode: Int, mimeType: String?, responseContent: Content) {
|
||||
self.statusCode = statusCode
|
||||
self.mimeType = mimeType
|
||||
self.responseContent = responseContent
|
||||
}
|
||||
|
||||
public func map<NewModel>(transform: @escaping (Content.Model) -> NewModel) -> ContentMapping<MapResponseContent<NewModel>> {
|
||||
.init(statusCode: statusCode,
|
||||
mimeType: mimeType,
|
||||
responseContent: MapResponseContent(responseContent: responseContent,
|
||||
transform: transform))
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResponseType {
|
||||
func decode<C>(contentMapping: [ContentMapping<C>]) -> Result<C.Model, ErrorType> {
|
||||
for mapping in contentMapping where mapping.statusCode == statusCode && mapping.mimeType == mimeType {
|
||||
do {
|
||||
return .success(try mapping.responseContent.decodeResponse(data: data))
|
||||
} catch {
|
||||
return .failure(objectMappingError(underlyingError: error))
|
||||
}
|
||||
}
|
||||
|
||||
guard contentMapping.contains(where: { $0.statusCode == statusCode }) else {
|
||||
return .failure(unsupportedStatusCodeError(statusCode: statusCode))
|
||||
}
|
||||
|
||||
guard contentMapping.contains(where: { $0.mimeType == mimeType }) else {
|
||||
return .failure(unsupportedMimeTypeError(mimeType: mimeType))
|
||||
}
|
||||
|
||||
return .failure(unsupportedStatusCodeMimeTypePairError(statusCode: statusCode, mimeType: mimeType))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import Foundation
|
||||
|
||||
public protocol ResponseType {
|
||||
associatedtype ErrorType: Error
|
||||
|
||||
var statusCode: Int { get }
|
||||
var data: Data { get }
|
||||
var mimeType: String? { get }
|
||||
|
||||
func unsupportedStatusCodeError(statusCode: Int) -> ErrorType
|
||||
func unsupportedMimeTypeError(mimeType: String?) -> ErrorType
|
||||
func objectMappingError(underlyingError: Error) -> ErrorType
|
||||
func unsupportedStatusCodeMimeTypePairError(statusCode: Int, mimeType: String?) -> ErrorType
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
open class StatusCodeMimeTypePairUnsupportedError: MimeTypeUnsupportedError {
|
||||
|
||||
public let statusCode: Int
|
||||
|
||||
public init(statusCode: Int, mimeType: String?) {
|
||||
self.statusCode = statusCode
|
||||
|
||||
super.init(mimeType: mimeType)
|
||||
}
|
||||
|
||||
public override var debugDescription: String {
|
||||
"Status code: \(statusCode), mimeType: \(mimeType ?? "nil") pair is unsupported!"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import Foundation
|
||||
|
||||
private enum Scheme: String {
|
||||
case https
|
||||
|
||||
var urlPrefix: String {
|
||||
rawValue + "://"
|
||||
}
|
||||
}
|
||||
|
||||
public struct Server {
|
||||
public struct Variable {
|
||||
public let values: [String]
|
||||
public let defaultValue: String
|
||||
|
||||
public init(values: [String], defaultValue: String) {
|
||||
self.values = values
|
||||
self.defaultValue = defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
public let urlTemplate: String
|
||||
public let variables: [String: Variable]
|
||||
|
||||
public init(urlTemplate: String, variables: [String: Variable]) {
|
||||
self.urlTemplate = urlTemplate
|
||||
self.variables = variables
|
||||
}
|
||||
|
||||
public init(baseUrl: String) {
|
||||
self.init(urlTemplate: baseUrl, variables: [:])
|
||||
}
|
||||
|
||||
private var defaultVariables: [KeyValueTuple<String, String>] {
|
||||
variables.map { ($0.key, $0.value.defaultValue) }
|
||||
}
|
||||
|
||||
public func url(using variables: [KeyValueTuple<String, String>] = [],
|
||||
appendHttpsSchemeIfMissing: Bool = true) -> URL? {
|
||||
|
||||
guard !variables.isEmpty else {
|
||||
return URL(string: .render(template: urlTemplate, using: defaultVariables))
|
||||
}
|
||||
|
||||
let defaultVariablesToApply = self.defaultVariables
|
||||
.filter { (key, _) in variables.contains { $0.key == key } }
|
||||
|
||||
let defaultParametersTemplate = String.render(template: urlTemplate,
|
||||
using: defaultVariablesToApply)
|
||||
|
||||
let formattedUrlString = String.render(template: defaultParametersTemplate, using: variables)
|
||||
|
||||
if appendHttpsSchemeIfMissing, !formattedUrlString.contains(Scheme.https.urlPrefix) {
|
||||
return URL(string: Scheme.https.urlPrefix + formattedUrlString)
|
||||
}
|
||||
|
||||
return URL(string: formattedUrlString)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
open class SessionFactory {
|
||||
|
||||
/// Timeout interval for requests.
|
||||
public var timeoutInterval: TimeInterval
|
||||
|
||||
/// A dictionary of additional headers to send with requests.
|
||||
public var additionalHttpHeaders: HTTPHeaders
|
||||
|
||||
/// Server trust policies.
|
||||
public var serverTrustPolicies: [String: ServerTrustEvaluating]
|
||||
|
||||
public init(timeoutInterval: TimeInterval = 20,
|
||||
additionalHttpHeaders: HTTPHeaders = HTTPHeaders(),
|
||||
trustPolicies: [String: ServerTrustEvaluating] = [:]) {
|
||||
|
||||
self.timeoutInterval = timeoutInterval
|
||||
self.additionalHttpHeaders = additionalHttpHeaders
|
||||
self.serverTrustPolicies = Dictionary(uniqueKeysWithValues: trustPolicies.map { ($0.key.urlHost, $0.value) })
|
||||
}
|
||||
|
||||
open func createSession() -> Session {
|
||||
Session(configuration: createSessionConfiguration(),
|
||||
serverTrustManager: ServerTrustManager(allHostsMustBeEvaluated: false,
|
||||
evaluators: serverTrustPolicies))
|
||||
}
|
||||
|
||||
open func createSessionConfiguration() -> URLSessionConfiguration {
|
||||
let sessionConfiguration = URLSessionConfiguration.default
|
||||
sessionConfiguration.timeoutIntervalForResource = timeoutInterval
|
||||
sessionConfiguration.httpAdditionalHeaders = additionalHttpHeaders.dictionary
|
||||
|
||||
return sessionConfiguration
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import Foundation
|
||||
|
||||
public extension String {
|
||||
var urlEscaped: String {
|
||||
return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? self
|
||||
}
|
||||
|
||||
var urlHost: String {
|
||||
URL(string: self)?.host ?? self
|
||||
}
|
||||
|
||||
static func render(template: String, using variables: [KeyValueTuple<String, String>]) -> String {
|
||||
variables.reduce(template) {
|
||||
$0.replacingOccurrences(of: "{\($1.key)}", with: $1.value.urlEscaped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
public typealias KeyValueTuple<K, V> = (key: K, value: V)
|
||||
Loading…
Reference in New Issue