Skip to content

Latest commit

 

History

92 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fable

Write simple, event-driven applications.

Package Downloads Build

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:

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:

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.

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.

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.

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.

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.

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.

About

Your events have a story to tell.

Resources

Stars

88 stars

Watchers

14 watching

Forks

Releases

Contributors

Languages