ComposableRequest

5.3.2

A Swift library to abstract API clients.
sbertix/ComposableRequest

What's New

v5.3.2

2023-01-21T22:04:02Z

Bug fixes

  • fix an issue with delay not being applied correctly

Header


Swift codecov
iOS macOS tvOS watchOS


ComposableRequest is a networking layer based on a declarative interface, written in (modern) Swift.

It abstracts away URLSession implementation, in order to provide concise and powerful endpoint representations, thanks to the power of Combine Publishers.

It comes with Storage (inside of ComposableStorage), a way of caching Storable items, and related concrete implementations (e.g. UserDefaultsStorage, KeychainStorage – for which you're gonna need to add ComposableStorageCrypto, depending on Swiftchain, together with the ability to provide the final user of your API wrapper to inject code through Providers.

Status

push GitHub release (latest by date)

You can find all changelogs directly under every release.

What's next?

ComposableRequest was initially Swiftagram's networking layer and it still tends to follow roughly the same development cycle.

Milestones, issues, are the best way to keep updated with active developement.

Feel free to contribute by sending a pull request. Just remember to refer to our guidelines and Code of Conduct beforehand.

Installation

Swift Package Manager (Xcode 11 and above)

  1. Select File/Swift Packages/Add Package Dependency… from the menu.
  2. Paste https://github.com/sbertix/ComposableRequest.git.
  3. Follow the steps.
  4. Add ComposableStorage together with ComposableRequest for the full experience.

Why not CocoaPods, or Carthage, or blank?

Supporting multiple dependency managers makes maintaining a library exponentially more complicated and time consuming.
Furthermore, with the integration of the Swift Package Manager in Xcode 11 and greater, we expect the need for alternative solutions to fade quickly.

Targets

  • ComposableRequest, an HTTP client originally integrated in Swiftagram, the core library.
  • ComposableStorage, depending on KeychainAccess, can be imported together with ComposableRequest to extend its functionality.

Usage

Check out Swiftagram or visit the (auto-generated) documentation for ComposableRequest, ComposableStorage and ComposableStorageCrypto to learn about use cases.

Endpoint

As an implementation example, we can display some code related to the Instagram endpoint tasked with deleting a post.

/// A `module`-like `enum`.
public enum MediaEndpoint {
    /// Delete one of your own posts, matching `identifier`.
    /// Checkout https://github.com/sbertix/Swiftagram for more info.
    ///
    /// - parameter identifier: String
    /// - returns: A locked `AnyObservable`, waiting for authentication `HTTPCookie`s.
    public func delete(_ identifier: String) -> LockSessionProvider<[HTTPCookie], AnyPublisher<Bool, Error>> {
        // Wait for user defined values.
        LockSessionProvider { cookies, session in
            // Defer it so it only resumes when observed.
            Deferred {
                // Fetch first info about the post to learn if it's a video or picture
                // as they have slightly different endpoints for deletion.
                Request("https://i.instagram.com/api/v1/media")
                    .path(appending: identifier)
                    .info   // Equal to `.path(appending: "info")`.
                    // Wait for the user to `inject` an array of `HTTPCookie`s.
                    // You should implement your own `model` to abstract away
                    // authentication cookies, but as this is just an example
                    // we leave it to you.
                    .header(appending: HTTPCookie.requestHeaderFields(with: cookies))
                    // Create the `Publisher`.
                    .publish(with: session)
                    // Check it returned a valid media.
                    .map(\.data)
                    // Decode it inside a `Wrapper`, allowing to interrogate JSON
                    // representations of object without knowing them in advance.
                    // (It's literally the missing `AnyCodable`).
                    .wrap()
                    // Prepare the new request.
                    .flatMap { wrapper -> AnyPublisher<Bool, Error> in
                        guard let type = wrapper["items"][0].mediaType.int(),
                              [1, 2, 8].contains(type) else {
                            return Just(false).setFailureType(to: Failure.self).eraseToAnyPublisher()
                        }
                        // Actually delete it now that we have all data.
                        return Request("https://i.instagram.com/api/v1/media")
                            .path(appending: identifier)
                            .path(appending: "delete/")
                            .query(appending: type == 2 ? "VIDEO" : "PHOTO", forKey: "media_type")
                            // This will be applied exactly as before, but you can add whaterver
                            // you need to it, as it will only affect this `Request`.
                            .header(appending: HTTPCookie.requestHeaderFields(with: cookies))
                            // Create the `Publisher`.
                            .publish(with: session)
                            .map(\.data)
                            .wrap()
                            .map { $0.status == "ok" }
                    }
            }
            // Make sure it's observed from the main thread.
            .receive(on: .main)
            .eraseToAnyPublisher()
        }
    }
}

How can the user then retreieve the information?

All the user has to do is…

/// A valid post identifier.
let identifier: String = /* a valid String */
/// A valid array of cookies.
let cookies: [HTTPCookie] = /* an array of HTTPCookies */
/// A *retained* collection of `AnyCancellable`s.
var bin: Set<AnyCancellable> = []

/// Delete it.
MediaEndpoint.delete(identifier)
    .unlock(with: cookies)
    .session(.shared)
    .sink(receiveCompletion: { _ in }, receiveValue: { print($0) })
    .store(in: &bin)

Resume and cancel requests

What about cancelling the request, or starting it a later date?

As ComposableRequest is based on the Combine runtime, you can simply cancel the Cancellable returned on sink, or emptying the "dispose bag"-like Set you've stored it in.

Caching

Caching of Storables is provided through conformance to the Storage protocol, specifically by implementing either ThrowingStorage or NonThrowingStorage.

The library comes with several concrete implementations.

  • TransientStorage should be used when no caching is necessary, and it's what Authenticators default to when no Storage is provided.
  • UserDefaultsStorage allows for faster, out-of-the-box, testing, although it's not recommended for production as private cookies are not encrypted.
  • KeychainStorage, requiring you to add ComposableStorageCrypto, (preferred) stores them safely in the user's keychain.

Description

  • Swift Tools 5.2.0
View More Packages from this Author

Dependencies

Last updated: Thu Apr 11 2024 10:41:18 GMT-0900 (Hawaii-Aleutian Daylight Time)