Decree

4.3.0

Framework for making Declarative HTTP Requests
drewag/Decree

What's New

Swift 5.1, KeyValueEncoder customization, and base URL override

2019-10-30T18:20:03Z
  • Support Swift 5.1 and Xcode 11.
  • Allow customizing the date and array encoding strategy for URL query, Form URL Encoded, and Form Data inputs.
  • Allow overriding a particular endpoint's base URL

Also, we removed support for CocoaPods because of the effort involved in its maintenance and the ease of using Swift Package Manager moving forward, even for app projects.

Decree - Declarative HTTP Requests

Swift platforms Swift Package Manager compatible CocoaPods Compatible MIT Build Status

Twitter @drewag Blog drewag.me

Make HTTP requests in a clear and type safe way by declaring web services and endpoints on iOS, macOS, and Linux

When it comes to making URL requests with Swift, you largely have two options: use the URLSession APIs in Foundation or use some heavy handed framework.

This framework is designed to be light-weight while remaining customizable and focusing on declaring the interface to an API in a declarative manner. Once declared, making requests to the various endpoints is very straight-forward and type safe. It works on iOS, macOS, and Linux.

Andrew developed this strategy through the implementation of many different apps and backend services written in Swift. He's used this paradigm for communicating between his own front and back-ends (both implemented in Swift) as well as to services such as Spotify, FreshDesk, Stripe, and more.

We offer a separate repository DecreeServices with service declarations for popular services

Table of Contents

Features

Four types of Endpoints

These protocols declare if an endpoint has input and/or output.

  • EmptyEndpoint (no input or output)
  • InEndpoint (only input)
  • OutEndpoint (only output)
  • InOutEndpoint (input and output)

Five Input formats

These formats are used to encode the endpoint's input using the Swift Encodable protocol.

  • JSON
  • URL Query
  • Form URL Encoded
  • Form Data Encoded
  • XML

Two Output formats

These formats are used to initialize the endpoint's output using the Swift Decodable protocol.

  • JSON
  • XML

Three types of Authorization

Allows setting authorization to be used for all endpoints in a web service. Each endpoint can then specify an authorization requirement.

  • Basic
  • Bearer
  • Custom - Custom HTTP header key and value

Advanced Functionality

  • Download result to a file for to save memory on large requests.
  • Optionally get progress updates through an onProgress handler

Configurable

You can optionally perform advanced configuration to the processing of a request and response.

  • Customize URLRequest (e.g. custom headers)
  • Customize JSON encoders and decoders
  • Custom response validation
  • Custom error response format
  • Custom standard response format

Virtually 100% Code Coverage

The vast majority of our code is covered by unit tests to ensure reliability.

Request and Response Logging

Thorough Error Reporting

The errors thrown and returned by Decree are designed to be user friendly while also exposing detailed diagnostic information.

Mocking

Allows mocking endpoint responses for easy automated testing.

Third-Party Services

We created a separate framework that defines services and endpoints for several third-party services. Check it out at DecreeServices.

Examples

Here are a few examples of how this framework is used.

Simple Get

Here we define a CheckStatus endpoint that is a GET (the default) with no input or output that exists at the path “/status”. Scroll down to see the definition of ExampleService.

struct CheckStatus: EmptyEndpoint {
    typealias Service = ExampleService

    let path = "status"
}

We can then use that definition to make asynchronous requests.

CheckStatus().makeRequest() { result in
    switch result {
    case .success:
        print("Success :)")
    case .failure(let error):
        print("Error :( \(error)")
    }
}

We can also make synchronous requests that simply throw an error if an error occurs.

try CheckStatus().makeSynchronousRequest()

Input and Output

We can also define endpoints that have input and/or output. Here, we define a Login endpoint that is a POST to “/login” with username and password parameters encoded as JSON. If successful, the endpoint is expected to return a token.

struct Login: InOutEndpoint {
    typealias Service = ExampleService
    static let method = Method.post

    let path = "login"

    struct Input: Encodable {
        let username: String
        let password: String
    }

    struct Output: Decodable {
        let token: String
    }
}

Then we can make an asynchronous request.

Login().makeRequest(with: .init(username: "username", password: "secret")) { result in
    switch result {
    case .success(let output):
        print("Token: \(output.token)")
    case .failure(let error):
        print("Error :( \(error)")
    }
}

Or we can make a synchronous requests that returns the output if successful and throws otherwise.

let token = try Login().makeSynchronousRequest(with: .init(username: "username", password: "secret")).token

Download

For endpoints with larger output, we can download them directly to a file instead of holding the whole response in memory.

struct GetDocument: OutEndpoint {
    typealias Service = ExampleService
    typealias Output = Data

	let id: Int

    var path: String {
	    return "documents/\(id)"
    }
}

GetDocument(id: 42).makeDownloadRequest() { result in
    switch result {
    case .success(let url):
        // open or move the url
    case .failure(let error):
        print("Error :( \(error)")
    }
}

Note, you use a the makeDownloadRequest method on any endpoint with output (regardless of it's format), but it often makes most sense with raw data output.

The Service Definition

The only extra code necessary to make the above examples work, is to define the ExampleService:

struct ExampleService: WebService {
    // There is no service wide standard response format
    typealias BasicResponse = NoBasicResponse

    // Errors will be in the format {"message": "<reason>"}
    struct ErrorResponse: AnyErrorResponse {
        let message: String
    }

    // Requests should use this service instance by default
    static var shared = ExampleService()

    // All requests will be sent to their endpoint at "https://example.com"
    let baseURL = URL(string: "https://example.com")!
}

Here we define a WebService called ExampleService with the a few properties.

That's all you need. You can then define as many endpoints as you like and use them in a clear and type safe way.

Real World Examples

To see real world examples, check out how we declared services in DecreeServices.

Hopeful Features

Features we are hoping to implement are added as issues with the enhancement tag. If you have any feature requests, please don't hesitate to create a new issue.

Contributing

It is very much encouraged for you to report any issues and/or make pull requests for new functionality.

Description

  • Swift Tools 5.0.0
View More Packages from this Author

Dependencies

Last updated: Tue Mar 19 2024 07:07:08 GMT-0900 (Hawaii-Aleutian Daylight Time)