Palm trees, coral reefs and breaking waves. Welcome to the surf club Malibu , a networking library built on promises . It's more than just a wrapper around NSURLSession
, but a powerful framework that helps to chain your requests, validations and request processing.
UsingWhen under the hood, Malibu adds a lot of sugar helpers and moves your code up to the next level:
DRY
principle. Equip yourself with the necessary gears of Malibu , become a big wave surfer and let the days of shark infested asynchronous networking be a thing of the past. Enjoy the ride!
ETag
support You can start your ride straight away, not thinking about configurations:
// Declare your request struct BoardsRequest: GETRequestable { var message = Message(resource: "http://sharkywaters.com/api/boards") init(kind: Int, text: String) { message.parameters = ["type": kind, "text": text] } } // Make a call let request = BoardsRequest(kind: 1, text: "classic") Malibu.GET(request) .validate() .toJSONDictionary() .then({ dictionary -> [Board] in // Let's say we use https://github.com/zenangst/Tailor for mapping return try dictionary.relationsOrThrow("boards") as [Board] }) .done({ boards in // Handle response data }) .fail({ error in // Handle errors }) .always({ _ in // Hide progress bar })
If you still don't see any benefits, keep scrolling down and be ready for even more magic
You can love it or you can hate it, but either way you have to create a struct
or a class
representing your request. This decision has been made to separate concerns into well-defined layers, so request with all it's properties is described in one place, out from the actual usage.
There are 6 protocols corresponding to HTTP methods: GETRequestable
, POSTRequestable
, PATCHRequestable
, PUTRequestable
, DELETERequestable
, HEADRequestable
. Just conform to one of them and you're ready to surf.
struct BoardsRequest: GETRequestable { // Message is a container for request URL, parameters and headers var message = Message(resource: "boards") // Enables or disables automatic ETags handling var etagPolicy = .Disabled init() { message.headers = ["custom": "header"] } } struct BoardCreateRequest: POSTRequestable { var message = Message(resource: "boards") // Content type is set to `.JSON` by default for POST var contentType: ContentType = .FormURLEncoded init(kind: Int, title: String) { message.parameters = ["type" : kind, "title" : title] } } struct BoardDeleteRequest: DELETERequestable { var message: Message init(id: Int) { message = Message(resource: "boards:/(id)") } }
Query
- creates a query string to be appended to any existing URL. FormURLEncoded
- uses application/x-www-form-urlencoded
as a Content-Type
and formats your parameters with percent-encoding. JSON
- sets the Content-Type
to application/json
and sends a JSON representation of the parameters as the body of the request. MultipartFormData
- sends parameters encoded as multipart/form-data
. Custom(String)
- uses given Content-Type
string as a header. Malibucomes with 3 parameter encoding implementations:
FormURLEncoder
- a percent-escaped encoding following RFC 3986. JSONEncoder
- NSJSONSerialization
based encoding. MultipartFormEncoder
- multipart data builder. You can extend default functionality by adding a custom parameter encoder that conforms to ParameterEncoding
protocol:
// Override default JSON encoder Malibu.parameterEncoders[.JSON] = CustomJSONEncoder() // Register encoder for the custom encoding type Malibu.parameterEncoders[.Custom("application/xml")] = CustomXMLEncoder()
Malibucares about HTTP ETags . When the web server returns an HTTP response header ETag
, it will be cached locally and set as If-None-Match
request header next time you perform the same request. Automatic ETags handling is enabled by default for GET
, PUT
and PATCH
requests, but it could easily be changed for the each request specifically.
struct BoardsRequest: GETRequestable { var etagPolicy = .Disabled }
Networking
class is a core component of Malibu that sets shared headers, pre-process and executes actual HTTP requests.
Networking
is created with SessionConfiguration
which is a wrapper around NSURLSessionConfiguration
and could represent 3 standard session types + 1 custom type:
Default
- configuration that uses the global singleton credential, cache and cookie storage objects. Ephemeral
- configuration with no persistent disk storage for cookies, cache or credentials. Background
- session configuration that can be used to perform networking operations on behalf of a suspended application, within certain constraints. Custom(NSURLSessionConfiguration)
- if you're not satisfied with standard types, your custom NSURLSessionConfiguration
goes here. It's pretty straightforward to create a new Networking
instance:
// Simple networking with `Default` configuration and no base URL let simpleNetworking = Networking() // More advanced networking let networking = Networking( // Every request made on this networking will be scoped by the base URL baseURLString: "http://sharkywaters.com/api/", // `Background` session configuration sessionConfiguration: .Background, // Custom `NSURLSessionDelegate` could set if needed sessionDelegate: self )
Additional headers will be used in the each request made on the networking:
networking.additionalHeaders = { ["Accept" : "application/json"] }
Notethat Accept-Language
, Accept-Encoding
and User-Agent
headers are included automatically.
// Use this closure to modify your `Requestable` value before `NSURLRequest` // is created on base of it networking.beforeEach = { request in var request = request request.message.parameters["userId"] = "12345" return request } // Use this closure to modify generated `NSMutableURLRequest` object // before the request is made networking.preProcessRequest = { (request: NSMutableURLRequest) in request.addValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", forHTTPHeaderField: "token") }
// HTTP basic authentication with username and password networking.authenticate(username: "malibu", password: "surfingparadise") // OAuth 2.0 authentication with Bearer token networking.authenticate(bearerToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") // Custom authorization header networking.authenticate(authorizationHeader: "Malibu-Header")
Networking
is set up and ready, so it's time to fire some requests. Make a request by calling GET
, POST
, PUT
, PATCH
, DELETE
or HEAD
functions with the corresponding request as an argument.
let networking = Networking(baseURLString: "http://sharkywaters.com/api/") networking.GET(BoardsRequest()) .validate() .toJSONDictionary() .done({ data in print(data) }) networking.POST(BoardCreateRequest(kind: 2, title: "Balsa Fish")) .validate() .toJSONDictionary() .done({ data in print(data) }) networking.DELETE(BoardDeleteRequest(id: 11)) .fail({ error in print(error) })
Wave
object consists of NSData
, NSURLRequest
and NSHTTPURLResponse
properties.
Ride
is returned by every request method. It extends Promise<Wave>
by adding NSURLSessionTask
that you might want to cancel when it's needed. You may use Ride
object to add different callbacks and build chains of tasks. It has a range of useful helpers, such as validations and serialization.
let ride = networking.GET(BoardsRequest()) // Cancel the task ride.cancel() // Create chains and add callbacks on promise object ride .validate() .toString() .then({ string in // ... }) .done({ _ in // ... })
Mocking is great when it comes to writing your tests. But it also could speed up your development while the backend developers are working really hardly on API implementation.
In order to start mocking you have to do the following:
Change the mode
// A mode for real HTTP request only Malibu.mode = .Regular // A mode for mocks only Malibu.mode = .Fake // Both real and fake requests can be used in a mix Malibu.mode = .Partial
// With response data from file networking.register(mock: Mock( // Request to be mocked request: BoardsRequest(), // Name of the file fileName: "boards.json" )) // With response from JSON dictionary networking.register(mock: Mock( // Request to be mocked request: BoardsRequest(), // JSON dictionary JSON: ["boards": [["id": 1, "title": "Balsa Fish"]]] )) // NSData mock networking.register(mock: Mock( // Request to be mocked request: BoardsRequest(), // Needed response response: mockedResponse, // Response data data: responseData, // Custom error, `nil` by default error: customError ))
Malibugives you a bunch of methods to serialize response data:
let ride = networking.GET(BoardsRequest()) ride.toData() // -> Promise<NSData> ride.toString() // -> Promise<String> ride.toJSONArray() // -> Promise<[[String: AnyObject]]> ride.toJSONDictionary() // -> Promise<[String: AnyObject]>
Malibucomes with 4 validation methods:
// Validates a status code to be within 200..<300 // Validates a response content type based on a request's "Accept" header networking.GET(BoardsRequest()).validate() // Validates a response content type networking.GET(BoardsRequest()).validate( contentTypes: ["application/json; charset=utf-8"] ) // Validates a status code networking.GET(BoardsRequest()).validate(statusCodes: [200]) // Validates with custom validator conforming to `Validating` protocol networking.GET(BoardsRequest()).validate(validator: CustomValidator())
Malibuhandles multiple networkings which you can register and resolve from the container. Doing that, it's super easy to support several APIs and configurations in your app.
let networking = Networking(baseURLString: "http://sharkywaters.com/api/") networking.additionalHeaders = { ["Accept" : "application/json"] } // Register Malibu.register("base", networking: networking) // Perform request using specified networking configuration Malibu.networking("base").GET(BoardsRequest(kind: 1, text: "classic")) // Unregister Malibu.unregister("base")
Malibuhas a shared networking object with default configurations for the case when you need just something simple to catch the wave. It's not necessary to invoke it directly, just call the same GET
, POST
, PUT
, PATCH
, DELETE
methods right on Malibu
:
Malibu.GET(BoardsRequest())
If you want to see some request, response and error info in the console, you get this for free. Just choose one of the available log levels:
None
- logging is disabled, so your console is not littered with networking stuff. Error
- prints only errors that occur during the request execution. Info
- prints incoming request method + URL, response status code and errors. Verbose
- prints incoming request headers and parameters in addition to everything printed in the Info
level. Optionally you can set your own loggers and adjust the logging to your needs:
// Custom logger that conforms to `ErrorLogging` protocol Malibu.logger.errorLogger = CustomErrorLogger.self // Custom logger that conforms to `RequestLogging` protocol Malibu.logger.requestLogger = RequestLogger.self // Custom logger that conforms to `ResponseLogging` protocol Malibu.logger.responseLogger = ResponseLogger.self
Hyper Interaktiv AS, ios@hyper.no
Malibuis available through CocoaPods . To install it, simply add the following line to your Podfile:
pod 'Malibu'
Malibuis also available throughCarthage. To install just write into your Cartfile:
github "hyperoslo/Malibu"
Malibucan also be installed manually. Just Download and drop /Sources
folder in your project.
Hyper Interaktiv AS, ios@hyper.no
Credits go toAlamofire for inspiration and toWhen for promises .
We would love you to contribute to Malibu , check theCONTRIBUTING file for more info.
Malibuis available under the MIT license. See theLICENSE file for more info.