diff --git a/CHANGELOG.md b/CHANGELOG.md index 820e1d4123..74abc8d5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Improving docs - Updating DSl to Decouple Machinery from the struct itself - [Pull Request](https://github.com/joaomdmoura/machinery/pull/10) - Adding support for automatic persistence - [Pull Request](https://github.com/joaomdmoura/machinery/pull/11) +- Converting states from Atoms to Strings - [Pull Request](https://github.com/joaomdmoura/machinery/pull/12) ## 0.4.1 - Updating wrong docs and README - [Pull Request](https://github.com/joaomdmoura/machinery/pull/5) diff --git a/README.md b/README.md index 8a6352a101..0b4edc95bc 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ logic. So let's say you want to add it to your `User` model, you should create a Machinery expects a `Keyword` as argument with two keys `states` and `transitions`. -- `states`: A List of Atoms representing each state. +- `states`: A List of Strings representing each state. - `transitions`: A Map for each state and it allowed next state(s). ### Example @@ -93,10 +93,10 @@ defmodule YourProject.UserStateMachine do use Machinery, # The first state declared will be considered # the intial state - states: [:created, :partial, :complete], + states: ["created", "partial", "complete"], transitions: %{ - created: [:partial, :complete], - partial: :completed + "created" => ["partial", "complete"], + "partial" => "completed" } end ``` @@ -111,12 +111,12 @@ It takes three arguments: - `struct`: The `struct` you want to transit to another state. - `state_machine_module`: The module that holds the state machine logic, where Machinery as imported. -- `next_event`: `atom` of the next state you want the struct to transition to. +- `next_event`: `string` of the next state you want the struct to transition to. **Guard functions, before and after callbacks will be checked automatically.** ```elixir -Machinery.transition_to(your_struct, YourStateMachine, :next_state) +Machinery.transition_to(your_struct, YourStateMachine, "next_state") # {:ok, updated_struct} ``` @@ -124,7 +124,7 @@ Machinery.transition_to(your_struct, YourStateMachine, :next_state) ```elixir user = Accounts.get_user!(1) -UserStateMachine.transition_to(user, UserStateMachine, :complete) +UserStateMachine.transition_to(user, UserStateMachine, "complete") ``` ## Persist State @@ -145,10 +145,9 @@ defmodule YourProject.UserStateMachine do alias YourProject.Accounts use Machinery, - states: [:created, :complete], - transitions: %{created: :complete} + states: ["created", "complete"], + transitions: %{"created" => "complete} - # `next_state` in this case will be a string not an atom. def persist(struct, next_state) do # Updating a user on the database with the new state. {:ok, user} = Accounts.update_user(struct, %{state: next_state}) @@ -159,14 +158,14 @@ end ## Guard functions Create guard conditions by adding signatures of the `guard_transition/2` -function, it will receive two arguments, the `struct` and an `atom` of the state -it will transit to, use this second argument to pattern matching the desired -state you want to guard. +function, it will receive two arguments, the `struct` and an `string` of the +state it will transit to, use this second argument to pattern matching the +desired state you want to guard. ```elixir # The second argument is used to pattern match into the state # and guard the transition to it -def guard_transition(struct, :guarded_state) do +def guard_transition(struct, "guarded_state") do # Your guard logic here end ``` @@ -180,12 +179,12 @@ Guard conditions should return a boolean: ```elixir defmodule YourProject.UserStateMachine do use Machinery, - states: [:created, :complete], - transitions: %{created: :complete} + states: ["created", "complete"], + transitions: %{"created" => "complete} - # Guard the transition to the :complete state. - def guard_transition(struct, :complete) do - Map.get(struct, :missing_fields) == false + # Guard the transition to the "complete" state. + def guard_transition(struct, "complete") do + Map.get(struct, :missing_fields) == false end end ``` @@ -195,15 +194,15 @@ end You can also use before and after callbacks to handle desired side effects and reactions to a specific state transition. -You can just declare `before_transition/2` and ` after_transition/2`, +You can just declare `before_transition/2` and `after_transition/2`, pattern matching the desired state you want to. **Make sure Before and After callbacks should return the struct.** ```elixir # callbacks should always return the struct. -def before_transition(struct, :state), do: struct -def after_transition(struct, :state), do: struct +def before_transition(struct, "state"), do: struct +def after_transition(struct, "state"), do: struct ``` ### Example: @@ -211,18 +210,18 @@ def after_transition(struct, :state), do: struct ```elixir defmodule YourProject.UserStateMachine do use Machinery, - states: [:created, :partial, :complete], + states: ["created", "partial", "complete"], transitions: %{ - created: [:partial, :complete], - partial: :completed + "created" => ["partial", "complete"], + "partial" => "completed" } - def before_transition(struct, :partial) do + def before_transition(struct, "partial") do # ... overall desired side effects struct end - def after_transition(struct, :completed) do + def after_transition(struct, "completed") do # ... overall desired side effects struct end diff --git a/lib/machinery.ex b/lib/machinery.ex index 31b42ece6f..7f77492d98 100644 --- a/lib/machinery.ex +++ b/lib/machinery.ex @@ -10,13 +10,10 @@ defmodule Machinery do Machinery expects a `Keyword` as argument with two keys `states` and `transitions`. - - `states`: A List of Atoms representing each state. - - `transitions`: A Map for each state and it allowed next state(s). - ## Parameters - `opts`: A Keyword including `states` and `transitions`. - - `states`: A List of Atoms representing each state. + - `states`: A List of Strings representing each state. - `transitions`: A Map for each state and it allowed next state(s). ## Example @@ -25,10 +22,10 @@ defmodule Machinery do use Machinery, # The first state declared will be considered # the intial state - states: [:created, :partial, :complete], + states: ["created", "partial", "complete"], transitions: %{ - created: [:partial, :complete], - partial: :completed + "created" => ["partial", "complete"], + "partial" => "completed" } end ``` @@ -47,7 +44,7 @@ defmodule Machinery do It expects a `Keyword` as argument with two keys `states` and `transitions`. - - `states`: A List of Atoms representing each state. + - `states`: A List of Strings representing each state. - `transitions`: A Map for each state and it allowed next state(s). P.S. The first state declared will be considered the intial state @@ -80,14 +77,14 @@ defmodule Machinery do - `struct`: The `struct` you want to transit to another state. - `state_machine_module`: The module that holds the state machine logic, where Machinery as imported. - - `next_state`: Atom of the next state you want to transition to. + - `next_state`: String of the next state you want to transition to. ## Examples Machinery.transition_to(%User{state: :partial}, UserStateMachine, :completed) {:ok, %User{state: :completed}} """ - @spec transition_to(struct, module, atom) :: {:ok, struct} | {:error, String.t} + @spec transition_to(struct, module, String.t) :: {:ok, struct} | {:error, String.t} def transition_to(struct, state_machine_module, next_state) do initial_state = state_machine_module._machinery_initial_state() transitions = state_machine_module._machinery_transitions() @@ -96,7 +93,7 @@ defmodule Machinery do # first declared state on the struct model. current_state = case Map.get(struct, :state) do nil -> initial_state - current_state -> String.to_atom(current_state) + current_state -> current_state end # Checking declared transitions and guard functions before @@ -111,7 +108,7 @@ defmodule Machinery do true -> struct = struct |> Transition.before_callbacks(next_state, state_machine_module) - |> Transition.persist_struct(Atom.to_string(next_state), state_machine_module) + |> Transition.persist_struct(next_state, state_machine_module) |> Transition.after_callbacks(next_state, state_machine_module) {:ok, struct} end diff --git a/test/machinery_test.exs b/test/machinery_test.exs index f2102e1d76..834bb0856c 100644 --- a/test/machinery_test.exs +++ b/test/machinery_test.exs @@ -12,13 +12,13 @@ defmodule MachineryTest do defmodule TestStateMachineWithGuard do use Machinery, - states: [:created, :partial, :completed], + states: ["created", "partial", "completed"], transitions: %{ - created: [:partial, :completed], - partial: :completed + "created" => ["partial", "completed"], + "partial" => "completed" } - def guard_transition(struct, :completed) do + def guard_transition(struct, "completed") do # Code to simulate and force an exception inside a # guard function. if Map.get(struct, :force_exception) do @@ -31,13 +31,13 @@ defmodule MachineryTest do defmodule TestStateMachine do use Machinery, - states: [:created, :partial, :completed], + states: ["created", "partial", "completed"], transitions: %{ - created: [:partial, :completed], - partial: :completed + "created" => ["partial", "completed"], + "partial" => "completed" } - def before_transition(struct, :partial) do + def before_transition(struct, "partial") do # Code to simulate and force an exception inside a # guard function. if Map.get(struct, :force_exception) do @@ -47,7 +47,7 @@ defmodule MachineryTest do Map.put(struct, :missing_fields, true) end - def after_transition(struct, :completed) do + def after_transition(struct, "completed") do Map.put(struct, :missing_fields, false) end @@ -74,40 +74,40 @@ defmodule MachineryTest do stateless_struct = %TestStruct{} completed_struct = %TestStruct{state: "completed"} - assert {:ok, %TestStruct{state: "partial"}} = Machinery.transition_to(created_struct, TestStateMachine, :partial) - assert {:ok, %TestStruct{state: "completed", missing_fields: false}} = Machinery.transition_to(created_struct, TestStateMachine, :completed) - assert {:ok, %TestStruct{state: "completed", missing_fields: false}} = Machinery.transition_to(partial_struct, TestStateMachine, :completed) - assert {:error, "Transition to this state isn't declared."} = Machinery.transition_to(stateless_struct, TestStateMachine, :created) - assert {:error, "Transition to this state isn't declared."} = Machinery.transition_to(completed_struct, TestStateMachine, :created) + assert {:ok, %TestStruct{state: "partial"}} = Machinery.transition_to(created_struct, TestStateMachine, "partial") + assert {:ok, %TestStruct{state: "completed", missing_fields: false}} = Machinery.transition_to(created_struct, TestStateMachine, "completed") + assert {:ok, %TestStruct{state: "completed", missing_fields: false}} = Machinery.transition_to(partial_struct, TestStateMachine, "completed") + assert {:error, "Transition to this state isn't declared."} = Machinery.transition_to(stateless_struct, TestStateMachine, "created") + assert {:error, "Transition to this state isn't declared."} = Machinery.transition_to(completed_struct, TestStateMachine, "created") end test "Guard functions should be executed before moving the resource to the next state" do struct = %TestStruct{state: "created", missing_fields: true} - assert {:error, "Transition not completed, blocked by guard function."} = Machinery.transition_to(struct, TestStateMachineWithGuard, :completed) + assert {:error, "Transition not completed, blocked by guard function."} = Machinery.transition_to(struct, TestStateMachineWithGuard, "completed") end test "Guard functions should allow or block transitions" do allowed_struct = %TestStruct{state: "created", missing_fields: false} blocked_struct = %TestStruct{state: "created", missing_fields: true} - assert {:ok, %TestStruct{state: "completed", missing_fields: false}} = Machinery.transition_to(allowed_struct, TestStateMachineWithGuard, :completed) - assert {:error, "Transition not completed, blocked by guard function."} = Machinery.transition_to(blocked_struct, TestStateMachineWithGuard, :completed) + assert {:ok, %TestStruct{state: "completed", missing_fields: false}} = Machinery.transition_to(allowed_struct, TestStateMachineWithGuard, "completed") + assert {:error, "Transition not completed, blocked by guard function."} = Machinery.transition_to(blocked_struct, TestStateMachineWithGuard, "completed") end test "The first declared state should be considered the initial one" do stateless_struct = %TestStruct{} - assert {:ok, %TestStruct{state: "partial"}} = Machinery.transition_to(stateless_struct, TestStateMachine, :partial) + assert {:ok, %TestStruct{state: "partial"}} = Machinery.transition_to(stateless_struct, TestStateMachine, "partial") end test "Modules without guard conditions should allow transitions by default" do struct = %TestStruct{state: "created"} - assert {:ok, %TestStruct{state: "completed"}} = Machinery.transition_to(struct, TestStateMachine, :completed) + assert {:ok, %TestStruct{state: "completed"}} = Machinery.transition_to(struct, TestStateMachine, "completed") end test "Implict rescue on the guard clause internals should raise any other excepetion not strictly related to missing guard_tranistion/2 existence" do wrong_struct = %TestStruct{state: "created", force_exception: true} assert_raise UndefinedFunctionError, fn() -> - Machinery.transition_to(wrong_struct, TestStateMachineWithGuard, :completed) + Machinery.transition_to(wrong_struct, TestStateMachineWithGuard, "completed") end end @@ -115,29 +115,29 @@ defmodule MachineryTest do struct = %TestStruct{} assert struct.missing_fields == nil - {:ok, partial_struct} = Machinery.transition_to(struct, TestStateMachine, :partial) + {:ok, partial_struct} = Machinery.transition_to(struct, TestStateMachine, "partial") assert partial_struct.missing_fields == true - {:ok, completed_struct} = Machinery.transition_to(struct, TestStateMachine, :completed) + {:ok, completed_struct} = Machinery.transition_to(struct, TestStateMachine, "completed") assert completed_struct.missing_fields == false end test "Implict rescue on the callbacks internals should raise any other excepetion not strictly related to missing callbacks_fallback/2 existence" do wrong_struct = %TestStruct{state: "created", force_exception: true} assert_raise UndefinedFunctionError, fn() -> - Machinery.transition_to(wrong_struct, TestStateMachine, :partial) + Machinery.transition_to(wrong_struct, TestStateMachine, "partial") end end test "Persist function should be called after the transition" do struct = %TestStruct{state: "partial"} - assert {:ok, _} = Machinery.transition_to(struct, TestStateMachine, :completed) + assert {:ok, _} = Machinery.transition_to(struct, TestStateMachine, "completed") end test "Persist function should still reaise errors if not related to the existence of persist/1 method" do wrong_struct = %TestStruct{state: "created", force_exception: true} assert_raise UndefinedFunctionError, fn() -> - Machinery.transition_to(wrong_struct, TestStateMachine, :completed) + Machinery.transition_to(wrong_struct, TestStateMachine, "completed") end end end