Skip to content

Commit d8c4e9e

Browse files
committed
rewrite parseable protocol with associated types, inject response and error parsers everywhere
1 parent 3de99ac commit d8c4e9e

18 files changed

+330
-267
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
## Next
5+
6+
* `Parseable` protocol was rewritten from generic static func to having associatedtype and a func inside. This is done to broaden support for any kinds of mappers. Good example would be CoreData objects creation, that requires different contexts for background threads, and was difficult to implement using old syntax.
7+
* All `TRON` request methods now contain obligatory `responseParser` and `errorParser` properties, that define, how response and errors are parsed.
8+
* `APIStub` objects are now using `responseParser` and `errorParser` of the request, and therefore have been entirely rewritten with new properties, that accept `Data` objects instead of directly setting result.
9+
* `JSONDecodable` protocol now provides convenience methods to use with `TRON` and `APIRequest`, that work the same as before, therefore almost maintaining backwards code compatibility for `TRON/SwiftyJSON` subspec.
10+
* `ErrorBuilder` class was removed.
11+
412
## [2.0.0-beta.1](https://github.com/MLSDev/TRON/releases/tag/2.0.0-beta.1)
513

614
`TRON` 2.0 is supported on iOS 9.0, macOS 10.11 and higher due to Alamofire.framework required versions. Read [migration guide](/Docs/2.0 Migration Guide.md) for overview of API changes.

Source/Core/APIRequest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Alamofire
2828
/**
2929
`APIRequest` encapsulates request creation logic, stubbing options, and response/error parsing.
3030
*/
31-
open class APIRequest<Model: Parseable, ErrorModel: Parseable>: BaseRequest<Model,ErrorModel> {
31+
open class APIRequest<Model, ErrorModel>: BaseRequest<Model,ErrorModel> {
3232

3333
override func alamofireRequest(from manager: SessionManager) -> Request {
3434
return manager.request(urlBuilder.url(forPath: path), method: method,

Source/Core/APIStub.swift

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,57 @@ public extension APIStub {
4040
public func buildModel(fromFileNamed fileName: String, inBundle bundle: Bundle = Bundle.main) {
4141
if let filePath = bundle.path(forResource: fileName as String, ofType: nil)
4242
{
43-
guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
44-
print("failed building response model from file: \(filePath)")
45-
return
46-
}
47-
model = try? Model.parse(data)
43+
successData = try? Data(contentsOf: URL(fileURLWithPath: filePath))
4844
}
4945
}
5046
}
5147

5248
/**
5349
`APIStub` instance that is used to represent stubbed successful or unsuccessful response value.
5450
*/
55-
open class APIStub<Model: Parseable, ErrorModel: Parseable> {
51+
open class APIStub<Model, ErrorModel> {
5652

5753
/// Should the stub be successful. By default - true
5854
open var successful = true
5955

56+
/// Data to be passed to successful stub
57+
open var successData : Data?
58+
59+
/// Error to be passed into request's `errorParser` if stub is failureful.
60+
open var errorRequest : URLRequest?
61+
62+
/// HTTP response to be passed into request's `errorParser` if stub is failureful.
63+
open var errorResponse: HTTPURLResponse?
64+
65+
/// Error Data to be passed into request's `errorParser` if stub is failureful.
66+
open var errorData : Data?
67+
68+
/// Loading error to be passed into request's `errorParser` if stub is failureful.
69+
open var loadingError : Error?
70+
6071
/// Response model for successful API stub
61-
open var model : Model?
72+
open var model : Model? {
73+
guard let data = successData else { return nil }
74+
guard let request = request else { return nil}
75+
return try? request.responseParser(data)
76+
}
6277

6378
/// Error model for unsuccessful API stub
64-
open var error: APIError<ErrorModel>?
79+
open var error: APIError<ErrorModel>? {
80+
return request?.errorParser(errorRequest, errorResponse, errorData, loadingError)
81+
}
6582

6683
/// Delay before stub is executed
6784
open var stubDelay = 0.1
6885

86+
/// Weak request property, that is used when executing stubs using request `errorParser` or `responseParser`.
87+
open weak var request: BaseRequest<Model,ErrorModel>?
88+
89+
/// Creates `APIStub`, and configures it for `request`.
90+
init(request: BaseRequest<Model,ErrorModel>) {
91+
self.request = request
92+
}
93+
6994
/**
7095
Stub current request.
7196

@@ -74,12 +99,18 @@ open class APIStub<Model: Parseable, ErrorModel: Parseable> {
7499
- parameter failureBlock: Failure block to be executed if request fails. Nil by default.
75100
*/
76101
open func performStub(withSuccess successBlock: ((Model) -> Void)? = nil, failure failureBlock: ((APIError<ErrorModel>) -> Void)? = nil) {
77-
if let model = model, successful {
78-
delay(stubDelay) {
102+
delay(stubDelay) { [weak self] in
103+
if self?.successful ?? false {
104+
guard let model = self?.model else {
105+
print("Attempting to stub successful request, however successData is nil")
106+
return
107+
}
79108
successBlock?(model)
80-
}
81-
} else if let error = error {
82-
delay(stubDelay) {
109+
} else {
110+
guard let error = self?.error else {
111+
print("Attempting to stub failed request, however apiStub does not produce error")
112+
return
113+
}
83114
failureBlock?(error)
84115
}
85116
}
@@ -91,33 +122,48 @@ open class APIStub<Model: Parseable, ErrorModel: Parseable> {
91122
- parameter completionBlock: Completion block to be executed when request is stubbed.
92123
*/
93124
open func performStub(withCompletion completionBlock : @escaping ((Alamofire.DataResponse<Model>) -> Void)) {
94-
delay(stubDelay) {
125+
delay(stubDelay) { [weak self] in
95126
let result : Alamofire.Result<Model>
96-
if let model = self.model, self.successful {
127+
if self?.successful ?? false {
128+
guard let model = self?.model else {
129+
print("Attempting to stub successful request, however successData is nil")
130+
return
131+
}
97132
result = Result.success(model)
98-
} else if let error = self.error {
99-
result = Result.failure(error)
100133
} else {
101-
let error : APIError<ErrorModel> = APIError(request: nil, response: nil, data: nil, error: nil)
134+
guard let error = self?.error else {
135+
print("Attempting to stub failed request, however apiStub does not produce error")
136+
return
137+
}
102138
result = Result.failure(error)
103139
}
104140
let response: Alamofire.DataResponse<Model> = Alamofire.DataResponse(request: nil, response: nil, data: nil, result: result)
105141
completionBlock(response)
106142
}
107143
}
108144

145+
/**
146+
Stub current download request.
147+
148+
- parameter completionBlock: Completion block to be executed when request is stubbed.
149+
*/
109150
open func performStub(withCompletion completionBlock : @escaping ((Alamofire.DownloadResponse<Model>) -> Void)) {
110-
delay(stubDelay) {
151+
delay(stubDelay) { [weak self] in
111152
let result : Alamofire.Result<Model>
112-
if let model = self.model, self.successful {
153+
if self?.successful ?? false {
154+
guard let model = self?.model else {
155+
print("Attempting to stub successful download request, however successData is nil")
156+
return
157+
}
113158
result = Result.success(model)
114-
} else if let error = self.error {
115-
result = Result.failure(error)
116159
} else {
117-
let error : APIError<ErrorModel> = APIError(request: nil, response: nil, data: nil, error: nil)
160+
guard let error = self?.error else {
161+
print("Attempting to stub failed download request, however apiStub does not produce error")
162+
return
163+
}
118164
result = Result.failure(error)
119165
}
120-
let response: Alamofire.DownloadResponse<Model> = DownloadResponse(request: nil, response: nil, temporaryURL: nil, destinationURL: nil, resumeData: nil, result: result)
166+
let response: DownloadResponse<Model> = DownloadResponse(request: nil, response: nil, temporaryURL: nil, destinationURL: nil, resumeData: nil, result: result)
121167
completionBlock(response)
122168
}
123169
}

Source/Core/BaseRequest.swift

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@ public protocol TronDelegate: class {
8484
}
8585

8686
/// Base class, that contains common functionality, extracted from `APIRequest` and `MultipartAPIRequest`.
87-
open class BaseRequest<Model: Parseable, ErrorModel: Parseable> {
87+
open class BaseRequest<Model, ErrorModel> {
88+
89+
/// Serializes Data into Model
90+
public typealias ResponseParser = (Data) throws -> Model
91+
92+
/// Serializes received failed response into APIError<ErrorModel> object
93+
public typealias ErrorParser = (_ request: URLRequest?, _ response: HTTPURLResponse?, _ data: Data?, _ error: Error?) -> APIError<ErrorModel>
8894

8995
/// Relative path of current request
9096
open let path: String
@@ -111,14 +117,17 @@ open class BaseRequest<Model: Parseable, ErrorModel: Parseable> {
111117
/// URL builder for current request
112118
open var urlBuilder: URLBuildable
113119

114-
/// Error builder for current request
115-
open var errorBuilder = ErrorBuilder<ErrorModel>()
120+
/// Serializes Data into Model
121+
open var responseParser : ResponseParser
122+
123+
/// Serializes received failed response into APIError<ErrorModel> object
124+
open var errorParser : ErrorParser
116125

117126
/// Is stubbing enabled for current request?
118127
open var stubbingEnabled = false
119128

120129
/// API stub to be used when stubbing this request
121-
open var apiStub = APIStub<Model, ErrorModel>()
130+
lazy open var apiStub : APIStub<Model, ErrorModel> = { return APIStub(request: self) }()
122131

123132
/// Queue, used for processing response, received from the server. Defaults to TRON.processingQueue queue.
124133
open var processingQueue : DispatchQueue
@@ -133,7 +142,7 @@ open class BaseRequest<Model: Parseable, ErrorModel: Parseable> {
133142
open var plugins : [Plugin] = []
134143

135144
/// Creates `BaseRequest` instance, initialized with several `TRON` properties.
136-
public init(path: String, tron: TRON) {
145+
public init(path: String, tron: TRON, responseParser: @escaping ResponseParser, errorParser: @escaping ErrorParser) {
137146
self.path = path
138147
self.tronDelegate = tron
139148
self.stubbingEnabled = tron.stubbingEnabled
@@ -142,6 +151,8 @@ open class BaseRequest<Model: Parseable, ErrorModel: Parseable> {
142151
self.processingQueue = tron.processingQueue
143152
self.resultDeliveryQueue = tron.resultDeliveryQueue
144153
self.parameterEncoding = tron.parameterEncoding
154+
self.responseParser = responseParser
155+
self.errorParser = errorParser
145156
self.apiStub.successful = tron.stubbingShouldBeSuccessful
146157
}
147158

@@ -156,18 +167,15 @@ open class BaseRequest<Model: Parseable, ErrorModel: Parseable> {
156167
$0.requestDidReceiveResponse(urlRequest, response,data,error)
157168
}
158169
})
159-
guard error == nil else {
160-
return .failure(self.errorBuilder.buildErrorFromRequest(urlRequest, response: response, data: data, error: error))
170+
if let error = error {
171+
return .failure(self.errorParser(urlRequest, response, data, error))
161172
}
162173
let model: Model
163-
do {
164-
model = try Model.parse(data ?? Data())
174+
do { try model = self.responseParser(data ?? Data()) }
175+
catch {
176+
return .failure(self.errorParser(urlRequest, response, data, error))
165177
}
166-
catch let parsingError {
167-
return .failure(self.errorBuilder.buildErrorFromRequest(urlRequest, response: response, data: data, error: parsingError))
168-
}
169-
return .success(model)
170-
}
178+
return .success(model) }
171179
}
172180

173181
internal func performStub(success: ((Model) -> Void)?, failure: ((APIError<ErrorModel>) -> Void)?) -> Bool {

Source/Core/DownloadAPIRequest.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,16 @@ public enum DownloadRequestType {
3636
/**
3737
`DownloadAPIRequest` encapsulates download request creation logic, stubbing options, and response/error parsing.
3838
*/
39-
open class DownloadAPIRequest<ErrorModel: Parseable>: BaseRequest<EmptyResponse,ErrorModel> {
39+
open class DownloadAPIRequest<ErrorModel>: BaseRequest<EmptyResponse,ErrorModel> {
4040

4141
let type : DownloadRequestType
4242

4343
// Creates `DownloadAPIRequest` with specified `type`, `path` and configures it with to be used with `tron`.
44-
public init(type: DownloadRequestType, path: String, tron: TRON) {
44+
public init(type: DownloadRequestType, path: String, tron: TRON, errorParser: @escaping ErrorParser) {
4545
self.type = type
46-
super.init(path: path, tron: tron)
46+
super.init(path: path, tron: tron,
47+
responseParser: { try EmptyResponseParser().parse($0)},
48+
errorParser: errorParser)
4749
}
4850

4951
override func alamofireRequest(from manager: SessionManager) -> Request? {
@@ -101,7 +103,7 @@ open class DownloadAPIRequest<ErrorModel: Parseable>: BaseRequest<EmptyResponse,
101103
}
102104
})
103105
guard error == nil else {
104-
return .failure(self.errorBuilder.buildErrorFromRequest(urlRequest, response: response, data: nil, error: error))
106+
return .failure(self.errorParser(urlRequest, response, nil, error))
105107
}
106108
return .success(EmptyResponse())
107109
}

Source/Core/EmptyResponse.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@
2626
import Foundation
2727

2828
/// Struct, that can be used as Model generic constraint in cases, where you don't care about response type.
29-
public struct EmptyResponse : Parseable {
30-
31-
public static func parse<T : Parseable>(_ data: Data) throws -> T {
32-
guard let type = T.self as? EmptyResponse.Type else {
33-
throw ParsingError.wrongType
34-
}
35-
return type.init() as! T
29+
public struct EmptyResponse {}
30+
31+
public struct EmptyResponseParser : Parseable {
32+
public func parse(_ data: Data) throws -> EmptyResponse {
33+
return EmptyResponse()
3634
}
3735
}

Source/Core/ErrorBuilder.swift

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,8 @@
2525

2626
import Foundation
2727

28-
/**
29-
`ErrorBuilder` class is used to build error object from unsuccessful API requests.
30-
*/
31-
open class ErrorBuilder<U:Parseable>
32-
{
33-
/// initialize default error builder
34-
public init() {}
35-
36-
/**
37-
Build concrete APIError instance.
38-
39-
- parameter request: URLRequest that was unsuccessful
40-
41-
- parameter response: response received from web service
42-
43-
- parameter data: data, contained in response
44-
45-
- error: Error instance, created by Foundation Loading System or Alamofire.
46-
47-
- returns APIError instance
48-
*/
49-
open func buildErrorFromRequest(_ request : URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) -> APIError<U> {
50-
return APIError<U>(request: request, response: response, data: data, error: error)
51-
}
52-
}
53-
5428
/// `APIError<T>` is used as a generic wrapper for all kinds of APIErrors.
55-
public struct APIError<T:Parseable> : Error {
29+
public struct APIError<T> : Error {
5630

5731
/// URLRequest that was unsuccessful
5832
public let request : URLRequest?
@@ -86,7 +60,6 @@ public struct APIError<T:Parseable> : Error {
8660
self.response = response
8761
self.data = data
8862
self.error = error
89-
self.errorModel = try? T.parse(data ?? Data())
9063
}
9164

9265
/**

Source/Core/Parseable.swift

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,8 @@ import Foundation
2929
Protocol, that allows creating parsed models from Data, received in request response.
3030
*/
3131
public protocol Parseable {
32+
associatedtype ModelType
3233

3334
/// Parse `data`, creating `Parseable` model.
34-
static func parse<T:Parseable>(_ data: Data) throws -> T
35-
}
36-
37-
/// Enum with possible parsing errors.
38-
public enum ParsingError : Error
39-
{
40-
/**
41-
Error is thrown, if `Parseable` object does not satisfy requirement of current parser
42-
43-
For example, `JSONDecodable` tries to decode object that is `Parseable` but not `JSONDecodable`, it raises following error.
44-
*/
45-
case wrongType
46-
47-
/// `Parseable` protocol throwed an error while creating `Parseable` model
48-
case constructionFailed
35+
func parse(_ data: Data) throws -> ModelType
4936
}

0 commit comments

Comments
 (0)