Hello. I 'm creating a mix project which builds a finite state machine on redis. While I’m writing code of the repository, I referred a repository finist which is written in ruby.
Implementation
defmodule Dfm do
@moduledoc """
Documentation for `Dfm`.
"""
require Logger
@script """
local curr = redis.call("GET", KEYS[1])
local next = redis.call("HGET", KEYS[2], curr)
if next then
redis.call("SET", KEYS[1], next)
return { next, true }
else
return { curr, false }
end
"""
@redis_host "localhost"
@redis_port 6379
defp conn() do
with {:ok, conn} <- Redix.start_link(host: @redis_host, port: @redis_port) do
conn
else
error -> raise "Failed to connect to #{@redis_host}:#{@redis_port} #{error}"
end
end
def flushall() do
conn = conn()
Redix.command(conn, ["FLUSHALL"])
Logger.info("Flushed all")
end
@doc """
Initializes state of automaton.
"""
@spec initialize(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
def initialize(key_name, db_index, initial_state) do
conn = conn()
name = name(key_name)
Redix.command!(conn, ["SELECT", db_index])
Redix.command!(conn, ["SET", name, initial_state, "NX"])
end
@spec name(String.t()) :: String.t()
defp name(key_name), do: "finite:#{key_name}"
@spec event_key(String.t(), String.t()) :: String.t()
defp event_key(key_name, event), do: "#{key_name}:#{event}"
@doc """
Defines how automaton changes the state.
"""
@spec on(String.t(), integer(), String.t(), String.t(), String.t()) :: Redix.Protocol.redis_value()
def on(key_name, db_index, event, current_state, next_state) do
conn = conn()
Redix.command!(conn, ["SELECT", db_index])
Redix.command!(conn, ["HSET", event_key(key_name, event), current_state, next_state])
end
@doc """
Removes a pattern of state change.
"""
@spec rm(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
def rm(key_name, db_index, event) do
conn = conn()
Redix.command!(conn, ["SELECT", db_index])
Redix.command!(conn, ["HDEL", event_key(key_name, event)])
end
@doc """
Return current state.
"""
@spec state(String.t(), integer()) :: Redix.Protocol.redis_value()
def state(key_name, db_index) do
conn = conn()
Redix.command!(conn, ["SELECT", db_index])
Redix.command!(conn, ["GET", name(key_name)])
end
@spec send_event(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
defp send_event(key_name, db_index, event) do
conn = conn()
Redix.command!(conn, ["SELECT", db_index])
Redix.command!(conn, ["EVAL", @script, 2, name(key_name), event_key(key_name, event)])
end
@doc """
Triggers state change.
"""
@spec trigger(String.t(), integer(), String.t()) :: {:ok, String.t()} | {:error, String.t()}
def trigger(key_name, db_index, event) do
[state, result] = send_event(key_name, db_index, event)
do_trigger(state, result)
end
defp do_trigger(state, nil), do: {:error, state}
defp do_trigger(state, _), do: {:ok, state}
end
Test
defmodule DfmTest do
use ExUnit.Case
doctest Dfm
describe "dfm" do
test "simple automaton" do
Dfm.flushall()
1..5
|> Enum.to_list()
|> Enum.each(fn n ->
key_name = "user#{n}"
db_index = 15
state1 = "a"
state2 = "b"
state3 = "c"
trigger1 = "x"
trigger2 = "y"
invalid_trigger = "invalid"
# NOTE: Define state changes
Dfm.initialize(key_name, db_index, state1)
Dfm.on(key_name, db_index, trigger1, state1, state2)
Dfm.on(key_name, db_index, trigger1, state2, state3)
Dfm.on(key_name, db_index, trigger1, state3, state1)
Dfm.on(key_name, db_index, trigger2, state1, state3)
assert Dfm.state(key_name, db_index) == state1
assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
assert Dfm.state(key_name, db_index) == state2
assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
assert Dfm.state(key_name, db_index) == state3
assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
assert Dfm.state(key_name, db_index) == state1
assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger2)
assert Dfm.state(key_name, db_index) == state3
# NOTE: Invalid patterns
assert {:error, state3} = Dfm.trigger(key_name, db_index, trigger2)
assert {:error, state3} = Dfm.trigger(key_name, db_index, invalid_trigger)
end)
end
end
end
That’s all of this repository. I think it works as a finite state machine properly. But I’m not a skilled engineer, so I’d like to show the codes to everyone. I want advice for the repository!
Thank you.