From 041ead08e9b8d6180d2a9d0cef2096755bc83f80 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Wed, 24 Jun 2020 00:27:46 +0100 Subject: [PATCH] :sunrise: --- .formatter.exs | 4 +++ .gitignore | 24 +++++++++++++++ README.md | 21 +++++++++++++ lib/fsmx.ex | 23 ++++++++++++++ lib/fsmx/struct.ex | 7 +++++ mix.exs | 28 +++++++++++++++++ test/fsmx_test.exs | 33 +++++++++++++++++++++ test/support/test_structs/simple.ex | 5 ++++ test/support/test_structs/with_callbacks.ex | 9 ++++++ test/test_helper.exs | 1 + 10 files changed, 155 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/fsmx.ex create mode 100644 lib/fsmx/struct.ex create mode 100644 mix.exs create mode 100644 test/fsmx_test.exs create mode 100644 test/support/test_structs/simple.ex create mode 100644 test/support/test_structs/with_callbacks.ex create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fbd23c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +fsmx-*.tar + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab098fe --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Fsmx + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `fsmx` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:fsmx, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/fsmx](https://hexdocs.pm/fsmx). + diff --git a/lib/fsmx.ex b/lib/fsmx.ex new file mode 100644 index 0000000..f56f91b --- /dev/null +++ b/lib/fsmx.ex @@ -0,0 +1,23 @@ +defmodule Fsmx do + @type state_t :: binary + + def transition(%mod{state: state} = struct, new_state) do + transitions = mod.__fsmx__(:transitions) + + if valid_transition?(state, new_state, transitions) do + {:ok, %{struct | state: new_state}} + else + {:error, "invalid transition of #{mod} from #{state} to #{new_state}"} + end + end + + defp valid_transition?(state, new_state, transitions) do + transitions + |> Map.get(state, []) + |> is_or_contains?(new_state) + end + + defp is_or_contains?(state, state), do: true + defp is_or_contains?(states, state) when is_list(states), do: Enum.member?(states, state) + defp is_or_contains?(_, _), do: false +end diff --git a/lib/fsmx/struct.ex b/lib/fsmx/struct.ex new file mode 100644 index 0000000..9e56bc5 --- /dev/null +++ b/lib/fsmx/struct.ex @@ -0,0 +1,7 @@ +defmodule Fsmx.Struct do + defmacro __using__(opts) do + quote do + def __fsmx__(:transitions), do: Keyword.fetch!(unquote(opts), :transitions) + end + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..440e0ec --- /dev/null +++ b/mix.exs @@ -0,0 +1,28 @@ +defmodule Fsmx.MixProject do + use Mix.Project + + def project do + [ + app: :fsmx, + version: "0.1.0", + elixir: "~> 1.9", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [] + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] +end diff --git a/test/fsmx_test.exs b/test/fsmx_test.exs new file mode 100644 index 0000000..e3c44d5 --- /dev/null +++ b/test/fsmx_test.exs @@ -0,0 +1,33 @@ +defmodule FsmxTest do + use ExUnit.Case + doctest Fsmx + + alias Fsmx.TestStructs.{Simple, WithCallbacks} + + describe "transition/2" do + test "can do simple transitions" do + one = %Simple{state: "1"} + + {:ok, two} = Fsmx.transition(one, "2") + + assert %Simple{state: "2"} = two + + {:ok, three} = Fsmx.transition(two, "3") + + assert %Simple{state: "3"} = three + end + + test "fails to perform invalid transitions" do + one = %Simple{state: "1"} + + assert {:error, msg} = Fsmx.transition(one, "3") + + assert msg == "invalid transition of Elixir.Fsmx.TestStructs.Simple from 1 to 3" + end + end + + describe "transition/2 with callbacks" do + test "calls before_transition/2 on struct" do + end + end +end diff --git a/test/support/test_structs/simple.ex b/test/support/test_structs/simple.ex new file mode 100644 index 0000000..ae0ef86 --- /dev/null +++ b/test/support/test_structs/simple.ex @@ -0,0 +1,5 @@ +defmodule Fsmx.TestStructs.Simple do + defstruct [:state] + + use Fsmx.Struct, transitions: %{"1" => ["2"], "2" => ["3"]} +end diff --git a/test/support/test_structs/with_callbacks.ex b/test/support/test_structs/with_callbacks.ex new file mode 100644 index 0000000..a9d2129 --- /dev/null +++ b/test/support/test_structs/with_callbacks.ex @@ -0,0 +1,9 @@ +defmodule Fsmx.TestStructs.WithCallbacks do + defstruct [:state] + + use Fsmx.Struct, transitions: %{"1" => ["2"], "2" => ["3"]} + + def before_transition(struct, state) do + IO.inspect(struct, state) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()