A lightweight, event-driven architectural framework

Overview

Trellis

Swift Xcode MIT

Trellis features a declarative DSL that simplifies service bootstrapping:

let cluster = try await Bootstrap {
    Group {
        Store(model: IdentityModel.self)
            .mutate(on: IdentityAction.self) { model, action, send in
                // ...
            }
            .mutate(on: StartUpAction.self) { model, action, send in
                // ...
            }
            .with(model: identityModel)
        Store(model: ArticlesModel.self)
            .mutate(on: ArticlesAction.self) { model, action, send in
                // ...
            }
            .with(model: articlesModel)
    }
    .emit(using: notificationsStream)
    .transformError {
        ErrorAction.error($0)
    }
    .observe(on: IdentityAction.self) {
        // ...
    }
}

This sets up two services managing the identity of the user and his articles. The resulting cluster exposes only one function, send, which can be used to interact with the services without explicitly know which service handles which action.

try await cluster.send(action: StartUpAction.appDidCompleteLaunching)

Most of the time we won't declare services like this. Instead, we'd write a custom service wrapping each store:

// IdentityService.swift
struct IdentityService: Service {
    var body: some Service {
        Store(model: IdentityModel.self)
            .mutate(on: IdentityAction.self) { model, action, send in
                // ...
            }
            .mutate(on: StartUpAction.self) { model, action, send in
                // ...
            }
    }

// SomeOtherFile.swift
let cluster = try await Bootstrap {
    IdentityService()
        .with(model: identityModel)
}

Notice how the actual model is injected from outside the service, enabling dependency injection.

Index

Installation

Using Swift Package Manager:

.package(name: "Trellis",
         url: "https://github.com/valentinradu/Trellis.git",
         .upToNextMinor(from: "0.3.0-beta"))

Getting started

Actions and services

Services are entities that react to actions. They form a tree-like structure that allows each parent service to delegate actions to its children. Most of the entities in Trellis are services.

Modifiers

Modifiers change the behavior of a service. Most modifiers, like .serial() will traverse the service tree and apply to all sub-services under it, while some, like .mutate(on:) only make sense when applied to the service immediately under it. For more info about modifiers check the appropriate section below.

Groups

Groups are inert services that pass actions to their children without taking any other additional steps. They're mostly used to apply a modifier (e.g. emit(using:consumeAtBootstrap:)) to multiple services or to bypass the number of maximum sub-services (8) a service can have.

Stores

Each store encapsulates a model, which in turn, handles a set of tasks (and their associated data) that go together well. Stores allow you to use and mutate the wrapped model each time an action is sent to the cluster.

Modifiers

.emit(using:consumeAtBootstrap:) - Takes an external source of events (async stream) that outputs actions and feeds them to all services under it. When setting `

.transformError(transformHandler:) - Turns all errors originating from services under it into actions and feeds them back into the cluster. If the transformed error throws again, the operation will fail and the send(action:) function with throw.

.concurrent() - Executes all services under it in a concurrent fashion. This is the default.

.serial() - Executes all services under it one after the other. Ideal for cases where you want to something, like the identity of the user, before allowing other services to process the action.

.bootstrap(bootstrapHandler:) - Called right after service creation, it gives services the possibility to initialize state or bootstrap models before handling any actions.

.observe(observeHandler:) - Called each time an action is received. Ideal for logging and updating external (e.g. presentation layer) state.

.mutate(on:mutateHandler:) - Called each time an action is received. Inside the handler you can mutate the model depending on the received action and send other actions to further processing.

.with(model:) - Sets the model for all sub-services under it.

Concurrency

Trellis uses the Swift concurrency model and guarantees that the services will be always built and bootstrapped on the main thread. There is on other guarantee, and for this reason, all models should be actors.

Testing

With Trellis, unit testing is mostly focused around the models. However, if you wish to also test the service integration, it's easy to do so. You can simply replace the model with a mocked version and the cluster send function with one that records actions instead:

// SomeTest.swift
let cluster = try await Bootstrap {
    IdentityService()
        .with(model: mockedIdentityModel)
        .environment(\.send, recordingSend)
}

try await cluster.send(action: StartUpAction.appDidCompleteLaunching)
// Assert the state of the mocked identity model and the recorded actions

License

MIT License

You might also like...
Simple, lightweight swift bindings

Bindy Just a simple bindings. Installation Add pod 'Bindy' to your podfile, and run pod install SPM is supported too. Usage For now, Bindy has a coupl

A lightweight Elm-like Store for SwiftUI

ObservableStore A simple Elm-like Store for SwiftUI, based on ObservableObject. ObservableStore helps you craft more reliable apps by centralizing all

Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.
Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

Reactive extensions to Cocoa frameworks, built on top of ReactiveSwift. ⚠️ Looking for the Objective-C API? 🎉 Migrating from RAC 4.x? 🚄 Release Road

An observables framework for Swift
An observables framework for Swift

🐌 snail A lightweight observables framework, also available in Kotlin Installation Carthage You can install Carthage with Homebrew using the followin

Two-way data binding framework for iOS. Only one API to learn.

BindKit A simple to use two-way data binding framework for iOS. Only one API to learn. Supports Objective-C, Swift 5, Xcode 10.2, iOS 8 and above. Shi

A Swift framework for reactive programming.

CwlSignal An implementation of reactive programming. For details, see the article on Cocoa with Love, CwlSignal, a library for reactive programming. N

Open source implementation of Apple's Combine framework for processing values over time.
Open source implementation of Apple's Combine framework for processing values over time.

OpenCombine Open-source implementation of Apple's Combine framework for processing values over time. The main goal of this project is to provide a com

This Repository holds learning data on Combine Framework

Combine Framework List of Topics Welcome, every section in this repo contains a collection of exercises demonstrating combine's utilization as well as

Binding - Data binding framework (view model binding on MVVM) written using propertyWrapper and resultBuilder

Binding Data binding framework (view model binding on MVVM) written using @prope

Releases(v0.3.0-beta)
  • v0.3.0-beta(Apr 17, 2022)

    • added @EnvironmentObject for passing dependencies through the service tree without explicitly name them
    • added Store, a better way to handle stateful services
    • added .mutate(on:mutateHandler:), used to mutate the Store
    • added .with(model:), used to inject a model into the Store
    • added .observe(observeHandler:), used to observe any action that passes through the service tree
    Source code(tar.gz)
    Source code(zip)
  • v0.2.1(Apr 12, 2022)

    • added emitters for external events that need to be consumed by the cluster
    • added state observers allowing reducer's state to be immutable
    Source code(tar.gz)
    Source code(zip)
  • v0.2.0-beta(Apr 8, 2022)

    • new DSL for easier service creation
    • new dependency injection mechanism via the environment
    • serial and concurrent execution for multiple services
    Source code(tar.gz)
    Source code(zip)
  • v0.1.0(Mar 27, 2022)

  • v0.0.1-beta(Nov 28, 2021)

Owner
Valentin Radu
Valentin Radu
Aftermath is a stateless message-driven micro-framework in Swift

Aftermath is a stateless message-driven micro-framework in Swift, which is based on the concept of the unidirectional data flow architecture.

HyperRedink 70 Dec 24, 2021
iOS app for open event

CircleCI Code Quality Chat Open Event iOS iOS app for Open Event Introduction This is an iOS app developed for FOSSASIA in mind. The Open Event Projec

FOSSASIA 1.6k Jan 5, 2023
Open Event Orga iOS App

Open Event Organizer iOS App Event management app for organizers using Open Event Platform Roadmap Make the app functionality and UI/UX similar to the

FOSSASIA 1.5k Dec 10, 2022
MVVM + FLUX iOS Instagram client in Swift, eliminates Massive View Controller in unidirectional event/state flow manner

CZInstagram MVVM + FLUX iOS Instagram client in Swift, eliminates Massive View Controller in unidirectional event/state flow manner. Unidirectional Da

Cheng Zhang 56 Nov 1, 2022
A Swift event bus for UIWebView/WKWebView and JS.

An event bus for sending messages between UIWebView/WKWebView and embedded JS. Made with pure Swift. Features Easy, fast and reliable event bus system

Coshx 149 Oct 9, 2022
🎧 Protocol driven object observation

Listenable Swift object that provides an observable platform for multiple listeners. Requirements iOS 9.0+ Xcode 9.x+ Swift 4 Installation Listenable

Merrick Sapsford 9 Nov 30, 2022
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

About Architecture concerns RxReduce Installation The key principles How to use RxReduce Tools and dependencies Travis CI Frameworks Platform Licence

RxSwift Community 125 Jan 29, 2022
Simple and lightweight Functional Reactive Coding in Swift for the rest of us

The simplest Observable<T> implementation for Functional Reactive Programming you will ever find. This library does not use the term FRP (Functional R

Jens Ravens 1.1k Jan 3, 2023
Lightweight observations and bindings in Swift

What is Hanson? Hanson is a simple, lightweight library to observe and bind values in Swift. It's been developed to support the MVVM architecture in o

Blendle 526 Oct 18, 2022
📬 A lightweight implementation of an observable sequence that you can subscribe to.

Features Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minima

Felix M. 133 Aug 17, 2022