diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc4d6f8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +Copyright 2020 Subvisual + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index a525996..7de0cef 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ end defmodule App.StateMachine do defstruct [:state, :data] - use Fsmx, transitions: %{ + use Fsmx.Struct, transitions: %{ "one" => ["two", "three"], "two" => ["three", "four"], "three" => "four" @@ -127,7 +127,7 @@ all that business logic into a separate module: defmodule App.StateMachine do defstruct [:state] - use Fsmx, fsm: App.Logic + use Fsmx.Struct, fsm: App.Logic end defmodule App.BusinessLogic do @@ -162,7 +162,7 @@ defmodule App.StateMachineSchema do field :data, :map end - use Fsmx, transitions: %{ + use Fsmx.Struct, transitions: %{ "one" => ["two", "three"], "two" => ["three", "four"], "three" => "four" diff --git a/lib/fsmx.ex b/lib/fsmx.ex index b5b1ed9..f6b0222 100644 --- a/lib/fsmx.ex +++ b/lib/fsmx.ex @@ -1,4 +1,8 @@ defmodule Fsmx do + @moduledoc """ + """ + + @spec transition(struct(), binary()) :: {:ok, struct} | {:error, any} def transition(struct, new_state) do with {:ok, struct} <- before_transition(struct, new_state) do {:ok, %{struct | state: new_state}} @@ -6,7 +10,8 @@ defmodule Fsmx do end if Code.ensure_loaded?(Ecto) do - def transition_changeset(%mod{state: state} = schema, new_state, params \\ %{}) do + @spec transition_changeset(struct(), binary, map) :: {:ok, Ecto.Changeset.t()} | {:error, any} + def(transition_changeset(%mod{state: state} = schema, new_state, params \\ %{})) do fsm = mod.__fsmx__() with {:ok, schema} <- before_transition(schema, new_state) do @@ -17,6 +22,8 @@ defmodule Fsmx do end end + @spec transition_multi(Ecto.Multi.t(), struct(), any, binary, map) :: + {:ok, Ecto.Multi.t()} | {:error, any} def transition_multi(multi, %mod{state: state} = schema, id, new_state, params \\ %{}) do fsm = mod.__fsmx__() diff --git a/lib/fsmx/fsm.ex b/lib/fsmx/fsm.ex index 48e9e12..6f67e59 100644 --- a/lib/fsmx/fsm.ex +++ b/lib/fsmx/fsm.ex @@ -1,4 +1,32 @@ defmodule Fsmx.Fsm do + @moduledoc """ + Holds transition and callback logic for finite state machines + + By default, when using `use Fsmx.Struct`, this is automatically included as well. + Specifying `use Fsmx.Struct, fsm: MyApp.Fsm` allows you to decouple this, though + + ```elixir + defmodule MyApp.Struct do + defstruct [:state] + end + + defmodule MyApp.Fsm do + use Fsmx.Fsm, transitions: %{} + + def before_transition(struct, _from, _to) do + # ... + end + end + """ + + @callback before_transition(struct, binary, binary) :: {:ok, struct} | {:error, any} + + if Code.ensure_loaded?(Ecto) do + @callback transition_changeset(struct, binary, binary) :: + {:ok, Ecto.Changeset.t()} | {:error, any} + @callback after_transition_multi(struct, binary, binary) :: {:ok, struct} | {:error, any} + end + defmacro __using__(opts \\ []) do quote do @before_compile unquote(__MODULE__) @@ -13,8 +41,11 @@ defmodule Fsmx.Fsm do defmacro __before_compile__(_env) do quote generated: false do def before_transition(struct, _from, _to), do: {:ok, struct} - def transition_changeset(changeset, _from, _to, _params), do: {:ok, changeset} - def after_transition_multi(struct, _from, _to), do: {:ok, struct} + + if Code.ensure_loaded?(Ecto) do + def transition_changeset(changeset, _from, _to, _params), do: {:ok, changeset} + def after_transition_multi(struct, _from, _to), do: {:ok, struct} + end end end end diff --git a/lib/fsmx/struct.ex b/lib/fsmx/struct.ex index c9d4d5d..760ed79 100644 --- a/lib/fsmx/struct.ex +++ b/lib/fsmx/struct.ex @@ -1,4 +1,20 @@ defmodule Fsmx.Struct do + @moduledoc """ + Main module to include finite-state machine logic into your struct/schema + + It assumes a `:state` string field exists in your model + + Basic usage: + + ```elixir + defmodule MyApp.Struct do + defstruct [:state] + + use Fsmx.Struct, transitions: %{} + end + ``` + """ + defmacro __using__(opts \\ []) do quote do @fsm Keyword.get(unquote(opts), :fsm, __MODULE__) diff --git a/mix.exs b/mix.exs index 180f9c7..457528f 100644 --- a/mix.exs +++ b/mix.exs @@ -1,15 +1,20 @@ defmodule Fsmx.MixProject do use Mix.Project + @version "0.1.0" + def project do [ app: :fsmx, - version: "0.1.0", + description: description(), + version: @version, elixir: "~> 1.8", applications: applications(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, - deps: deps() + package: package(), + deps: deps(), + docs: docs() ] end @@ -26,10 +31,37 @@ defmodule Fsmx.MixProject do [ {:postgrex, ">= 0.0.0", only: :test}, {:ecto, ">= 3.0.0", optional: true}, - {:ecto_sql, ">= 3.0.0", optional: true} + {:ecto_sql, ">= 3.0.0", optional: true}, + {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, + {:dialyxir, "~> 1.0.0", only: :dev, runtime: false} ] end defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] + + defp package do + end + + defp description do + "A Finite-state machine implementation in Elixir, with opt-in Ecto friendliness." + end + + defp package do + [ + maintainers: ["Miguel Palhas"], + licenses: ["ISC"], + links: %{"GitHub" => "https://github.com/subvisual/fsmx"}, + files: ~w(.formatter.exs mix.exs README.md lib) + ] + end + + defp docs do + [ + extras: ["README.md"], + main: "readme", + source_url: "https://github.com/subvisual/fsmx", + source_ref: "v#{@version}" + ] + end end diff --git a/mix.lock b/mix.lock index 66114f9..d77006a 100644 --- a/mix.lock +++ b/mix.lock @@ -2,9 +2,16 @@ "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, + "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, + "earmark": {:hex, :earmark, "1.4.5", "62ffd3bd7722fb7a7b1ecd2419ea0b458c356e7168c1f5d65caf09b4fbdd13c8", [:mix], [], "hexpm", "b7d0e6263d83dc27141a523467799a685965bf8b13b6743413f19a7079843f4f"}, "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "etso": {:hex, :etso, "0.1.1", "bfc5e30483d397774a64981fc93511d3ed0dcb9d19bc3cba03df7b9555a68636", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "194ed738ce2e1197b326071b42dc092d6df4a7492575a63d6b797a0c59727088"}, + "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"}, + "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, }