Swiftpack.co - Package - sviatoslav/EndpointProcedure


CI Status Swift License Coverage

EndpointProcedure is flexible typesafe bridge between network and application model. It's based on ProcedureKit framework.

EndpointProcedure itself does not perform any requests and does not parse the response, it chains procedures and transfers output of one procedure to the next one and uses output of last procedure as own output.

Data flow looks as follows:

Loading -> Validation -> Deserialization -> Interception -> Mapping

  • Loading: loads HTTPResponseData from any source.
  • Validation: validates HTTPResponseData
  • Deserialization: converts loaded Data to Any
  • Interception: converts deserialized object to format expected by mapping
  • Mapping: Converts Any to Result


Basic usage

The easiest way to create instance of EndpointProcedure is to creates type that conforms to EndpointProcedureFactory protocol. This example will show how to use EndpointProcedure for loading list of Star Wars films from Star Wars API.

Let's start from defining struct Film.

import PlaygroundSupport
import Foundation
struct Film {
    let title: String
    let director: String
    let producer: String
    let characters: [URL]

For EndpointProcedure creation we should create new type that conforms to EndpointProcedureFactory protocol and implement createOrThrow(with:) method.

cereateOrThrow(with:) method requires one input parameter of type ConfigurationProtocol. EndpointProcedure.framework contains struct Configuration which conforms to ConfigurationProtocol. Initializer of Configuration type has 3 input parameters: HTTPRequestProcedureFactory, DataDeserializationProcedureFactory and ResponseMappingProcedureFactory.

We'll use AlamofireProcedureFactory for data loading.

let loadingFactory = AlamofireProcedureFactory()

For response mapping we'll use DecodingProcedureFactory with JSONDecoder. DecodingProcedureFactory requires output type to conform Decodable protocol.

extension Film: Decodable {}
let decodingFactory = DecodingProcedureFactory(decoder: JSONDecoder())

The aim of data deserialization procedure is converting data loaded by loading procedure into format expected by response mapping procedure. DecodingProcedure works with plain data, so deserialization should simply return output of data loading procedure.

let deseriazationFactory = AnyDataDeserializationProcedureFactory(syncDeserialization: {$0})

If structure of input expected by mapping procedure is not the same as structure of deserialization procedure's output, we should implement interception procedure. In our case, array of films is not root of the response json. It's under "results" key. For such cases DecodingProcedure accepts input of type NestedData.

NestedData contains two values codingPath: [CodingKey] and data: Data

let interceptionProcedure = TransformProcedure<Any, Any> {
    guard let data = $0 as? Data else { throw ProcedureKitError.requirementNotSatisfied() }
    let codingPath: [AnyCodingKey] = ["results"]
    return NestedData(codingPath: codingPath, data: data)
let config = Configuration(dataLoadingProcedureFactory: loadingFactory,
                           dataDeserializationProcedureFactory: deseriazationFactory,
                           responseMappingProcedureFactory: decodingFactory)

struct FilmsEndpointProcedureFactory: EndpointProcedureFactory {
    func createOrThrow(with configuration: ConfigurationProtocol) throws -> EndpointProcedure<[Film]> {
        let url = URL(string: "https://swapi.co/api/films")!
        let data = HTTPRequestData.Builder.for(url).build()
        return EndpointProcedure(requestData: data,
                                 interceptionProcedure: interceptionProcedure,
                                 configuration: configuration)

let procedure = FilmsEndpointProcedureFactory().create(with: config)
procedure.addDidFinishBlockObserver { procedure, _ in
    switch procedure.output {
    case .pending: print("No result after finishing")
    case .ready(.success(let films)): print((["Star Wars Films:"] + films.map({ $0.title })).joined(separator: "\n"))
    case .ready(.failure(let error)): print("Error: \(error)")
ProcedureQueue.main.add(operation: procedure)
PlaygroundPage.current.needsIndefiniteExecution = true

The output is:

Star Wars Films:
A New Hope
Attack of the Clones
The Phantom Menace
Revenge of the Sith
Return of the Jedi
The Empire Strikes Back
The Force Awakens
Using DefaultConfigurationProvider and HTTPRequestDataBuidlerProvider

Usually our connections to backend endpoints have common base URL, requests creation behaviour, response format and response parsing.

This examle will show how to avoid a boilerplate during implementation of multiple endpoint procedures. We'll create procerures for character and vehicle loading.

Let's start from inheriting EndpointProcedureFactory protocol.

protocol SWProcedureFactory: EndpointProcedureFactory, DefaultConfigurationProvider, HTTPRequestDataBuidlerProvider,
                            BaseURLProvider {}

All our procedures will use Alamofire for data loading and JSONDecoder for response mapping.

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
import Foundation
private enum SWProcedureFactoryStorage {
    static let configuration = Configuration(dataLoadingProcedureFactory: AlamofireProcedureFactory(),
                                             dataDeserializationProcedureFactory: AnyDataDeserializationProcedureFactory { $0 },
                                             responseMappingProcedureFactory: DecodingProcedureFactory(decoder: JSONDecoder()))

extension SWProcedureFactory {
    var defaultConfiguration: ConfigurationProtocol {
        return SWProcedureFactoryStorage.configuration

All our requests will have same base URL.

extension SWProcedureFactory {
    var baseURL: URL {
        return URL(string: "https://swapi.co/api/")!

Implementation of CharacterProcedureFactory will look as follows:

struct Character {
    let name: String
    let vehicles: [URL]

extension Character: Decodable {}

struct CharacterProcedureFactory: SWProcedureFactory {
    let id: Int
    func createOrThrow(with configuration: ConfigurationProtocol) throws -> EndpointProcedure<Character> {
        return try EndpointProcedure(requestData: self.builder(for: "people/\(self.id)").build(),
                                     configuration: configuration)

Conformance to DefaltConfigurationProvider allows us to call create method without arguments.

var loadedCharacter: Character? = nil
let skywalkerProcedure = CharacterProcedureFactory(id: 1).create()
skywalkerProcedure.addDidFinishBlockObserver { procedure, _ in
    switch procedure.output {
    case .pending: print("No result after finishing")
    case .ready(.success(let character)):
        loadedCharacter = character
        print("Character name: \(character.name)")
    case .ready(.failure(let error)): print("Error: \(error)")
ProcedureQueue.main.add(operation: skywalkerProcedure)

Output of code above:

Character name: Luke Skywalker

Let's load Sand Crawler vehicle record

struct Vehicle {
    let name: String
    let model: String
extension Vehicle: Decodable {}

struct VehicleProcedureFactory: SWProcedureFactory {
    let id: Int
    func createOrThrow(with configuration: ConfigurationProtocol) throws -> EndpointProcedure<Vehicle> {
        return try EndpointProcedure(requestData: self.builder(for: "vehicles/\(self.id)").build(),
                                     configuration: configuration)

let vehicleProcedure = VehicleProcedureFactory(id: 4).create()
vehicleProcedure.addDidFinishBlockObserver { procedure, _ in
    switch procedure.output {
    case .pending: print("No result after finishing")
    case .ready(.success(let vehicle)):
        print("Vehicle name: \(vehicle.name), model: \(vehicle.model)")
    case .ready(.failure(let error)): print("Error: \(error)")
ProcedureQueue.main.add(operation: vehicleProcedure)


Vehicle name: Sand Crawler, model: Digger Crawler


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


Stars: 0
Help us keep the lights on

Used By

Total: 0