TRON 5.0 Migration Guide

October 30, 2020 ยท View on GitHub

Following Semantic Versioning conventions, TRON 5.0 introduces API-breaking changes.

This guide is provided in order to ease the transition of existing applications using previous versions to the latest APIs, as well as explain the design and structure of new and updated functionality.

Requirements

  • iOS 10.0+ / tvOS 10.0+ / macOS 10.12+ / watchOS 3.0+
  • Xcode 10
  • Swift 4.1 and higher

Benefits of Upgrading

  • Improved error serialization
  • New convenient DSL
  • Compatibility with Alamofire 5
  • Simpler network stubs, consistent with Plugin API
  • Simpler Codable configuration
  • New convenience parsers
  • Support for Swift Package Manager in Xcode 11
  • Miscellaneous improvements

Breaking API changes

Error parsing

TRON releases prior to 5.0 required ErrorModel to conform to the same serialization protocol as Model on APIRequest level. Result of network request was presented as Model in successful case and APIError<ErrorModel> in failure case.

This presented several challenges when dealing with errors. First of all you could not use different serialization mechanisms - for example you might have needed Codable Model and JSONDecodable error, which was not possible. Second, more importantly, if you unwrapped APIError<ErrorModel> to ErrorModel level, you no longer have access to HTTP communication information such as HTTP status codes, headers, etc.

TRON 5 changes this design by introducing ErrorSerializable protocol, which is a flattened version of APIError and ErrorModel combined:

public protocol ErrorSerializable: Error {
    init(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?)
}

This protocol replaces generic requirements for ErrorModel, and thus allows to capture request information in a way, that is more convenient to you. APIError now has a new purpose as well, previously it was an abstraction that captured ErrorModel and HTTP request information, now it only captures HTTP request information. This means that when you don't need to handle errors directly, you can just use APIError as a generic constraint for request:

let request : APIRequest<String,APIError> = tron.swiftyJSON.request("get")

If you do need to handle errors, you can subclass APIError and parse errors the way you want. Examples how this can be done with Codable and with SwiftyJSON can be found here.

Those changes propogate through entire framework, so plugins obviously no longer use APIError, CodableParser and JSONDecodableParser now have only one generic argument - Model, since ErrorModel was dropped in favor of ErrorSerializable protocol, which does not have concrete implementation on any of those parsers.

ErrorHandlingDataResponseSerializerProtocol and ErrorHandlingDownloadResponseSerializer have been removed following those changes to error handling. If you've been using them, consider porting your implementation to ErrorSerializable implementation.

Alamofire 5 and API removals

Alamofire 5 brings several breaking changes to TRON. Some of them impact TRON directly, and some - only if you used Alamofire API directly. For the latter case, you might read Alamofire 5 Migration Guide.

Empty type

Alamofire.Empty type was introduced under Alamofire.EmptyResponse protocol, which directly intersects with TRON.EmptyResponse struct. Considering that those data structures serve identical purposes, and have special behavior built into Alamofire, TRON 5 removes EmptyResponse struct completely. As a replacement, TRON Codable and JSONDecodable parsers now understand both Alamofire.Empty and Alamofire.EmptyResponse types, and allow using it the same way as EmptyResponse was previously used:

let request : APIRequest<Empty,APIError> = tron.swiftyJSON.request("status")

Multipart upload

Multipart upload requests previously were built asynchronously, and were a special case that was different from UploadRequest. Starting with Alamofire 5, asynchronous operations have been moved down the line, and multipart form upload requests are now a usual UploadRequest with the same interface.

Following this change performMultipart method on UploadAPIRequest was removed along with its RX counterpart rxPerformMultipart. You can use perform method on UploadAPIRequest and rxResult as it's RX counterpart instead.

Headers

Alamofire 5 introduces HTTPHeaders type that serves as an addition to usual [String:String] API that is commonly used for HTTP headers. TRON.BaseRequest and all request types now use HTTPHeaders type for their headers properties.

Following their deprecation in previous version of TRON HeaderBuildable and HeaderBuilder types were removed along with AuthorizationRequirement enum. You can set headers on request level, or implement a RequestInterceptor instance(Alamofire protocol, that subsumes RequestAdapter and RequestRetrier protocols). Reasons for those types removals is described in #63.

Session

Alamofire.SessionManager was renamed to Alamofire.Session. Following this chage all properties of TRON that worked with it, were renamed to session.

TRON.defaultAlamofireManager() method was removed in favor of using Alamofire.Session.default in TRON initializer.

Changes to error handling

Alamofire 5 now uses AFError as a generic Failure result type as compared to opaque Error type. Because of that, all usage of DataResponse, DownloadResponse instances has been updated with AFError. Also, Plugin API has been migrated to AFError as well.

Make sure to update Plugin methods, as old methods will simply stop working without compile error due to empty protocol extension implementations.

API Stubs

APIStub infrastructure was scaled down dramatically. Previously it had a bunch of methods that followed structure of APIRequest and were resolved when request was performed. This led to a bunch of bugs and shortcomings, that prevented several methods from being called when you were using stubs.

In TRON 5, APIStub serves as mere container for HTTP response data, as well as successful or failureful result. To achieve request stubbing, stubs are now attached to Alamofire.Request itself using associated objects. This allows to always call the same methods for both stubbed and unstubbed requests, thus making them consistent regardless of changes in the infrastructure.

In addition to that, entire test suite for TRON is now itself stubbed using custom implementation of URLProtocol and does not require network to run tests.

New Features

DSL

TRON 5 provides new dot syntax for configuring requests, allowing to take advantage of type inference and reducing duplication. It's important to note, that new DSL does not replace current API but serves as an optional supplement to it, so existing API will continue working just fine.

For example, if you have a method, that returns logout request, like so:

func deleteSession() -> APIRequest<Empty, UnknownError> {
    let request : APIRequest<Empty, UnknownError> = tron.codable.request("session")
    request.method = .delete
    return request
}

It can be expressed with new DSL like so:

func deleteSession() -> APIRequest<Empty, UnknownError> {
    return tron.codable.request("session").delete()
}

New DSL includes methods for configuring parameters, HTTP method, validation, HTTP headers, parameter encoding and more. It also goes beyond just syntax sugar.

One example of that is wrapping internal parameters into a root level dictionary, which is quite common behavior for some API calls. So, let's say login request requires you to send a JSON with object, that has one key "session" and other data wrapped inside. In current API you might have written a following code to implement that:

func signIn(login: String, password: String) -> APIRequest<Session, APIError> {
    let request: APIRequest<Session, APIError> = tron.swiftyJSON.request("auth/session")
    request.parameters["session[email]"] = login
    request.parameters["session[password]"] = password
    request.method = .post
    return request
}

It can be rewritten with new DSL like so:

func signIn(login: String, password: String) -> APIRequest<Session, APIError> {
    return tron.swiftyJSON
    .request("auth/session").post()
    .parameters(["email": login, "password": password], rootKey: "session")
}

If rootKey is present, parameters are wrapped into additional dictionary using rootKey as a key and parameters as values.

Another example is a handling of optional parameters. Sometimes you only need to append parameters if they are non-nil. This is achieved using new optionalParameters method:

func searchEntities(query: String, categoryID: Int?) -> APIRequest<Entity, APIError> {
  return tron.request("search").optionalParameters(["query": query, "category_id": categoryID])
}

If you do need to send null values for nil values in your parameters, there is a Bool flag that allows you to do just that:

func updateUser(firstName: String, lastName: String?) -> APIRequest<User,APIError> {
  return tron.request("me/update").patch().optionalParameters(["firstName": firstName, "lastName": lastName], setNilToNull: true)
}

Interceptors

Alamofire 5 introduced concept of Request.Interceptor as a wrapper for Request.Adapter and Request.Retrier. To use them globally, just set interceptor property on Session object you are passing to TRON initializer. To use per-request interceptors, just add intercept(using:) method call to request creation:

let request: APIRequest<Empty, APIError> = tron.swiftyJSON
    .request("status/200")
    .intercept(using: TimeoutInterceptor(timeoutInterval: 3))

Complete documentation

TRON now includes complete documentation for all functionality, generated by jazzy and hosted by Github Pages: Documentation link.

Response serializers

New response serializers TRONDataResponseSerializer and TRONDownloadResponseSerializer allow to serialize responses using custom closure.

You can now traverse JSON before parsing when using SwiftyJSONDecodable:

let traverseJSON: (JSON) -> JSON = { \$0["root"]["subRoot"] }
let request: APIRequest<JSONDecodableResponse,APIError> = tron
    .swiftyJSON(traversingJSON: traverseJSON)
    .request("path")

TRON how has a property codable, that allows to customize Codable parsing for all network requests:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
tron.codable = CodableSerializer(tron, modelDecoder: decoder)

Miscellaneous stuff

NetworkLoggerPlugin now has a convenience constructor:

let plugin = NetworkLoggerPlugin(logSuccess: false, logFailures: true, logCancelledRequests: false)

JSONDecodable conformances have been added to additional arithmetic types: Int8...Int64, UInt8...64.

Multiple Swift versions of TRON are now allowed using CocoaPods 1.7.0 .swift_versions syntax. Supported versions are: ['4.0', '4.2', '5.0'].

URLBuildable protocol has been replaced with three behaviors on URLBuilder: .appendingPathComponent, .relativeToBaseURL and .custom. Those can be set in TRON initializer:

let tron = TRON(baseURL: "https://www.example.com/", buildingURL: .relativeToBaseURL)

Or you can change URLBuilder.Behavior on per-request basis, using the new DSL:

let request: APIRequest<Int,APIError> = tron.swiftyJSON
.request("status/200")
.buildURL(.relativeToBaseURL)