Skip to content

Commit

Permalink
Merge pull request #12 from joaomdmoura/atoms-to-stings
Browse files Browse the repository at this point in the history
Changing states from atoms to strings
  • Loading branch information
joaomdmoura authored Dec 18, 2017
2 parents 82a2c88 + 25a8ddf commit e912aec
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 26 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand All @@ -111,20 +111,20 @@ 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}
```

### Example:

```elixir
user = Accounts.get_user!(1)
UserStateMachine.transition_to(user, UserStateMachine, :complete)
UserStateMachine.transition_to(user, UserStateMachine, "complete")
```

## Persist State
Expand All @@ -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})
Expand All @@ -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
```
Expand All @@ -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
```
Expand All @@ -195,34 +194,34 @@ 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:

```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
Expand Down
21 changes: 9 additions & 12 deletions lib/machinery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand Down
50 changes: 25 additions & 25 deletions test/machinery_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -74,70 +74,70 @@ 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

test "after_transition/2 and before_transition/2 callbacks should be automatically executed" 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

0 comments on commit e912aec

Please sign in to comment.