Skip to content
This repository has been archived by the owner on Nov 16, 2022. It is now read-only.
/ Forest Public archive

Declarative REST API request construction for iOS and macOS

License

Notifications You must be signed in to change notification settings

CozmoNate/Forest

Repository files navigation

Forest Client

License Language Version Build Status Coverage Status

Forest client is a flexible and extensible RESTful API client framework built on top of URLSession and URLSessionTask. It already includes network object mappers from JSON to the most commonly used data types. Because of its simple data encoding/decoding approach and extensible architecture you can easily add your custom network object mappers. Forest provides all of the features needed to build robust client for your backend services.

You could ask why not to get any other proven networking framework? Sure, you’ll get one best suitable for your needs and style preference, but it's always good to have an options. Following is the list of features I wanted from higher level networking layer and implemented in Forest client:

  • Declarative request and response configuration
  • Transparent and completely customizable response interception and rewind/retry, allowing async handling. Mainly to map errors and catch expired tokens in case of OAuth-like secured request
  • Extensibility. Easily add custom response mappers and body data encoders using closures, or protocols, or subclassing whichever will be more convenient for specific need. Use this with default framework’s classes or extend them or subclass, but it should be easy.
  • Download and keep files where I need them
  • Deserialization of JSON body. Map JSON to array, or dictionary, or by using objects conforming to Decodable protocol
  • Serialization and deserialization of Protobufs messages (gRPC over HTTP)
  • Multipart form data support as a bonus

Installation

CocoaPods

pod 'Forest'

Add Protobufs supporting extensions:

pod 'Forest/Protobuf'

To use reachability service:

pod 'Forest/Reachability'

And don't forget to import the framework:

import Forest

Manually

Just put the files from Core and Protobuf directories somethere in your project. To use Protobuf extensions you need additionally integrate SwiftProtobuf framework into your project.

Usage

The core class which handles network task is ServiceTask. ServiceTask includes factory methods helping to configure request and response params and handlers. If you need more control over the process of making request and handling the response, you can use delegation and implement ServiceTaskRetrofitting protocol and modify task behavior via retrofitter. Also you can subclass ServiceTask, it is built for that.

Make a GET request expecting json response

ServiceTask()
    .url("https://host.com/path/to/endpoint")
    .method(.GET)
    .query(["param": value])
    // Expecting valid JSON response
    .json { (object, response) in
        print("JSON response received: \(object)")
    }
    .error { (error, response) in
        print("Error occurred: \(error)")
    }
    .perform()

Sending and receiving data

Send and receive data using objects conforming to Codable protocol:

struct NameRequest: Encodable {
    let name: String
}

struct NameResponse: Decodable {
    let isValid: Bool
}

ServiceTask()
    // Set base url and HTTP method
    .endpoint(.POST, "https://host.com")
    // Add path to resource
    .path("/path/to/resource")
    // Serialize our Codable struct and set body
    .body(codable: NameRequest("some"))
    // Expect response with the object of 'NameResponse' type
    .codable { (object: NameResponse, response) in
        print("Name valid: \(object.isValid)")
    }
    // Otherwise will fail with error
    .error { (error, response) in
        print("Error occured: \(error)")
    }
    .perform()

Just download some file:

ServiceTask()
    .headers(["Authorization": "Bearer \(token)"])
    .method(.PUT)
    .url("https://host.com/file/12345")
    .body(text: "123456789")
    .file { (url, response) in
        print("Downloaded: \(url)")
        // Remove temp file
        try? FileManager.default.removeItem(at: url)
    }
    .error { (error, response) in
        print("Error occured: \(error)")
    }
    // When download destination not provided, content will be downloaded and saved to temp file
    .download()

Upload multipart form data encoded content:

do {

    // Create new form data builder
    var formDataBuilder = FormDataBuilder()

    // Filename and MIME type will be obtained automatically from URL. It can be provided explicitly too
    formDataBuilder.append(.file(name: "image", url: *url*))
    
    // Generate form data in memory. It also can be written directly to disk or stream using encode(to:) method 
    let formData = try formDataBuilder.encode()
    
    ServiceTask()
            .endpoint(.POST, "https://host.com/upload")
            .body(data: formData, contentType: formDataBuilder.contentType)
            .response(content: { (response) in
                switch response {
                case .success:
                    print("Done!")
                case .failure(let error):
                    print("Failed to upload: \(error)")
                }
            })
            .perform()
}
catch {
    print("\(error))
    return
}

Send and receive Protobuf messages (gRPC over HTTP):

ServiceTask()
    .endpoint(.POST, "https://host.com")
    // Create and configure request message in place
    .body { (message: inout Google_Protobuf_StringValue) in
        message.value = "something"
    }
    // Expecting Google_Protobuf_Empty message response
    .proto{ (message: Google_Protobuf_Empty, response) in
        print("Done!")
    }
    .error { (error, response) in
        print("Error occured: \(error)")
    }
    .perform()

// Or another version of the code above with explicitly provided types
ServiceTask()
    .endpoint(.POST, "https://host.com")
    // Create and configure request message in place
    .body(proto: Google_Protobuf_SourceContext.self) { (message) in
        message.fileName = "file.name"
    }
    // Expecting Google_Protobuf_Empty message response
    .response(proto: Google_Protobuf_Empty.self) { (response) in
        switch response {
        case .success(let message):
            print("Done!")
        case .failure(let error):
            print("Error occured: \(error)")
        }
    }
    .perform()

Author

Natan Zalkin natan.zalkin@me.com

License

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