A way of structuring task pipelines that is friendly to extensions and customisability
Epics are a extensible way of structuring tasks.
This library is designed to provide a structured way to define and execute complex workflows in Elixir applications. It introduces the concept of "Epics" and "Acts" to organize and run sequences of operations.
Bonfire.Epics.Epic
: An Epic represents a complete workflow or process. It's a container that holds a sequence of Acts to be executed, along with state information, errors, and assigned values.Bonfire.Epics.Act
: An Act is an individual step or operation within an Epic. Each Act is typically a module that implements a specific task or functionality.Bonfire.Ecto
library for helpers to queue changeset operations within Acts and then run them all together in a single transaction: https://github.com/bonfire-networks/bonfire_ectoThis library is particularly useful for applications that need to manage complex, multi-step tasks with error handling and state management. It provides a flexible and extensible way to define, configure, and execute these processes, making it easier to maintain and modify complex workflows.
run/2
function that performs a specific task.Write a module with a run/2
function that takes an Epic and an Act, performs a specific task, and returns an Epic.
defmodule Bonfire.Label.Acts.LabelObject do
@moduledoc """
Takes an object and label and returns a changeset for labeling that object.
Implements `Bonfire.Epics.Act`.
Epic Options:
* `:current_user` - user that will create the page, required.
Act Options:
* `:as` - key to where we find the label(s) to add, and then assign changeset to, default: `:label`.
* `:object` (configurable) - id to use for the thing to label
* `:attrs` - epic options key to find the attributes at, default: `:attrs`.
"""
use Arrows
import Bonfire.Epics
@doc false
def run(epic, act) do
current_user = Bonfire.Common.Utils.current_user(epic.assigns[:options])
cond do
epic.errors != [] ->
maybe_debug(
epic,
act,
length(epic.errors),
"Skipping due to epic errors"
)
epic
not (is_struct(current_user) or is_binary(current_user)) ->
maybe_debug(
epic,
act,
current_user,
"Skipping due to missing current_user"
)
epic
true ->
as = Keyword.get(act.options, :as) || Keyword.get(act.options, :on, :label)
object_key = Keyword.get(act.options, :object, :object)
label = Keyword.get(epic.assigns[:options], as, [])
object = Keyword.get(epic.assigns[:options], object_key, nil)
Bonfire.Label.Labelling.label_object(label, object,
return: :changeset,
current_user: current_user
)
|> Map.put(:action, :insert)
|> Bonfire.Epics.Epic.assign(epic, as, ...)
|> Bonfire.Ecto.Acts.Work.add(:label)
end
end
end
@page_act_opts [on: :page, attrs: :page_attrs]
config :bonfire_pages, Bonfire.Pages,
epics: [
create: [
# Create a changeset for insertion
{Bonfire.Pages.Acts.Page.Create, @page_act_opts},
# with a sanitised body and tags extracted,
{Bonfire.Social.Acts.PostContents, @page_act_opts},
# a caretaker,
{Bonfire.Me.Acts.Caretaker, @page_act_opts},
# and a creator,
{Bonfire.Me.Acts.Creator, @page_act_opts},
# and possibly fetch contents of URLs,
{Bonfire.Files.Acts.URLPreviews, @page_act_opts},
# possibly with uploaded files,
{Bonfire.Files.Acts.AttachMedia, @page_act_opts},
# with extracted tags fully hooked up,
{Bonfire.Tag.Acts.Tag, @page_act_opts},
# and the appropriate boundaries established,
{Bonfire.Boundaries.Acts.SetBoundaries, @page_act_opts},
# Now we open a Postgres transaction and actually do the insertions in DB
Bonfire.Ecto.Acts.Begin,
# Run our inserts
Bonfire.Ecto.Acts.Work,
Bonfire.Ecto.Acts.Commit,
# Enqueue for indexing by meilisearch
{Bonfire.Search.Acts.Queue, @page_act_opts}
]
]
config :bonfire_posts, Bonfire.Posts,
epics: [
publish: [
# Create a changeset for insertion
Bonfire.Posts.Acts.Posts.Publish,
# These next 3 Acts are run in parallel
[
# with a sanitised body and tags extracted,
{Bonfire.Social.Acts.PostContents, on: :post},
# assign a caretaker,
{Bonfire.Me.Acts.Caretaker, on: :post},
# record the creator,
{Bonfire.Me.Acts.Creator, on: :post}
],
# These next 4 Acts are run in parallel (they run after the previous 3 because they depend on the outputs of those Acts)
[
# possibly fetch contents of URLs,
{Bonfire.Files.Acts.URLPreviews, on: :post},
# possibly occurring in a thread,
{Bonfire.Social.Acts.Threaded, on: :post},
# with extracted tags/mentions fully hooked up,
{Bonfire.Tag.Acts.Tag, on: :post},
# maybe set as sensitive,
{Bonfire.Social.Acts.Sensitivity, on: :post}
],
# These next 3 Acts are run in parallel (they run after the previous 4 because they depend on the outputs of those Acts)
[
# possibly with uploaded/linked media (optionally depends on URLPreviews),
{Bonfire.Files.Acts.AttachMedia, on: :post},
# with appropriate boundaries established (depends on Threaded),
{Bonfire.Boundaries.Acts.SetBoundaries, on: :post},
# summarised by an activity (possibly appearing in feeds),
{Bonfire.Social.Acts.Activity, on: :post}
],
# Now we open a Postgres transaction and actually do the insertions in DB
Bonfire.Ecto.Acts.Begin,
# Run our inserts
Bonfire.Ecto.Acts.Work,
Bonfire.Ecto.Acts.Commit,
# Preload data & Publish live feed updates via (in-memory) PubSub
{Bonfire.Social.Acts.LivePush, on: :post},
# These steps are run in parallel
[
# Enqueue for indexing by meilisearch
{Bonfire.Search.Acts.Queue, on: :post},
# Prepare JSON for federation and add to queue (oban).
{Bonfire.Social.Acts.Federate, on: :post}
],
# Once the activity/object exists (depends on federation being done)
{Bonfire.Tags.Acts.AutoBoost, on: :post}
]
]
Bonfire.Epics.run_epic(Bonfire.Posts, :publish, on: :post)
Copyright (c) 2022 Bonfire Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.