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..735f29d --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# 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"). +commandex-*.tar + +.iex.exs diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b819db --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Commandex + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `commandex` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:commandex, "~> 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/commandex](https://hexdocs.pm/commandex). + diff --git a/lib/commandex.ex b/lib/commandex.ex new file mode 100644 index 0000000..89d025a --- /dev/null +++ b/lib/commandex.ex @@ -0,0 +1,81 @@ +defmodule Commandex do + @moduledoc """ + Documentation for Commandex. + """ + + @doc false + defmacro __using__(_opts) do + prelude = + quote do + # @after_compile Commandex + Module.register_attribute(__MODULE__, :struct_fields, accumulate: true) + Module.put_attribute(__MODULE__, :struct_fields, {:success, false}) + Module.put_attribute(__MODULE__, :struct_fields, {:error, nil}) + Module.put_attribute(__MODULE__, :struct_fields, {:halted, false}) + end + + postlude = + quote unquote: false do + params = for key <- Module.get_attribute(__MODULE__, :params), into: %{}, do: {key, nil} + data = for key <- Module.get_attribute(__MODULE__, :data), into: %{}, do: {key, nil} + + Module.put_attribute(__MODULE__, :struct_fields, {:params, params}) + Module.put_attribute(__MODULE__, :struct_fields, {:data, data}) + defstruct @struct_fields + import Commandex + end + + quote do + unquote(prelude) + unquote(postlude) + + def new(opts) do + Commandex.parse_params(%__MODULE__{}, opts) + end + + def run(command) do + pipeline() + |> Enum.reduce_while(command, fn fun, acc -> + case acc do + %{halted: false} -> {:cont, fun.(acc, acc.params, acc.data)} + _ -> {:halt, acc} + end + end) + |> Commandex.maybe_mark_successful() + end + end + end + + def maybe_mark_successful(%{halted: false} = command), do: %{command | success: true} + def maybe_mark_successful(command), do: command + + @doc false + def parse_params(%{params: p} = struct, params) when is_list(params) do + params = for {key, _} <- p, into: %{}, do: {key, Keyword.get(params, key)} + %{struct | params: params} + end + + def parse_params(%{params: p} = struct, %{} = params) do + params = for {key, _} <- p, into: %{}, do: {key, get_param(params, key)} + %{struct | params: params} + end + + defp get_param(params, key) do + case Map.get(params, key) do + nil -> Map.get(params, to_string(key)) + val -> val + end + end + + def put_data(%{data: data} = command, key, val) do + %{command | data: Map.put(data, key, val)} + end + + def put_error(command, error) do + %{command | error: error} + end + + def halt(command) do + %{command | halted: true} + end +end diff --git a/lib/register_user.ex b/lib/register_user.ex new file mode 100644 index 0000000..d88c6eb --- /dev/null +++ b/lib/register_user.ex @@ -0,0 +1,27 @@ +defmodule Commandex.RegisterUser do + @params ~w(email password)a + @data ~w(user auth)a + + use Commandex + + def pipeline do + [ + &create_user/3, + &record_auth_attempt/3 + ] + end + + def create_user(command, %{password: nil} = _params, data) do + command + |> put_error(:no_password) + |> halt() + end + + def create_user(command, %{email: email} = _params, data) do + put_data(command, :user, %{email: email}) + end + + def record_auth_attempt(command, _params, _data) do + put_data(command, :auth, true) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..36000bd --- /dev/null +++ b/mix.exs @@ -0,0 +1,21 @@ +defmodule Commandex.MixProject do + use Mix.Project + + @version "0.1.0" + + def project do + [ + app: :commandex, + deps: [], + elixir: "~> 1.9", + start_permanent: Mix.env() == :prod, + version: @version + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end +end diff --git a/test/commandex_test.exs b/test/commandex_test.exs new file mode 100644 index 0000000..0e527ce --- /dev/null +++ b/test/commandex_test.exs @@ -0,0 +1,4 @@ +defmodule CommandexTest do + use ExUnit.Case + doctest Commandex +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()