diff --git a/Package.resolved b/Package.resolved
index 3dfc4c9c..94b92e20 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -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",
diff --git a/Package.swift b/Package.swift
index cc2dca77..f4763d53 100644
--- a/Package.swift
+++ b/Package.swift
@@ -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
diff --git a/TINetworking/Sources/Mapping/BodyContent/ApplicationJsonBodyContent.swift b/TINetworking/Sources/Mapping/BodyContent/ApplicationJsonBodyContent.swift
new file mode 100644
index 00000000..316b3113
--- /dev/null
+++ b/TINetworking/Sources/Mapping/BodyContent/ApplicationJsonBodyContent.swift
@@ -0,0 +1,25 @@
+import Foundation
+
+open class ApplicationJsonBodyContent
: 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()
+ }
+}
diff --git a/TINetworking/Sources/Mapping/BodyContent/BodyContent.swift b/TINetworking/Sources/Mapping/BodyContent/BodyContent.swift
new file mode 100644
index 00000000..58093c6c
--- /dev/null
+++ b/TINetworking/Sources/Mapping/BodyContent/BodyContent.swift
@@ -0,0 +1,5 @@
+import Foundation
+
+public protocol BodyContent: Content {
+ func encodeBody() throws -> Data
+}
diff --git a/TINetworking/Sources/Mapping/BodyContent/EmptyBodyContent.swift b/TINetworking/Sources/Mapping/BodyContent/EmptyBodyContent.swift
new file mode 100644
index 00000000..12d1579f
--- /dev/null
+++ b/TINetworking/Sources/Mapping/BodyContent/EmptyBodyContent.swift
@@ -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()
+ }
+}
diff --git a/TINetworking/Sources/Mapping/Codable+Required.swift b/TINetworking/Sources/Mapping/Codable+Required.swift
new file mode 100644
index 00000000..8cceb3b6
--- /dev/null
+++ b/TINetworking/Sources/Mapping/Codable+Required.swift
@@ -0,0 +1,21 @@
+public extension KeyedDecodingContainer {
+ func decode(_ 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(_ value: T?,
+ forKey key: Key,
+ required: Bool) throws {
+
+ if let value = value {
+ try encode(value, forKey: key)
+ } else if required {
+ try encodeNil(forKey: key)
+ }
+ }
+}
diff --git a/TINetworking/Sources/Mapping/Content.swift b/TINetworking/Sources/Mapping/Content.swift
new file mode 100644
index 00000000..41b26a9b
--- /dev/null
+++ b/TINetworking/Sources/Mapping/Content.swift
@@ -0,0 +1,3 @@
+public protocol Content {
+ var mediaTypeName: String { get }
+}
diff --git a/TINetworking/Sources/Mapping/MediaType.swift b/TINetworking/Sources/Mapping/MediaType.swift
new file mode 100644
index 00000000..387a36f1
--- /dev/null
+++ b/TINetworking/Sources/Mapping/MediaType.swift
@@ -0,0 +1,4 @@
+public enum MediaType: String {
+ case applicationJson = "application/json"
+ case textPlain = "text/plain"
+}
diff --git a/TINetworking/Sources/Mapping/OneOfMapping/AnyTypeMapping.swift b/TINetworking/Sources/Mapping/OneOfMapping/AnyTypeMapping.swift
new file mode 100644
index 00000000..b1b97cb2
--- /dev/null
+++ b/TINetworking/Sources/Mapping/OneOfMapping/AnyTypeMapping.swift
@@ -0,0 +1,22 @@
+public struct AnyTypeMapping {
+ public let type: Any.Type
+ private let mappingClosure: () -> Result
+
+ public init(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 {
+ mappingClosure()
+ }
+}
diff --git a/TINetworking/Sources/Mapping/OneOfMapping/OneOfMappingError.swift b/TINetworking/Sources/Mapping/OneOfMapping/OneOfMappingError.swift
new file mode 100644
index 00000000..c7c025b8
--- /dev/null
+++ b/TINetworking/Sources/Mapping/OneOfMapping/OneOfMappingError.swift
@@ -0,0 +1,21 @@
+public struct OneOfMappingError: Error, CustomDebugStringConvertible {
+ public typealias MappingFailures = [KeyValueTuple]
+
+ 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
+ }
+}
diff --git a/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift
new file mode 100644
index 00000000..498ea08b
--- /dev/null
+++ b/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+open class ApplicationJsonResponseContent: 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)
+ }
+}
diff --git a/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift
new file mode 100644
index 00000000..ebcca1c0
--- /dev/null
+++ b/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift
@@ -0,0 +1,18 @@
+import Foundation
+
+public struct MapResponseContent: ResponseContent {
+ public let mediaTypeName: String
+
+ private let decodeClosure: (Data) throws -> Model
+
+ public init(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)
+ }
+}
diff --git a/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift
new file mode 100644
index 00000000..3d600f79
--- /dev/null
+++ b/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+public protocol ResponseContent: Content {
+ associatedtype Model
+
+ func decodeResponse(data: Data) throws -> Model
+}
diff --git a/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift
new file mode 100644
index 00000000..ebfa6c73
--- /dev/null
+++ b/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift
@@ -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
+ }
+}
diff --git a/TINetworking/Sources/Parameters/Encoding/BaseUrlParameterEncoding.swift b/TINetworking/Sources/Parameters/Encoding/BaseUrlParameterEncoding.swift
new file mode 100644
index 00000000..85b1c119
--- /dev/null
+++ b/TINetworking/Sources/Parameters/Encoding/BaseUrlParameterEncoding.swift
@@ -0,0 +1,30 @@
+import Alamofire
+
+open class BaseUrlParameterEncoding {
+ private let encoding: URLEncoding = .queryString
+
+ public init() {
+ }
+
+ open func encode(parameters: [String: Parameter]) -> [KeyValueTuple] {
+ var filteredComponents: [(String, String)] = []
+
+ for key in parameters.keys.sorted(by: <) {
+ guard let parameter = parameters[key] else {
+ continue
+ }
+
+ let components: [KeyValueTuple] = encoding.queryComponents(fromKey: key, value: parameter.value)
+
+ for component in components {
+ if component.value.isEmpty && !parameter.allowEmptyValue {
+ continue
+ }
+
+ filteredComponents.append(component)
+ }
+ }
+
+ return filteredComponents
+ }
+}
diff --git a/TINetworking/Sources/Parameters/Encoding/HeadersParameterEncoding.swift b/TINetworking/Sources/Parameters/Encoding/HeadersParameterEncoding.swift
new file mode 100644
index 00000000..7c4f058a
--- /dev/null
+++ b/TINetworking/Sources/Parameters/Encoding/HeadersParameterEncoding.swift
@@ -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]) -> [String: String] {
+ Dictionary(encode(parameters: parameters)) {
+ $0 + sequenceSeparator + $1
+ }
+ }
+}
diff --git a/TINetworking/Sources/Parameters/Encoding/PathParameterEncoding.swift b/TINetworking/Sources/Parameters/Encoding/PathParameterEncoding.swift
new file mode 100644
index 00000000..f9501c5f
--- /dev/null
+++ b/TINetworking/Sources/Parameters/Encoding/PathParameterEncoding.swift
@@ -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]) -> String {
+ .render(template: templateUrl, using: encode(parameters: parameters))
+ }
+}
diff --git a/TINetworking/Sources/Parameters/Encoding/QueryStringParameterEncoding.swift b/TINetworking/Sources/Parameters/Encoding/QueryStringParameterEncoding.swift
new file mode 100644
index 00000000..bef188bb
--- /dev/null
+++ b/TINetworking/Sources/Parameters/Encoding/QueryStringParameterEncoding.swift
@@ -0,0 +1,9 @@
+open class QueryStringParameterEncoding: BaseUrlParameterEncoding, ParameterEncoding {
+ open func encode(parameters: [String: Parameter]) -> [String: Any] {
+ let includedKeys = Set(super.encode(parameters: parameters).map { $0.key })
+
+ return parameters
+ .filter { includedKeys.contains($0.key) }
+ .mapValues { $0.value }
+ }
+}
diff --git a/TINetworking/Sources/Parameters/Parameter.swift b/TINetworking/Sources/Parameters/Parameter.swift
new file mode 100644
index 00000000..5158c7dd
--- /dev/null
+++ b/TINetworking/Sources/Parameters/Parameter.swift
@@ -0,0 +1,9 @@
+public struct Parameter {
+ public let value: Any
+ public let allowEmptyValue: Bool
+
+ public init(value: Any, allowEmptyValue: Bool = false) {
+ self.value = value
+ self.allowEmptyValue = allowEmptyValue
+ }
+}
diff --git a/TINetworking/Sources/Parameters/ParameterEncoding.swift b/TINetworking/Sources/Parameters/ParameterEncoding.swift
new file mode 100644
index 00000000..ff1b362c
--- /dev/null
+++ b/TINetworking/Sources/Parameters/ParameterEncoding.swift
@@ -0,0 +1,6 @@
+protocol ParameterEncoding {
+ associatedtype Location: ParameterLocation
+ associatedtype Result
+
+ func encode(parameters: [String: Parameter]) -> Result
+}
diff --git a/TINetworking/Sources/Parameters/ParameterLocation.swift b/TINetworking/Sources/Parameters/ParameterLocation.swift
new file mode 100644
index 00000000..92e0d943
--- /dev/null
+++ b/TINetworking/Sources/Parameters/ParameterLocation.swift
@@ -0,0 +1,6 @@
+public protocol ParameterLocation {}
+
+public struct LocationQuery: ParameterLocation {}
+public struct LocationPath: ParameterLocation {}
+public struct LocationHeader: ParameterLocation {}
+public struct LocationCookie: ParameterLocation {}
diff --git a/TINetworking/Sources/Request/Request.swift b/TINetworking/Sources/Request/Request.swift
new file mode 100644
index 00000000..3505e96e
--- /dev/null
+++ b/TINetworking/Sources/Request/Request.swift
@@ -0,0 +1,42 @@
+import Alamofire
+
+public struct Request {
+
+ public var templatePath: String
+ public var method: HTTPMethod
+ public var requestBodyContent: Content
+ public var queryParameters: [String: Parameter]
+ public var pathParameters: [String: Parameter]
+ public var headerParameters: [String: Parameter]
+ public var cookieParameters: [String: Parameter]
+ public var acceptableStatusCodes: Set
+ public var serverOverride: Server?
+ public var serverVariablesOverride: [KeyValueTuple]
+
+ public var path: String {
+ PathParameterEncoding(templateUrl: templatePath).encode(parameters: pathParameters)
+ }
+
+ public init(templatePath: String,
+ method: HTTPMethod,
+ requestBodyContent: Content,
+ queryParameters: [String: Parameter] = [:],
+ pathParameters: [String: Parameter] = [:],
+ headerParameters: [String: Parameter] = [:],
+ cookieParameters: [String: Parameter] = [:],
+ acceptableStatusCodes: Set = [200],
+ serverOverride: Server? = nil,
+ serverVariablesOverride: [KeyValueTuple] = []) {
+
+ 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
+ }
+}
diff --git a/TINetworking/Sources/Response/MimeTypeUnsupportedError.swift b/TINetworking/Sources/Response/MimeTypeUnsupportedError.swift
new file mode 100644
index 00000000..8a885b58
--- /dev/null
+++ b/TINetworking/Sources/Response/MimeTypeUnsupportedError.swift
@@ -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."
+ }
+}
diff --git a/TINetworking/Sources/Response/ResponseType+Decoding.swift b/TINetworking/Sources/Response/ResponseType+Decoding.swift
new file mode 100644
index 00000000..e1f475a5
--- /dev/null
+++ b/TINetworking/Sources/Response/ResponseType+Decoding.swift
@@ -0,0 +1,45 @@
+public struct ResponseTypeDecodingError: Error {
+ public let statusCode: Int
+ public let contentType: String
+}
+
+public struct ContentMapping {
+ 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(transform: @escaping (Content.Model) -> NewModel) -> ContentMapping> {
+ .init(statusCode: statusCode,
+ mimeType: mimeType,
+ responseContent: MapResponseContent(responseContent: responseContent,
+ transform: transform))
+ }
+}
+
+public extension ResponseType {
+ func decode(contentMapping: [ContentMapping]) -> Result {
+ 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))
+ }
+}
diff --git a/TINetworking/Sources/Response/ResponseType.swift b/TINetworking/Sources/Response/ResponseType.swift
new file mode 100644
index 00000000..0e45359c
--- /dev/null
+++ b/TINetworking/Sources/Response/ResponseType.swift
@@ -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
+}
diff --git a/TINetworking/Sources/Response/StatusCodeMimeTypePairUnsupportedError.swift b/TINetworking/Sources/Response/StatusCodeMimeTypePairUnsupportedError.swift
new file mode 100644
index 00000000..909c20a9
--- /dev/null
+++ b/TINetworking/Sources/Response/StatusCodeMimeTypePairUnsupportedError.swift
@@ -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!"
+ }
+}
diff --git a/TINetworking/Sources/Server.swift b/TINetworking/Sources/Server.swift
new file mode 100644
index 00000000..2d3a11c3
--- /dev/null
+++ b/TINetworking/Sources/Server.swift
@@ -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] {
+ variables.map { ($0.key, $0.value.defaultValue) }
+ }
+
+ public func url(using variables: [KeyValueTuple] = [],
+ 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)
+ }
+}
diff --git a/TINetworking/Sources/SessionFactory.swift b/TINetworking/Sources/SessionFactory.swift
new file mode 100644
index 00000000..275975e4
--- /dev/null
+++ b/TINetworking/Sources/SessionFactory.swift
@@ -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
+ }
+}
diff --git a/TINetworking/Sources/String+URLExtensions.swift b/TINetworking/Sources/String+URLExtensions.swift
new file mode 100644
index 00000000..0d722e85
--- /dev/null
+++ b/TINetworking/Sources/String+URLExtensions.swift
@@ -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 {
+ variables.reduce(template) {
+ $0.replacingOccurrences(of: "{\($1.key)}", with: $1.value.urlEscaped)
+ }
+ }
+}
diff --git a/TINetworking/Sources/Typealiases.swift b/TINetworking/Sources/Typealiases.swift
new file mode 100644
index 00000000..308efd25
--- /dev/null
+++ b/TINetworking/Sources/Typealiases.swift
@@ -0,0 +1 @@
+public typealias KeyValueTuple = (key: K, value: V)