Latest 0.0.2
Homepage https://github.com/moray95/RestingKit
License MIT
Platforms ios 10.0
Dependencies Alamofire, GRMustache.swift4, PromiseKit
Authors

CI Status
Version
License
Platform

Introduction

RestingKit is a higher-level wrapper around Alamofire and PromiseKit written in Swift that allows developers to concentrate on the important stuff instead of writing boiler-plate code for their REST API.

Features

  • Configurable HTTP client (Alamofire is currently the only one provided, but you can write your own!)
  • Path variable expansion powered by GRMustache.swift
  • Interception (and modification) of all requests and responses

Requirements

  • iOS 10.0+

  • Swift 4.2

Installation

RestingKit is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'RestingKit'

Example project

An example project is included within the repositry. To run it, first execute pod install, then open RestingKit.xcworkspace. If you want to test file uploads with the example app, go into the image_server directory and run php -S 0.0.0.0:9000 -c ., which will start a dummy server for your uploads. The uploads will be stored in the uploads directory.

Usage

Basic example

  1. Create a RestingClient
import RestingKit

let requestConverter = RestingRequestConverter()
let restingClient = RestingClient(baseUrl: "https://jsonplaceholder.typicode.com",
                                  decoder: decoder,
                                  requestConverter: requestConverter)
  • RestingClient is the core class within RestingKit that does the heavy lifting by executing the requests. It is configured to use a single base URL, so if you need to access multiple APIs, you’ll need to create multiple clients.
  • RequestConverter s transforms a RestingRequest to a HTTPRequest. RestingRequestConverter is the provided implementation that supports path templating.
  1. Define your models and endpoints

    struct PostCreateModel: Codable {
       let userId: Int
       let title: String
       let body: String
    
       init(userId: Int, title: String, body: String) {
           self.userId = userId
           self.title = title
           self.body = body
       }
    }
    
    struct PostModel: Codable {
       let id: Int
       let userId: Int
       let title: String
       let body: String
    }
    
    let createPostEndpoint = Endpoint<PostCreateModel, PostModel>(.post,
                                                                 "/posts",
                                                                 encoding: .json)

    An Endpoint is defined by the models of the request and response, the path (relative to the RestingClient‘s baseUrl), the HTTP method to use and the encoding. If the request doesn’t expect any content or doesn’t return anything, you can use the special Nothing class. Ideally, we would use Void, but it is not possible to make it Encodable or Decodable.

  2. Create the request and make the actual call

    let postCreateModel = PostCreateModel(userId: 1,
                                         title: "Hello world",
                                         body: "Some awesome message")
    let request = RestingRequest(endpoint createPostEndpoint,
                                body: postCreateModel)
    restingClient.perform(request).done { response in
        print("Headers: (response.headers)")
        let post = response.body
        print("Created post with id: (post.id)")
    }.catch { error in
        print("An error occurred: (error)")
    }

    The promise will fail when the server responds with an HTTP status >299, so you don’t have to handle this case.

    And that’s it!

Handling responses with no content

If a request might provide a response that might be empty, you can create an Endpoint with an optional response type. That way, if the response is empty, nil will be returned.

let createPostEndpoint = Endpoint<PostCreateModel, PostModel?>(.post,
                                                               "/posts",
                                                               encoding: .json)
let postCreateModel = PostCreateModel(userId: 1,
                                      title: "Hello world",
                                      body: "Some awesome message")
let request = RestingRequest(endpoint createPostEndpoint,
                             body: postCreateModel)
restingClient.perform(request).done { response in
    print("Headers: (response.headers)")
    if let post = response.body {
        print("Created post with id: (post.id)")
    } else {
        print("Empty body")
    }
}.catch { error in
    print("An error occurred: (error)")
}

Note: For this feature to work, the response needs to be truely empty (ie. a content-length of 0). An empty JSON object will produce a decoding error.

Path variables

The provided RestingRequestConverter allows templating in paths by using Mustache.swift.

let getPostEndpoint = Endpoint<Nothing, PostModel>(.get,
                                                   "/posts/{{post_id}}",
                                                   encoding: .query)
let request = RestingRequest(endpoint: getPostEndpoint,
                             body: Nothing(),
                             pathVariables: ["post_id": 1])

restingClient.perform(request).done { response in
    print("Got post: (response.body)")
}.catch { error in
    print("An error occurred: (error)")
}

Multipart form data & file upload

It is possible to perform a multipart form data request with RestingKit. The only thing to make the request is to set the Endpoint‘s encoding to .multipartFormData:

let multipartEndpoint = Endpoint<MyModel, Nothing>(.post,
                                                   "/some_resource",
                                                   encoding: .multipartFormData)

Now, each request using this endpoint will be encoded as multipart/form-data.
Uploading files is also that easy. You can use the provided MultipartFile class within your models, and magically, the file will be uploaded.

class ImageUploadModel: Encodable {
    let file: MultipartFile
    init(imageURL: URL) {
        self.file = MultipartFile(url: imageURL)
    }
}

let request = RestingRequest(endpoint: multipartEndpoint,
                             body: ImageUploadModel(url: imageUrl))
restingClient.upload(request).promise.done { _ in
    print("Success!")
}.catch {
    print("Error: ($0)")
}

Note: You should use upload methods on the RestingClient instead of perform when dealing with files and large amounts of data. perform will load the whole request body into memory,
while upload will store it into a temporary file and stream it without loading into memory.

The encoding is handled by the MultipartFormDataEncoder, which provides an interface and configuration options similar to JSONEncoder. You can customize the MultipartFormDataEncoder
used by the RestingRequestConverter:

let formDataEncoder = MultipartFormDataEncoder()
formDataEncoder.keyEncodingStrategy = .convertToSnakeCase
formDataEncoder.dateEncodingStrategy = .secondsSince1970
formDataEncoder.dataEncodingStrategy = .raw
let converter = RestingRequestConverter(multipartFormDataEncoder: formDataEncoder)

Progress handlers

RestingClient‘s upload methods returns a ProgressablePromise, which acts like classic promisses but also
accept progress handlers.

restingClient.upload(request).progress { progress in
    print("Upload (progress.fractionCompleted * 100)% completed")
}.done { response in
    print("Uploaded completed with response: (response)")
}.catch { error in
    print("An error occurred")
}

Interceptors

Interceptors allow to intercept any request and response, and modify it before the request is sent or the response processed. Some basic usages of interceptors include:

  • Logging requests and responses
  • Injecting headers
  • Retrying failed requests

To use interceptors, you will need to implement the RestingInterceptor protocol and provide your interceptor to your RestingClient.

class LogInterceptor: RestingInterceptor {
    func intercept(request: HTTPRequest, execution: Execution)
        -> ProgressablePromise<HTTPDataResponse> {
        print("sending request (request)")
        return execution(request).get { response in
            print("got response (response)")
        }
    }
}

class DeviceIdInjector: RestingInterceptor {
    func intercept(request: HTTPRequest, execution: Execution) -> ProgressablePromise<HTTPDataResponse> {
        var urlRequest = request.urlRequest
        urlRequest.setValue(UIDevice.current.identifierForVendor?.uuidString,
                           forHTTPHeaderField: "device-id")
        let request = BasicHTTPRequest(urlRequest: urlRequest, fileUrl: request.fileUrl)
        return execution(request)
    }
}

let restingClient = RestingClient(baseUrl: "https://jsonplaceholder.typicode.com",
                                  decoder: decoder,
                                  requestConverter: requestConverter,
                                  interceptors: [DeviceIdInjector(), LogInterceptor()])

The RestingClient will pass the request to the interceptors in the provided order, while the response is passed in the reverse order. Therefore, it is important to place LogInterceptor at the end of the array (otherwise, it will not be able to log the device-id header added by DeviceIdInjector).

RestingKit provides an interceptor for logging requests and responses: RequestResponseLoggingInterceptor.

Important: It is required for each interceptor to call the execution parameter, as it is what will run the next interceptors and finally the request. Unless, of course, you do not want to run additional interceptors or send the request.

Using a custom HTTPClient

HTTPClients are the classes that performs the requests. They take an HTTPRequest and return a (Progressable)Promise<HTTPDataResponse> without doing anything. AlamofireClient is the provided implementation that uses Alamofire to perform the requests and the default client used by RestingClient. You can configure a RestingClient to use your own implementation:

class MyHTTPClient: HTTPClient {
    public func perform(urlRequest: URLRequest) -> Promise<HTTPDataResponse> {
        // Handle classic request
    }
    func upload(request: HTTPRequest) -> ProgressablePromise<HTTPDataResponse> {
        // Handle uplaod request, with a progress handler
    }
}

let restingClient = RestingClient(baseUrl: "https://jsonplaceholder.typicode.com",
                                  decoder: decoder,
                                  httpClient: MyHTTPClient(),
                                  requestConverter: requestConverter)

Work in progress

As RestingKit is still new and in development, there are some missing features that needs implementation:

  • File downloads
  • Any other feature you might request!

Additionally, there might be some api-breaking changes until the project reaches full maturity.

Contributing

If you need help with getting started or have a feature request, just open up an issue. Pull requests are also welcome for bug fixes and new features.

Author

Moray Baruh

License

RestingKit is available under the MIT license. See the LICENSE file for more info.

Latest podspec

{
    "name": "RestingKit",
    "version": "0.0.2",
    "summary": "Networking made easy.",
    "description": "# RestingKitnn[![CI Status](https://img.shields.io/travis/moray95/RestingKit.svg?style=flat)](https://travis-ci.org/moray95/RestingKit)n[![Version](https://img.shields.io/cocoapods/v/RestingKit.svg?style=flat)](https://cocoapods.org/pods/RestingKit)n[![License](https://img.shields.io/cocoapods/l/RestingKit.svg?style=flat)](https://cocoapods.org/pods/RestingKit)n[![Platform](https://img.shields.io/cocoapods/p/RestingKit.svg?style=flat)](https://cocoapods.org/pods/RestingKit)nn## IntroductionnnRestingKit is a higher-level wrapper around [Alamofire](https://github.com/Alamofire/Alamofire) and [PromiseKit](https://github.com/mxcl/PromiseKit) written in Swift that allows developers to concentrate on the important stuff instead of writing boiler-plate code for their REST API.nn## Featuresnn- Configurable HTTP client (Alamofire is currently the only one provided, but you can write your own!)n- Path variable expansion powered by [GRMustache.swift](https://github.com/groue/GRMustache.swift)n- Interception (and modification) of all requests and responsesnn## Requirementsnn- iOS 10.0+nn- Swift 4.2nn## InstallationnnRestingKit is available through [CocoaPods](https://cocoapods.org). To install it, simply add the following line to your Podfile:nn```rubynpod 'RestingKit'n```nn## Example projectnnAn example project is included within the repositry. To run it, first execute `pod install`, then open `RestingKit.xcworkspace`. If you want to test file uploads with the example app, go into the `image_server` directory and run `php -S 0.0.0.0:9000 -c .`, which will start a dummy server for your uploads. The uploads will be stored in the `uploads` directory.nn## Usagenn### Basic examplenn1. Create a `RestingClient`nn```swiftnimport RestingKitnnlet requestConverter = RestingRequestConverter()nlet restingClient = RestingClient(baseUrl: "https://jsonplaceholder.typicode.com",n                                  decoder: decoder,n                                  requestConverter: requestConverter)n```nn- `RestingClient` is the core class within RestingKit that does the heavy lifting by executing the requests. It is configured to use a single base URL, so if you need to access multiple APIs, you'll need to create multiple clients.n- `RequestConverter` s transforms a `RestingRequest` to a `HTTPRequest`. `RestingRequestConverter` is the provided implementation that supports path templating.nn2. Define your models and endpointsnn   ```swiftn   struct PostCreateModel: Codable {n       let userId: Intn       let title: Stringn       let body: Stringnn       init(userId: Int, title: String, body: String) {n           self.userId = userIdn           self.title = titlen           self.body = bodyn       }n   }nn   struct PostModel: Codable {n       let id: Intn       let userId: Intn       let title: Stringn       let body: Stringn   }nn   let createPostEndpoint = Endpoint(.post,n                                                                 "/posts",n                                                                 encoding: .json)n   ```nn    An `Endpoint` is defined by the models of the request and response, the path (relative to the `RestingClient`'s `baseUrl`), the HTTP method to use and the encoding. If the request doesn't expect any content or doesn't return anything, you can use the special `Nothing` class. Ideally, we would use `Void`, but it is not possible to make it `Encodable` or `Decodable`.nn3. Create the request and make the actual callnn   ```swiftn   let postCreateModel = PostCreateModel(userId: 1,n                                         title: "Hello world",n                                         body: "Some awesome message")n   let request = RestingRequest(endpoint createPostEndpoint,n                                body: postCreateModel)n   restingClient.perform(request).done { response inn        print("Headers: \(response.headers)")n        let post = response.bodyn        print("Created post with id: \(post.id)")n   }.catch { error inn        print("An error occurred: \(error)")n   }n   ```nn   The promise will fail when the server responds with an HTTP status >299, so you don't have to handle this case.nn   And that's it!nn### Handling responses with no contentnnIf a request might provide a response that might be empty, you can create an `Endpoint` with an optional response type. That way, if the response is empty, `nil` will be returned.nn```swiftnlet createPostEndpoint = Endpoint(.post,n                                                               "/posts",n                                                               encoding: .json)nlet postCreateModel = PostCreateModel(userId: 1,n                                      title: "Hello world",n                                      body: "Some awesome message")nlet request = RestingRequest(endpoint createPostEndpoint,n                             body: postCreateModel)nrestingClient.perform(request).done { response inn    print("Headers: \(response.headers)")n    if let post = response.body {n        print("Created post with id: \(post.id)")n    } else {n        print("Empty body")n    }n}.catch { error inn    print("An error occurred: \(error)")n}n```nn**Note:** For this feature to work, the response needs to be truely empty (ie. a content-length of 0). An empty JSON object will produce a decoding error.nn### Path variablesnnThe provided `RestingRequestConverter` allows templating in paths by using [Mustache.swift](https://github.com/groue/GRMustache.swift).nn```swiftnlet getPostEndpoint = Endpoint(.get,n                                                   "/posts/{{post_id}}",n                                                   encoding: .query)nlet request = RestingRequest(endpoint: getPostEndpoint,n                             body: Nothing(),n                             pathVariables: ["post_id": 1])nnrestingClient.perform(request).done { response inn    print("Got post: \(response.body)")n}.catch { error inn    print("An error occurred: \(error)")n}n```nn### Multipart form data & file uploadnnIt is possible to perform a multipart form data request with RestingKit. The only thing to make the request is to set the `Endpoint`'s encoding to `.multipartFormData`:nn```swiftnlet multipartEndpoint = Endpoint(.post,n                                                   "/some_resource",n                                                   encoding: .multipartFormData)n```nnNow, each request using this endpoint will be encoded as `multipart/form-data`.nUploading files is also that easy. You can use the provided `MultipartFile` class within your models, and magically, the file will be uploaded.nn```swiftnclass ImageUploadModel: Encodable {n    let file: MultipartFilen    init(imageURL: URL) {n        self.file = MultipartFile(url: imageURL)n    }n}nnlet request = RestingRequest(endpoint: multipartEndpoint,n                             body: ImageUploadModel(url: imageUrl))nrestingClient.upload(request).promise.done { _ inn    print("Success!")n}.catch {n    print("Error: \($0)")n}n```nn**Note:** You should use `upload` methods on the `RestingClient` instead of `perform` when dealing with files and large amounts of data. `perform` will load the whole request body into memory,nwhile `upload` will store it into a temporary file and stream it without loading into memory.nnThe encoding is handled by the `MultipartFormDataEncoder`, which provides an interface and configuration options similar to `JSONEncoder`. You can customize the `MultipartFormDataEncoder`nused by the `RestingRequestConverter`:nn```swiftnlet formDataEncoder = MultipartFormDataEncoder()nformDataEncoder.keyEncodingStrategy = .convertToSnakeCasenformDataEncoder.dateEncodingStrategy = .secondsSince1970nformDataEncoder.dataEncodingStrategy = .rawnlet converter = RestingRequestConverter(multipartFormDataEncoder: formDataEncoder)n```nn### Progress handlersnn`RestingClient`'s `upload`u00a0methods returns a `ProgressablePromise`, which acts like classic promisses but alsonaccept progress handlers.nn```swiftnrestingClient.upload(request).progress { progress inn    print("Upload \(progress.fractionCompleted * 100)% completed")n}.done { response inn    print("Uploaded completed with response: \(response)")n}.catch { error inn    print("An error occurred")n}n```nn### InterceptorsnnInterceptors allow to intercept any request and response, and modify it before the request is sent or the response processed. Some basic usages of interceptors include:nn- Logging requests and responsesn- Injecting headersn- Retrying failed requestsnnTo use interceptors, you will need to implement the `RestingInterceptor` protocol and provide your interceptor to your `RestingClient`.nn```swiftnclass LogInterceptor: RestingInterceptor {n    func intercept(request: HTTPRequest, execution: Execution)n        -> ProgressablePromise {n        print("sending request \(request)")n        return execution(request).get { response inn            print("got response \(response)")n        }n    }n}nnclass DeviceIdInjector: RestingInterceptor {n    func intercept(request: HTTPRequest, execution: Execution) -> ProgressablePromise {n        var urlRequest = request.urlRequestn        urlRequest.setValue(UIDevice.current.identifierForVendor?.uuidString,n                           forHTTPHeaderField: "device-id")n        let request = BasicHTTPRequest(urlRequest: urlRequest, fileUrl: request.fileUrl)n        return execution(request)n    }n}nnlet restingClient = RestingClient(baseUrl: "https://jsonplaceholder.typicode.com",n                                  decoder: decoder,n                                  requestConverter: requestConverter,n                                  interceptors: [DeviceIdInjector(), LogInterceptor()])n```nnThe `RestingClient` will pass the request to the interceptors in the provided order, while the response is passed in the reverse order. Therefore, it is important to place `LogInterceptor` at the end of the array (otherwise, it will not be able to log the `device-id` header added by `DeviceIdInjector`).nnRestingKit provides an interceptor for logging requests and responses: `RequestResponseLoggingInterceptor`.nn**Important**: It is required for each interceptor to call the `execution` parameter, as it is what will run the next interceptors and finally the request. Unless, of course, you do not want to run additional interceptors or send the request.nn### Using a custom HTTPClientnn`HTTPClient`s are the classes that performs the requests. They take an `HTTPRequest` and return a `(Progressable)Promise` without doing anything. `AlamofireClient` is the provided implementation that uses Alamofire to perform the requests and the default client used by `RestingClient`. You can configure a `RestingClient` to use your own implementation:nn```swiftnclass MyHTTPClient: HTTPClient {n    public func perform(urlRequest: URLRequest) -> Promise {n        // Handle classic requestn    }n    func upload(request: HTTPRequest) -> ProgressablePromise {n        // Handle uplaod request, with a progress handlern    }n}nnlet restingClient = RestingClient(baseUrl: "https://jsonplaceholder.typicode.com",n                                  decoder: decoder,n                                  httpClient: MyHTTPClient(),n                                  requestConverter: requestConverter)n```nn## Work in progressnnAs RestingKit is still new and in development, there are some missing features that needs implementation:nn- File downloadsn- Any other feature you might request!nnAdditionally, there might be some api-breaking changes until the project reaches full maturity.nn## ContributingnnIf you need help with getting started or have a feature request, just open up an issue. Pull requests are also welcome for bug fixes and new features.nn## AuthornnMoray Baruhnn## LicensennRestingKit is available under the MIT license. See the LICENSE file for more info.",
    "homepage": "https://github.com/moray95/RestingKit",
    "license": {
        "type": "MIT",
        "file": "LICENSE"
    },
    "authors": {
        "Moray Baruh": "[email protected]"
    },
    "source": {
        "git": "https://github.com/moray95/RestingKit.git",
        "tag": "0.0.2"
    },
    "platforms": {
        "ios": "10.0"
    },
    "source_files": [
        "RestingKit/*.swift",
        "RestingKit/**/*.swift"
    ],
    "dependencies": {
        "Alamofire": [
            "~> 4.8"
        ],
        "GRMustache.swift4": [
            "~> 3.0"
        ],
        "PromiseKit": [
            "~> 6.8"
        ]
    }
}

Pin It on Pinterest

Share This