Current section
Files
Jump to
Current section
Files
README.md
# Fable
Write simple, event-driven applications.
[](https://hex.pm/packages/fable)
[](https://hex.pm/packages/fable)
[](https://github.com/CargoSense/fable/actions/workflows/ci.yml)
## Design Philosophy
- Easy to retrofit.
- Drop-in compatible with Ecto tests (async included).
- Event log should always be consistent.
- Events are serialized around important "aggregate" database records.
## Installation
Add Fable in your project's `mix.exs` file:
```elixir
defp deps do
[
{:fable, "~> 0.0.1-alpha.1"}
]
end
```
Add the Fable migration to `priv/repo/migrations` and migrate it.
## Usage
As an example, we're going to wrap the following existing function:
```elixir
defmodule MyApp.Blog do
alias MyApp.Blog.Post
def create_post(params, user) do
with :ok <- is_admin(user) do
%Post{}
|> Post.changeset(params)
|> MyApp.Repo.insert()
end
end
def update_post(post, params, user) do
with :ok <- is_admin(user) do
post
|> Post.changeset(params)
|> MyApp.Repo.update()
end
end
end
```
### Create the `MyApp.Events` module
First, create an events module that will handle all the events added to the system.
```elixir
defmodule MyApp.Events do
use Fable.Events
@impl Fable.Router
def handlers do
%{}
end
end
```
By default, the module will implement `Fable.Router.handlers/0`. This can be changed by using `use Fable.Events, router: MyApp.EventsRouter`.
### Creating Events
The example system consists of two actions: creating a blog post and updating a blog post. Using the past tense, define `PostCreated` and `PostUpdated` events.
```elixir
defmodule MyApp.Blog.Events.PostCreated do
use Fable.Event
embedded_schema do
field :title, :string
field :body, :string
end
end
defmodule MyApp.Blog.Events.PostUpdated do
use Fable.Event
embedded_schema do
field :title, :string
field :body, :string
end
end
```
The data of these embedded schemas will be stored in Fable's event table along with some metadata. The schemas will hold the data your application needs to run the task it deals with. In this example case, all the data passed to the changeset function for posts to be persisted.
### Wrapping Existing Code
With the events being created, wrap the existing code to make use of `:fable`.
#### Moving logic to a separate function
First, move what shall happen as result of the event to a separate function.
```elixir
defmodule MyApp.Blog do
alias MyApp.Blog.Post
alias MyApp.Blog.Events
def create_post(params, user) do
with :ok <- is_admin(user) do
# moved
end
end
defp post_created(post, %Events.PostCreated{} = event) do
post
|> Post.changeset(Map.from_struct(event))
|> MyApp.Repo.insert()
end
def update_post(post, params, user) do
with :ok <- is_admin(user) do
# moved
end
end
defp post_updated(post, %Events.PostUpdated{} = event) do
post
|> Post.changeset(Map.from_struct(event))
|> MyApp.Repo.update()
end
end
```
#### Registering event handlers
Note that the new functions are private and should never be called externally. To make them be callable by Fable, register handlers for their respective events. One way to do this is by having `MyApp.Blog` implement `Fable.Router` and letting `MyApp.Events` call `handlers/0` on `MyApp.Blog`. This is only example of how to handle event registration.
```elixir
defmodule MyApp.Blog do
# …
@behaviour Fable.Router
@impl Fable.Router
def handlers do
%{
Events.PostCreated => &post_created/2,
Events.PostUpdated => &post_updated/2
}
end
# …
end
defmodule MyApp.Events do
use Fable.Events
@impl Fable.Router
def handlers do
%{}
|> Map.merge(MyApp.Blog.handlers(), &merge/3)
end
# If multiple merged maps want to handle the same event
defp merge(_event, handlers_a, handlers_b) do
List.wrap(handlers_a) ++ List.wrap(handlers_b)
end
end
```
#### Emitting events
The last thing to do is emit the events so that the code in the hevent andlers is called.
```elixir
defmodule MyApp.Blog do
# …
def create_post(params, user) do
%Post{id: Ecto.UUID.generate()}
|> MyApp.Events.emit(fn _, _, _ ->
with :ok <- is_admin(user) do
%Events.PostCreated{
title: params["title"],
body: params["body"]
}
end
end)
|> MyApp.Repo.transaction()
end
# …
def update_post(post, params, user) do
post
|> MyApp.Events.emit(fn _, _, _ ->
with :ok <- is_admin(user) do
%Events.PostUpdated{
title: params["title"],
body: params["body"]
}
end
end)
|> MyApp.Repo.transaction()
end
# …
end
defmodule MyApp.Blog.Post do
use Ecto.Schema
schema "posts" do
…
# needs a migration as well
field :last_event_id, :integer
end
end
```
> [!IMPORTANT]
> Note that the aggregate (the post) will need the `:last_event_id` field added on the schema and in the database. The aggregate also needs an `id` before events can be applied to it. This is simple for UUID-based IDs as shown. Using integer-based IDs is supported, but initial creation cannot be handled by a Fable event. Consider using in combination with `Ecto.Multi`.