Skip to content

Commit

Permalink
Merge branch 'v1' into 14-customize-formatter-to-not-set-parenthesis-…
Browse files Browse the repository at this point in the history
…on-transition-statements
  • Loading branch information
norbajunior authored Jul 21, 2022
2 parents 3feaca4 + c1a37e3 commit e5a28d8
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 24 deletions.
45 changes: 26 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ def transit(%Door{state: :locked} = struct, event: "unlock") do
end
```

_The functions `transit/2` implements the behaviour_ `Machinist.Transition`

So that we can transit between states by relying on the **state** + **event**
pattern matching.

Expand Down Expand Up @@ -86,29 +84,33 @@ iex> Door.transit(door_opened, event: "lock")
{:error, :not_allowed}
```

### Group same-state `from` definitions
## Guard conditions

We also could be implementing a state machine for an eletronic door which should validate a passcode to unlock it. In this scenario the `machinist` gives us the possibility to provide a function to evaluate a condition and return the new state.

Check out the diagram below representing it:

![state-machine-diagram](./assets/check-passcode.png)

In the example above we could group the `from :unlocked` definitions like this:
And in order to have this condition for the `unlock` event use the `event` macro passing the `guard` option with a one-arity function:

```elixir
# ...
# ..
transitions do
from :locked, to: :unlocked, event: "unlock"
from :unlocked do
to :locked, event: "lock"
to :opened, event: "open"
event "unlock", guard: &check_passcode/1 do
from :locked, to: :unlocked
from :locked, to: :locked
end
from :opened, to: :closed, event: "close"
from :closed, to: :opened, event: "open"
from :closed, to: :locked, event: "lock"
end
# ...

defp check_passcode(door) do
if some_condition, do: :unlocked, else: :locked
end
```

This is an option for a better organization and an increase of readability when having
a large number of `from` definitions with a same state.
So when we call `Door.transit(%Door{state: :locked}, event: "unlock")` the guard function `check_passcode/1` will be called with the struct door as the first parameter and returns the new state to be set.

### Setting different attribute name that holds the state
### Setting a different attribute name that holds the state

By default `machinist` expects the struct being updated holds a `state`
attribute, if you hold state in a different attribute, just pass the name as an
Expand Down Expand Up @@ -162,13 +164,18 @@ defmodule SelectionProcess.V1 do

alias SelectionProcess.Candidate

@minimum_score 100
@minimum_score 70

transitions Candidate do
from :new, to: :registered, event: "register"
from :registered, to: :started_test, event: "start_test"
from :started_test, to: &check_score/1, event: "send_test"
from :approved, to: :enrolled, event: "enroll"

event "send_test", guard: &check_score/1 do
from :started_test, to: :approved
from :started_test, to: :reroved
end

from :approved, to: :enrolled, event: "enroll"
end

defp check_score(%Candidate{test_score: score}) do
Expand Down
Binary file added assets/check-passcode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 130 additions & 4 deletions lib/machinist.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,55 @@ defmodule Machinist do
end
end

@doc """
Defines an `event` block grouping a same-event `from` -> `to` transitions
with a guard function that should evaluates a condition and returns a new state.
event "update_score"", guard: &check_score/1 do
from :test, to: :approved
from :test, to: :reproved
end
defp check_score(%{score: score}) do
if score >= 70, do: :approved, else: :reproved
end
"""
defmacro event(event, [guard: func], do: {:__block__, line, content}) do
content = prepare_transitions(event, func, content)

quote bind_quoted: [content: content, line: line] do
{:__block__, line, content}
end
end

defmacro event(_event, _f, do: {:from, _line, [_from, [do: _block]]} = content) do
raise Machinist.NoLongerSupportedSyntaxError, content
end

@doc """
Defines an `event` block grouping a same-event `from` -> `to` transitions
event "form_submitted" do
from :form1, to: :form2
from :form2, to: :test
end
"""
defmacro event(event, do: {:__block__, line, content}) do
content = prepare_transitions(event, content)

quote bind_quoted: [content: content, line: line] do
{:__block__, line, content}
end
end

defmacro event(event, do: content) do
content = prepare_transition(event, content)

quote bind_quoted: [content: content] do
[do: content]
end
end

@doc """
Defines a state transition with the given `state`, and the list of options `[to: new_state, event: event]`
Expand All @@ -129,22 +178,51 @@ defmodule Machinist do
define_transitions(state, to_statements)
end

defmacro from(state, to: {:&, _, _} = new_state_func, event: event) do
raise Machinist.NoLongerSupportedSyntaxError,
state: state,
to: new_state_func,
event: event
end

defmacro from(state, to: new_state, event: event) do
define_transition(state, to: new_state, event: event)
end

@doc false
defp prepare_transitions(_event, []), do: []

defp prepare_transitions(event, [head | tail]) do
[prepare_transition(event, head) | prepare_transitions(event, tail)]
end

defp prepare_transitions(event, guard_func, [head | _]) do
prepare_transition(event, guard_func, head)
end

defp prepare_transition(event, {:from, _line, [from, to]}) do
define_transition(from, to ++ [event: event])
end

defp prepare_transition(event, guard_func, {:from, _line, [from, _to]}) do
define_transition(from, to: guard_func, event: event)
end

defp define_transitions(_state, []), do: []

@doc false
defp define_transitions(state, [{:to, _line, [new_state, [event: event]]} | transitions]) do
[
define_transition(state, to: new_state, event: event)
| define_transitions(state, transitions)
]
end

@doc false
defp define_transitions(state, [{:&, _, _} = new_state_func, [event: event]]) do
raise Machinist.NoLongerSupportedSyntaxError,
state: state,
to: new_state_func,
event: event
end

defp define_transition(state, to: new_state, event: event) do
quote do
@impl true
Expand All @@ -156,7 +234,6 @@ defmodule Machinist do
end
end

@doc false
defmacro __before_compile__(_) do
quote do
@impl true
Expand All @@ -174,3 +251,52 @@ defmodule Machinist do
end
end
end

defmodule Machinist.NoLongerSupportedSyntaxError do
defexception [:message]

@impl true
def exception({:from, _line, [from, [do: block]]} = content) do
{:__block__, _line, to_statements} = block

new_dsl =
for {_, _line, to} <- to_statements do
"from(:#{from}, to: #{List.first(to)})\n"
end

msg = """
#{IO.ANSI.reset()}`event` block can't support `from` blocks inside anymore
Instead of this:
#{Macro.to_string(content)}
Do this:
#{new_dsl}
"""

%__MODULE__{message: msg}
end

@impl true
def exception(state: state, to: new_state_func, event: event) do
new_dsl = ~s"""
event "#{event}", guard: #{Macro.to_string(new_state_func)} do
from :#{state}, to: :your_new_state
end
"""

msg = """
#{IO.ANSI.reset()}`from` macro does not accept a function as a value to `:to` anymore
Instead use the `event` macro passing the function as a guard option:
#{new_dsl}
"""

%__MODULE__{message: msg}
end
end
Loading

0 comments on commit e5a28d8

Please sign in to comment.