Opera
Protocol-Oriented Network abstraction layer written in Swift.
Install / Use
/learn @xmartlabs/OperaREADME
OperaSwift
<p align="left"> <a href="https://travis-ci.org/xmartlabs/Opera"><img src="https://travis-ci.org/xmartlabs/Opera.svg?branch=master" alt="Build status" /></a> <img src="https://img.shields.io/badge/platform-iOS%20|%20OSX%20|%20watchOS%20|%20tvOS-blue.svg?style=flat" alt="Platform iOS" /> <a href="https://developer.apple.com/swift"><img src="https://img.shields.io/badge/swift4-compatible-4BC51D.svg?style=flat" alt="Swift 4 compatible" /></a> <a href="https://github.com/Carthage/Carthage"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat" alt="Carthage compatible" /></a> <a href="https://cocoapods.org/pods/OperaSwift"><img src="https://img.shields.io/cocoapods/v/OperaSwift.svg" alt="CocoaPods compatible" /></a> <a href="https://raw.githubusercontent.com/xmartlabs/Opera/master/LICENSE"><img src="http://img.shields.io/badge/license-MIT-blue.svg?style=flat" alt="License: MIT" /></a> </p>Made with ❤️ by XMARTLABS. View all our open source contributions.
Introduction
Protocol-Oriented Network abstraction layer written in Swift. Greatly inspired by RxPagination project but working on top of Alamofire and the JSON parsing library of your choice.
Features
- API abstraction through
RouteTypeconformance. - Pagination support through
PaginationRequestTypeconformance. - Supports for any JSON parsing library such as Decodable and Argo through
OperaDecodableprotocol conformance. - Networking errors abstraction through
OperaErrortype. OperaSwiftOperaErrorindicates either anNSURLSessionerror,Alamofireerror, or your JSON parsing library error. - RxSwift wrappers around
Alamofire.Requestthat return either aSingleof a JSON serialized type or an array if it or a completable sequence. NetworkError is passed when error event happens. - RxSwift wrappers around
PaginationRequestTypethat return aSingleof aPaginationResponseTypewhich contains the serialized elements and information about the current, next and previous page. - Ability to easily mock services through
RouteType.sampleData. - Ability to use multiple
RequestAdaptersthroughCompositeAdapter. - Easily upload files or images using HTTP multipart requests.
- Download progress on every
RouteTypeand upload progress onMultipartRouteType.
Usage
Route setup
A RouteType is a high level representation of the request for a REST API endpoint. By adopting the RouteType protocol a type is able to create its corresponding request.
import Alamofire
import OperaSwift
// just a hierarchy structure to organize routes
struct GithubAPI {
struct Repository {}
}
extension GithubAPI.Repository {
struct Search: RouteType {
var method: HTTPMethod { return .get }
var path: String { return "search/repositories" }
}
struct GetInfo: RouteType {
let owner: String
let repo: String
var method: HTTPMethod { return .get }
var path: String { return "repos/\(owner)/\(repo)" }
}
}
Alternatively, you can opt to conform to
RouteTypeform an enum where each enum value is a specific route (api endpoint) with its own associated values.
If you are curious check out the rest of RouteType protocol definition.
As you may have seen, any type that conforms to RouteType must provide baseUrl and the Alamofire manager instance.
Usually these values do not change among our routes so we can provide them by implementing a protocol extension over RouteType as shown below.
extension RouteType {
var baseURL: URL {
return URL(string: "https://api.github.com")!
}
var manager: ManagerType {
return Manager.singleton
}
}
Now, by default, all
RouteTypes we define will providehttps://api.github.comasbaseUrlandManager.singletonasmananger. It's up to you to customize it within a specific RouteType protocol conformance.
Default RouteTypes
To avoid having to implement the method property in every RouteType Opera provides A protocol for each HTTPMethod so you can implement those:
protocol GetRouteType: RouteType {}
protocol PostRouteType: RouteType {}
protocol OptionsRouteType: RouteType {}
protocol HeadRouteType: RouteType {}
protocol PutRouteType: RouteType {}
protocol PatchRouteType: RouteType {}
protocol DeleteRouteType: RouteType {}
protocol TraceRouteType: RouteType {}
protocol ConnectRouteType: RouteType {}
They are pretty simple, they only implement the method property of RouteType with the HTTPMethod that matches.
Additional RouteTypes
ImageUploadRouteType
struct Upload: ImageUploadRouteType {
let image: UIImage
let encoding: ImageUploadEncoding = .jpeg(quality: 0.80)
let path = "/upload"
let baseURL = URL(string: "...")!
}
And then use it like this:
Upload(image: UIImage(named: "myImage")!)
.rx
.completable()
.subscribe(
onCompleted: {
// success :)
},
onError: { error in
// do something when something went wrong
}
)
.addDisposableTo(disposeBag)
Note: If you want to upload a generic list of files through an HTTP multipart request, use
MultipartRouteTypeinstead.
Creating requests
At this point we can easily create an Alamofire Request:
let request: Request = GithubAPI.Repository.GetInfo(owner: "xmartlabs", repo: "Opera").request
Notice that
RouteTypeconforms toAlamofire.URLConvertibleso having the manager we can create the associatedRequest.
We can also take advantage of the reactive helpers provided by Opera:
request
.rx.collection()
.subscribe(
onNext: { (repositories: [Repository]) in
// do something when networking and Json parsing completes successfully
},
onError: {(error: Error) in
// do something when something went wrong
}
)
.addDisposableTo(disposeBag)
getInfoRequest
.rx.collection()
.subscribe(
onSuccess: { (repositories: [Repository]) in
// do something when networking and Json parsing completes successfully
},
onError: {(error: Error) in
guard let error = error as? OperaError else {
//do something when it's not an OperaError
}
// do something with the OperaError
}
)
.addDisposableTo(disposeBag)
If you are not interested in decode your JSON response into a Model you can invoke
request.rx.any()which returns anSingleofAnyfor the current request and propagates aOperaErrorerror through the result sequence if something goes wrong.
Error Handling
If you are using the reactive helpers (which are awesome btw!) you can handle the errors on the onError callback which returns an Error that, in case of Networking or Parsing issues, can be casted to OperaError for easier usage.
OperaError wraps any error that is Networking or Parsing related. Keep in mind that you have to cast the Error on the onError callback before using it.
OperaError also provides a set of properties that make accessing the error's data easier:
public var error: Error
public var request: URLRequest?
public var response: HTTPURLResponse?
public var body: Any?
public var statusCode: Int?
public var localizedDescription: String
Example:
getInfoRequest
.rx.object()
.subscribe(
onError: {(error: Error) in
guard let error = error as? OperaError else {
//do something when it's not an OperaError
}
// do something with the OperaError
debugPrint("Request failed with status code \(error.statusCode)")
}
)
.addDisposableTo(disposeBag)
Download & Upload progress
Every RouteType can optionally chain a download progress handler through its reactive extension:
let request: RouteType = ...
request
.rx.collection()
.downloadProgress {
debugPrint("Download progress: \($0.fractionCompleted)")
}
.subscribe(
onNext: { (repositories: [Repository]) in
// do something when networking and Json parsing completes successfully
},
onError: {(error: Error) in
// do something when something went wrong
}
)
.addDisposableTo(disposeBag)
Only if the routeType is a MultipartRouteType we can also chain an upload progress handler:
ImageUploadRouteTypeis a specificMultipartRouteTypeto easily upload images.
let imageUpload: ImageUploadRouteType = ...
imageUpload
.rx
.uploadProgress {
debugPrint("Upload progress: \($0.fractionCompleted)")
}
.downloadProgress {
debugPrint("Download progress: \($0.fractionCompleted)")
}
.completable()
.subscribe(
onCompleted: {
debugPrint("Completed")
},
onError: { error in
...
}
)
.addDisposableTo(disposeBag)
Decoding
We've said Opera is able to decode JSON response into a Model using your favorite JSON parsing library. Let's see how Opera accomplishes that.
At Xmartlabs we have been using
Decodableas our JSON parsing library since March 16. Before that we had used Argo, ObjectMapper and many others. I don't want to deep into the reason of our JSON parsing library choice (we do have our reasons ;)) but during Opera implementation/design we thought it was a good feature to be flexible about it.
This is our Repository model...
struct Repository {
let id: Int
let name: String
let desc: String?
let company: String?
let language: String?
let openIssues: Int
let stargazersCount: Int
let forksCount: Int
let url: NSURL
let createdAt: NSDate
}
and OperaDecodable
