Skip to content

Commit 889918a

Browse files
committed
Initial commit for Rx with tests
0 parents  commit 889918a

16 files changed

Lines changed: 747 additions & 0 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

Package.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version:5.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "ObservableNetworking",
8+
products: [
9+
// Products define the executables and libraries produced by a package, and make them visible to other packages.
10+
.library(
11+
name: "ObservableNetworking",
12+
targets: ["ObservableNetworking"]),
13+
],
14+
dependencies: [
15+
// Dependencies declare other packages that this package depends on.
16+
.package(url: "https://github.com/ReactiveX/RxSwift.git", from: "5.0.0"),
17+
],
18+
targets: [
19+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
20+
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
21+
.target(
22+
name: "ObservableNetworking",
23+
dependencies: ["RxSwift"]),
24+
.testTarget(
25+
name: "ObservableNetworkingTests",
26+
dependencies: ["ObservableNetworking"]),
27+
]
28+
)

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# ObservableNetworking
2+
3+
A simple, yet flexible, networking library that allows the use of RxSwift to return observable from network requests.
4+
5+
6+
### How to use
7+
8+
To get started, you will need to let the framework know a little about your different enviroments by defining an `enum` that conforms to `NetworkEnvironment`. This will allow the framework to build the desired network requests and URLs it needs.
9+
10+
```SWift
11+
enum Environment {
12+
case production
13+
case staging
14+
case dev
15+
}
16+
17+
extension Enviroment: NetworkEnvironment {
18+
var scheme: String {
19+
switch self {
20+
case .production:
21+
return "https"
22+
default:
23+
return "http"
24+
}
25+
}
26+
27+
var host: String {
28+
switch self {
29+
case .production:
30+
return "mycoolsite.com"
31+
case .staging:
32+
return "staging.mycoolsite.com"
33+
case .dev:
34+
return "dev.mycoolsite.com"
35+
}
36+
}
37+
38+
var path: String {
39+
return "api/v1/"
40+
}
41+
}
42+
```
43+
44+
Once the environment has been defined it is time to initialize the framework by passing in your selected environment. This framework conforms to the `ObservableNetwork` protocol so that it is easily mocked for testing. All that is then needed is to grab the `network` from the instantiation.
45+
46+
```Swift
47+
let networkingFramework = ObservableNetworking(environment: .dev)
48+
let network = networkingFramework.network
49+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// HTTPMethod.swift
3+
//
4+
//
5+
// Created by Steve Galbraith on 9/24/19.
6+
//
7+
8+
import Foundation
9+
10+
/// HTTP methods as defined at https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
11+
public enum HTTPMethod: String {
12+
case get = "GET"
13+
case head = "HEAD"
14+
case put = "PUT"
15+
case post = "POST"
16+
case delete = "DELETE"
17+
case connect = "CONNECT"
18+
case options = "OPTIONS"
19+
case trace = "TRACE"
20+
case patch = "PATCH"
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Steve Galbraith on 9/24/19.
6+
//
7+
8+
import Foundation
9+
10+
11+
public enum NetworkError: Error {
12+
case failure(message: String)
13+
case unauthorized
14+
case unexpected
15+
}
16+
17+
extension NetworkError: LocalizedError {
18+
public var errorDescription: String? {
19+
switch self {
20+
case .failure(let message):
21+
return message
22+
case .unauthorized:
23+
return "Unauthorized action"
24+
case .unexpected:
25+
return "An unexpected error occured. Please try again later."
26+
}
27+
}
28+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//
2+
// NetworkManager.swift
3+
//
4+
//
5+
// Created by Steve Galbraith on 9/24/19.
6+
//
7+
8+
import Foundation
9+
import RxSwift
10+
11+
public final class NetworkManager: Network {
12+
typealias Header = [String : String]
13+
private let environment: NetworkEnvironment
14+
private let session: Session
15+
private let sessionCookieName: String
16+
private var authCookie: HTTPCookie? {
17+
didSet {
18+
if authCookie != nil {
19+
saveCookie()
20+
} else {
21+
guard let oldDomain = oldValue?.domain else {
22+
deleteCookie(for: nil)
23+
return
24+
}
25+
26+
let url = URL(string: oldDomain)
27+
deleteCookie(for: url)
28+
}
29+
}
30+
}
31+
32+
// MARK: - Initializer
33+
34+
init(environment: NetworkEnvironment, session: Session, sessionCookieName: String = "") {
35+
self.environment = environment
36+
self.session = session
37+
self.sessionCookieName = sessionCookieName
38+
}
39+
40+
// MARK: - Network
41+
42+
public func request(method: HTTPMethod, endpoint: String, parameters: [String : Any]? = nil, headers: [String : String]? = nil) -> Observable<Result<Data, NetworkError>> {
43+
dataRequest(method: method, endpoint: endpoint, parameters: parameters, headers: defaultHeaders(headers))
44+
}
45+
46+
public func authenticatedRequest(method: HTTPMethod, endpoint: String, parameters: [String : Any]? = nil, headers: [String : String]? = nil) -> Observable<Result<Data, NetworkError>> {
47+
dataRequest(method: method, endpoint: endpoint, parameters: parameters, headers: authenticatedHeaders(headers), requiresAuthentication: true)
48+
}
49+
50+
// MARK: - Helpers
51+
52+
private func dataRequest(method: HTTPMethod, endpoint: String, parameters: [String : Any]?, headers: Header?, requiresAuthentication: Bool = false) -> Observable<Result<Data, NetworkError>> {
53+
guard let request = generateRequest(method: method, endpoint: endpoint, parameters: parameters, headers: headers) else {
54+
return Observable.error(NetworkError.unexpected)
55+
}
56+
let result = ReplaySubject<Result<Data, NetworkError>>.create(bufferSize: 1)
57+
let dataTask = session.dataTask(with: request as NSURLRequest) { [weak self] data, response, error in
58+
guard error == nil else {
59+
let message = error!.localizedDescription
60+
let apiError = NetworkError.failure(message: message)
61+
result.onNext(.failure(apiError))
62+
return
63+
}
64+
65+
guard let data = data else {
66+
let apiError: NetworkError = .failure(message: "Error: No data returned from network request")
67+
result.onNext(.failure(apiError))
68+
return
69+
}
70+
71+
if let httpURLResponse = response as? HTTPURLResponse {
72+
self?.setCookies(for: httpURLResponse.allHeaderFields)
73+
}
74+
75+
result.onNext(.success(data))
76+
}
77+
78+
dataTask.resume()
79+
return result
80+
}
81+
82+
private func generateRequest(method: HTTPMethod, endpoint: String, parameters: [String: Any]?, headers: [String: String]?) -> URLRequest? {
83+
guard let baseURL = URL(string: environment.url) else { return nil }
84+
85+
var urlString = endpoint
86+
var httpBody: Data? = nil
87+
88+
switch method {
89+
case .get:
90+
urlString += "?\(queryString(for: parameters))"
91+
case .post:
92+
httpBody = generateHTTPBody(from: parameters)
93+
default:
94+
break
95+
}
96+
guard let url = URL(string: urlString, relativeTo: baseURL) else { return nil }
97+
var request = URLRequest(url: url)
98+
request.httpBody = httpBody
99+
100+
headers?.forEach({ (key, value) in
101+
request.setValue(value, forHTTPHeaderField: key)
102+
})
103+
104+
return request
105+
}
106+
107+
private func queryString(for params: [String: Any]?) -> String {
108+
guard let parameters = params as? [String: String] else { return "" }
109+
110+
var paramList = [String]()
111+
112+
for (key, value) in parameters {
113+
guard let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {
114+
print("Failed to convert key \(key)")
115+
continue
116+
}
117+
118+
guard let escapedValue = (value as AnyObject).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {
119+
print("Failed to convert value \(value)")
120+
continue
121+
}
122+
123+
paramList.append("\(escapedKey)=\(escapedValue)")
124+
}
125+
126+
return paramList.joined(separator: "&")
127+
}
128+
129+
private func generateHTTPBody(from params: [String: Any]?) -> Data? {
130+
guard let parameters = params else { return nil }
131+
return try? JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
132+
}
133+
134+
private func merge(_ optional: [String: Any]?, with base: [String: Any]) -> [String: Any] {
135+
guard let additions = optional else {
136+
return base
137+
}
138+
// If there's a conflict, take the new override
139+
return base.merging(additions) { (_, new) in new }
140+
}
141+
142+
private func defaultHeaders(_ headers: [String: Any]?) -> Header {
143+
let defaultHeaders = [
144+
"Accept" : "application/json"
145+
]
146+
guard let merged = merge(headers, with: defaultHeaders) as? Header
147+
else { return defaultHeaders }
148+
149+
return merged
150+
}
151+
152+
private func authenticatedHeaders(_ headers: [String: String]?) -> Header {
153+
guard let cookie = authCookie else { return Header() }
154+
155+
let headerDefaults = defaultHeaders(headers)
156+
let authHeader = HTTPCookie.requestHeaderFields(with: [cookie])
157+
guard let merged = merge(headerDefaults, with: authHeader) as? Header
158+
else {
159+
return authHeader
160+
}
161+
162+
return merged
163+
}
164+
165+
private func setCookies(for headers: [AnyHashable : Any]) {
166+
guard
167+
let headerStrings = headers as? [String : String],
168+
let url = URL(string: environment.host)
169+
else { return }
170+
171+
let allCookies = HTTPCookie.cookies(withResponseHeaderFields: headerStrings, for: url)
172+
173+
guard let sessionCookie = allCookies.first(where: { $0.name == sessionCookieName }) else { return }
174+
authCookie = sessionCookie
175+
}
176+
177+
private func saveCookie() {
178+
guard
179+
let cookie = authCookie,
180+
let url = URL(string: environment.host)
181+
else { return }
182+
183+
let headerFields = HTTPCookie.requestHeaderFields(with: [cookie])
184+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
185+
HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: url)
186+
}
187+
188+
private func deleteCookie(with name: String? = nil, for url: URL?) {
189+
guard let cookieURL = url else {
190+
let components = DateComponents(month: -1)
191+
if let oneMonthAgo = Calendar.current.date(byAdding: components, to: Date()) {
192+
HTTPCookieStorage.shared.removeCookies(since: oneMonthAgo)
193+
}
194+
return
195+
}
196+
197+
guard let cookies = HTTPCookieStorage.shared.cookies(for: cookieURL) else { return }
198+
199+
guard let cookieName = name else {
200+
cookies.forEach { cookie in
201+
HTTPCookieStorage.shared.deleteCookie(cookie)
202+
}
203+
return
204+
}
205+
206+
cookies.forEach { cookie in
207+
if cookie.name == cookieName {
208+
HTTPCookieStorage.shared.deleteCookie(cookie)
209+
}
210+
}
211+
}
212+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
public struct ObservableNetworking: ObservableNetwork {
4+
public let network: Network
5+
6+
public init(environment: NetworkEnvironment, session: URLSession = .shared) {
7+
self.network = NetworkManager(environment: environment, session: session)
8+
}
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// Network.swift
3+
//
4+
//
5+
// Created by Steve Galbraith on 9/24/19.
6+
//
7+
8+
import Foundation
9+
import RxSwift
10+
11+
public protocol Network {
12+
// RxSwift
13+
func request(method: HTTPMethod, endpoint: String, parameters: [String : Any]?, headers: [String : String]?) -> Observable<Result<Data, NetworkError>>
14+
func authenticatedRequest(method: HTTPMethod, endpoint: String, parameters: [String : Any]?, headers: [String : String]?) -> Observable<Result<Data, NetworkError>>
15+
}
16+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// NetworkEnvironment.swift
3+
//
4+
//
5+
// Created by Steve Galbraith on 9/24/19.
6+
//
7+
8+
import Foundation
9+
10+
public protocol NetworkEnvironment {
11+
var scheme: String { get }
12+
var host: String { get }
13+
var path: String { get }
14+
var url: String { get }
15+
}
16+
17+
public extension NetworkEnvironment {
18+
var url: String {
19+
"\(scheme)://\(host)/\(path)"
20+
}
21+
}

0 commit comments

Comments
 (0)