Code Monkey home page Code Monkey logo

ecto_state_machine's Introduction

Ecto state machine

travis ci badge badge

This package allows to use finite state machine pattern in Ecto. Specify:

and go:

defmodule User do
  use Web, :model

  use EctoStateMachine,
    states: [:unconfirmed, :confirmed, :blocked, :admin],
    events: [
      [
        name:     :confirm,
        from:     [:unconfirmed],
        to:       :confirmed,
        callback: fn(model) -> Ecto.Changeset.change(model, confirmed_at: Ecto.DateTime.utc) 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, default: "unconfirmed"
  end
end

now you can do:

user = Repo.get_by(User, id: 1)

# Create changeset transition user state to "confirmed". We can make him admin!
confirmed_user = User.confirm(user)     # =>

# We can validate ability to change user's state
User.can_confirm?(confirmed_user)       # => false
User.can_make_admin?(confirmed_user)    # => true

# Create changeset transition user state to "admin"
admin = User.make_admin(confirmed_user)

# Store changeset to the database
Repo.update(admin)                      


# List all possible states
# If column isn't `:state`, function name will be prefixed. IE,
# for column `:rules` function name will be `rules_states`
User.states # => [:unconfirmed, :confirmed, :blocked, :admin]

# List all possible events
# If column isn't `:state`, function name will be prefixed. IE,
# for column `:rules` function name will be `rules_events`
User.events # => [:confirm, :block, :make_admin]

You can check out whole test/dummy directory to inspect how to organize sample app.

Installation

If available in Hex, the package can be installed as:

  1. Add ecto_state_machine to your list of dependencies in mix.exs:

    def deps do [{:ecto_state_machine, "~> 0.1.0"}] end

Custom column name

ecto_state_machine uses state database column by default. You can specify column option to change it. Like this:

defmodule Dummy.User do
  use Dummy.Web, :model

  use EctoStateMachine,
    column: :rules,
    # bla-bla-bla
end

Now your state will be stored into rules column.

Contributions

  1. Install dependencies mix deps.get
  2. Setup your config/test.exs & config/dev.exs
  3. Run migrations mix ecto.migrate & MIX_ENV=test mix ecto.migrate
  4. Develop new feature
  5. Write new tests
  6. Test it: mix test
  7. Open new PR!

Roadmap to 1.0

  • Cover by tests
  • Custom db column name
  • Validation method for changeset indicates its value in the correct range
  • Initial value
  • CI
  • Add status? methods
  • Introduce it at elixir-radar and my blog
  • Custom error messages for changeset (with translations by gettext ability)
  • 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

ecto_state_machine's People

Contributors

asiniy avatar merqlove avatar sespindola avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

ecto_state_machine's Issues

Using EctoStateMachine removes Ecto.Changeset functions from the model

By defining a state machine on a model, I can non longer define a changeset in my model. If I import Ecto.Changeset after I've defined my state machine, things tend to work well again.

I've included examples of what should fail and succeed here: https://gist.github.com/davidrichards/694c7eb030c01b17b014cbaea3aad4f1

The error I get is something along the lines of:

== Compilation error on file web/models/invitation.ex ==
** (CompileError) web/models/invitation.ex:77: undefined function cast/3
    (stdlib) lists.erl:1338: :lists.foreach/2
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6

This is the error I get from my own project, not the gist I created above.

Bottom line, I think by aliasing the Ecto.Changeset inside the macro we are changing the way we need to write changesets in our models for non-state changes. To get the import to work again after using the macro, I either have to import Ecto.Changeset again (as in the second example in the gist) or use Changeset.cast(...), Changest.put_change(...), etc.

If there's a way to make that macro less obtrusive to it's surrounding model, that would be useful.

Thanks for this work, btw.

Using multiple times defines duplicate of `validate_state_transition`

Fix would be to use this:

      events
      |> Enum.each(fn(event) ->
        unless event[:to] in states do
          raise "Target state :#{event[:to]} is not present in @states"
        end

        def unquote(event[:name])(model) do
          model
          |> Changeset.change(%{ unquote(column) => "#{unquote(event[:to])}" })
          |> unquote(event[:callback]).()
          |> unquote(:"validate_state_transition_#{column}")(unquote(event), model)
        end

        def unquote(:"can_#{event[:name]}?")(model) do
          :"#{Map.get(model, unquote(column))}" in unquote(event[:from])
        end
      end)

      defp unquote(:"validate_state_transition_#{column}")(changeset, event, model) do
        change = Map.get(model, unquote(column))

        if :"#{change}" in event[:from] do
          changeset
        else
          changeset
          |> Changeset.add_error(unquote(column),
            "You can't move state from :#{change} to :#{event[:to]}"
            )
        end
      end

Introducing `is_column_state?` Functions

It would be cool, to have a praedicate to check if a column is in a state, like: is_update_successfull?

Code to use here:

      states
      |> Enum.each(fn(state) ->
        def unquote(:"is_#{column}_#{state}?")(model) do
          :"#{Map.get(model, unquote(column))}" == unquote(state)
        end
      end)

License for ecto_state_machine?

It like there isn't a license for ecto_state_machine. Given that it's a public repo and on the Awesome Elixir list I'm assuming that you'd like it to be open source software, and including a LICENSE.md file would make that explicit.

Lifecycle callbacks

Would be nice to use multiple callbacks in order to support things like Ecto.Multi. So instead of just having a :callback which needs to call :validate_state_transition which expects an Ecto.Changeset, you have :before_transition and an :after_transition.

This allows you to do

  use EctoStateMachine,
    states: [:initial, :confirmed],
    events: [
      [
        name:     :confirm,
        from:     [:inital],
        to:       :confirmed,
        before_transition: fn(model, _params) -> Ecto.Changeset.change(model, expired_at: Ecto.DateTime.utc()),
        after_transition: fn(changeset, params) do
          Multi.new
          |> Multi.update(changeset)
          |> Multi.insert(TBD.changeset(%TBD{}, params)
        end
      ]
    ]

Now you can call User.confirm(params) and the params supplied will get applied to the transition functions.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.