{"id":13872054,"url":"https://github.com/valentinradu/Trellis","last_synced_at":"2025-07-16T01:32:40.184Z","repository":{"id":42571311,"uuid":"418231062","full_name":"valentinradu/Trellis","owner":"valentinradu","description":"A lightweight, event-driven architectural framework","archived":false,"fork":false,"pushed_at":"2022-04-19T19:09:52.000Z","size":348,"stargazers_count":33,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-30T17:19:34.882Z","etag":null,"topics":["architectural-patterns","ios","macos","redux","swift","swiftui","watchos"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/valentinradu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-17T19:11:04.000Z","updated_at":"2024-09-29T19:09:12.000Z","dependencies_parsed_at":"2022-09-02T04:12:27.641Z","dependency_job_id":null,"html_url":"https://github.com/valentinradu/Trellis","commit_stats":null,"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/valentinradu%2FTrellis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/valentinradu%2FTrellis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/valentinradu%2FTrellis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/valentinradu%2FTrellis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/valentinradu","download_url":"https://codeload.github.com/valentinradu/Trellis/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226090030,"owners_count":17572114,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architectural-patterns","ios","macos","redux","swift","swiftui","watchos"],"created_at":"2024-08-05T23:00:32.908Z","updated_at":"2024-11-23T19:31:33.248Z","avatar_url":"https://github.com/valentinradu.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"# Trellis\n\n[![Swift](https://img.shields.io/badge/Swift-5.6-orange.svg?style=for-the-badge\u0026logo=swift)](https://swift.org)\n[![Xcode](https://img.shields.io/badge/Xcode-13-blue.svg?style=for-the-badge\u0026logo=Xcode\u0026logoColor=white)](https://developer.apple.com/xcode)\n[![MIT](https://img.shields.io/badge/license-MIT-black.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)\n\nTrellis features a declarative DSL that simplifies service bootstrapping: \n\n```swift\nlet cluster = try await Bootstrap {\n    Group {\n        Store(model: IdentityModel.self)\n            .mutate(on: IdentityAction.self) { model, action, send in\n                // ...\n            }\n            .mutate(on: StartUpAction.self) { model, action, send in\n                // ...\n            }\n            .with(model: identityModel)\n        Store(model: ArticlesModel.self)\n            .mutate(on: ArticlesAction.self) { model, action, send in\n                // ...\n            }\n            .with(model: articlesModel)\n    }\n    .emit(using: notificationsStream)\n    .transformError {\n        ErrorAction.error($0)\n    }\n    .observe(on: IdentityAction.self) {\n        // ...\n    }\n}\n```\n\nThis 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.\n\n```swift\ntry await cluster.send(action: StartUpAction.appDidCompleteLaunching)\n```\n\nMost of the time we won't declare services like this. Instead, we'd write a custom service wrapping each store:\n\n```swift\n// IdentityService.swift\nstruct IdentityService: Service {\n    var body: some Service {\n        Store(model: IdentityModel.self)\n            .mutate(on: IdentityAction.self) { model, action, send in\n                // ...\n            }\n            .mutate(on: StartUpAction.self) { model, action, send in\n                // ...\n            }\n    }\n\n// SomeOtherFile.swift\nlet cluster = try await Bootstrap {\n    IdentityService()\n        .with(model: identityModel)\n}\n```\n\nNotice how the actual model is injected from outside the service, enabling dependency injection.\n\n## Index\n* [Installation](#installation)\n* [Getting started](#getting-started)\n* [Concurrency](#concurrency)\n* [Modifiers](#modifiers)\n* [Testing](#testing)\n* [License](#license)\n\n## Installation\n\nUsing Swift Package Manager:\n```\n.package(name: \"Trellis\",\n         url: \"https://github.com/valentinradu/Trellis.git\",\n         .upToNextMinor(from: \"0.3.0-beta\"))\n```\n\n## Getting started\n\n### Actions and services\n\nServices 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. \n\n### Modifiers\n\nModifiers 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.\n\n### Groups\n\nGroups 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.\n\n### Stores\n\nEach 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.\n\n## Modifiers\n\n`.emit(using:consumeAtBootstrap:)` - Takes an external source of events (async stream) that outputs actions and feeds them to all services under it. When setting `\n\n`.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.\n\n`.concurrent()` - Executes all services under it in a concurrent fashion. This is the default.\n\n`.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.\n\n`.bootstrap(bootstrapHandler:)` - Called right after service creation, it gives services the possibility to initialize state or bootstrap models before handling any actions.\n\n`.observe(observeHandler:)` - Called each time an action is received. Ideal for logging and updating external (e.g. presentation layer) state.\n\n`.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.\n\n`.with(model:)` - Sets the model for all sub-services under it.\n\n## Concurrency\n\nTrellis 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.\n\n## Testing\n\nWith 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:\n\n```swift\n// SomeTest.swift\nlet cluster = try await Bootstrap {\n    IdentityService()\n        .with(model: mockedIdentityModel)\n        .environment(\\.send, recordingSend)\n}\n\ntry await cluster.send(action: StartUpAction.appDidCompleteLaunching)\n// Assert the state of the mocked identity model and the recorded actions\n```\n\n## License\n[MIT License](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvalentinradu%2FTrellis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvalentinradu%2FTrellis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvalentinradu%2FTrellis/lists"}