This package allows to use finite state machine pattern in Ecto. Specify:
- states
- events
- column (optional)
If available in Hex, the package can be installed as:
- Add ecto_as_state_machine to your list of dependencies in
mix.exs
:
def deps do
[{:ecto_as_state_machine, "~> 2.0"}]
end
- Ensure ecto_as_state_machine is started before your application:
def application do
[applications: [:ecto_as_state_machine]]
end
defmodule User do
use Web, :model
use EctoAsStateMachine
easm column: :state,
initial: :unconfirmed,
states: [:unconfirmed, :confirmed, :blocked, :admin],
events: [
[
name: :confirm,
from: [:unconfirmed],
to: :confirmed,
callback: fn(model) -> Ecto.Changeset.change(model, confirmed_at: DateTime.utc_now |> DateTime.to_naive) end # yeah you can bring your own code to these functions.
], [
name: :block,
from: [:confirmed, :admin],
to: :blocked
], [
name: :make_admin,
from: [:confirmed],
to: :admin
]
]
schema "users" do
field :state, :string
end
end
now you can run:
user = Repo.get_by(User, id: 1)
new_user_changeset = User.confirm(user) # => Safe transition user state to "confirmed". We can make him admin!
Repo.update(new_user_changeset) # => Update manually
new_user = User.confirm!(user) # => Or auto-transition user state to "confirmed". We can make him admin!
User.confirmed?(new_user) # => true
User.admin?(new_user) # => false
User.can_confirm?(new_user) # => false
User.can_make_admin?(new_user) # => true
new_user = User.make_admin!(new_user)
User.admin?(new_user) # => true
ecto_as_state_machine
uses state
database column by default. You can specify
column
option to change it. Or additional column, like this:
defmodule User do
use Web, :model
use EctoAsStateMachine
easm initial: :some,
# bla-bla-bla
easm column: :rules,
# bla-bla-bla
end
ecto_as_state_machine
uses next_state
method to try next state. If struct have last state already,
it will be not changed. Look how it can be used:
defmodule User do
use Web, :model
use EctoAsStateMachine
easm states: [:unconfirmed, :confirmed, :blocked, :admin],
initial: :unconfirmed,
events: [
[
name: :confirm,
from: [:unconfirmed],
to: :confirmed
], [
name: :block,
from: [:confirmed],
to: :blocked
], [
name: :make_admin,
from: [:blocked],
to: :admin
]
],
# ...
end
user = Repo.get_by(User, id: 1) # state: unconfirmed
new_user = User.next_state(user) # state: confirmed
new_user = User.next_state(new_user) # state: blocked
new_user = User.next_state(new_user) # state: admin
new_user = User.next_state(new_user) # state: admin
- Clone repo:
git clone https://github.com/cnsa/ecto_as_state_machine.git
- Open directory
cd ecto_as_state_machine
- Install dependencies
mix deps.get
- Add feature
- Test it:
mix test
Once you've made your additions and mix test passes, go ahead and open a PR!
- Cover by tests
- Custom db column name
- Validation method for changeset indicates its value in the correct range
- Initial value
- CI
- Ecto 3.0 support
- Add status? methods
- Rely on last versions of ecto & elixir
- Write dedicated module instead of requiring everything into the model
- Write bang! methods which are raising exception instead of returning invalid changeset
- Rewrite spaghetti description in README