From a58e23858cde9de8856443c83ac4aed893690801 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Sat, 26 Aug 2023 20:51:59 -0300 Subject: [PATCH] Implements a Command input (#9) * correctly parses command and emit errors * correctly builds a CLI struct * split parser error --- .github/workflows/lint.yml | 4 +-- .github/workflows/test.yml | 3 +-- examples/escript/example.ex | 4 +-- lib/nexus.ex | 25 ++++-------------- lib/nexus/cli.ex | 16 +++++------ lib/nexus/command/input.ex | 16 +++++++++++ lib/nexus/command_dispatcher.ex | 29 ++++++++++++++++---- lib/nexus/failed_command_parsing.ex | 9 +++++++ lib/nexus/parser.ex | 41 +++++++++++++++++++++++++++++ mix.exs | 8 ++++-- 10 files changed, 113 insertions(+), 42 deletions(-) create mode 100644 lib/nexus/command/input.ex create mode 100644 lib/nexus/failed_command_parsing.ex create mode 100644 lib/nexus/parser.ex diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e915a6a..05d468e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: lint -on: [push, pull_request] +on: pull_request permissions: contents: read @@ -8,7 +8,7 @@ permissions: jobs: lint: runs-on: ubuntu-latest - name: Run tests + name: Check format and lint strategy: matrix: otp: ['25.1.2.1'] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f48eb6e..92cbdb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: test -on: [push, pull_request] +on: pull_request jobs: test: @@ -18,4 +18,3 @@ jobs: elixir-version: ${{matrix.elixir}} - run: mix deps.get - run: mix test - diff --git a/examples/escript/example.ex b/examples/escript/example.ex index b6cd8a4..5975307 100644 --- a/examples/escript/example.ex +++ b/examples/escript/example.ex @@ -21,8 +21,8 @@ defmodule Escript.Example do def version, do: "0.1.0" @impl true - def handle_input(:foo, _args) do - IO.puts("Running :foo command...") + def handle_input(:foo, input) do + IO.puts(inspect(input)) end Nexus.parse() diff --git a/lib/nexus.ex b/lib/nexus.ex index fdb15fb..03d9bba 100644 --- a/lib/nexus.ex +++ b/lib/nexus.ex @@ -32,7 +32,7 @@ defmodule Nexus do end """ - @type command :: {atom, Nexus.Command.t()} + @type command :: Nexus.Command.t() defmacro __using__(_opts) do quote do @@ -112,11 +112,12 @@ defmodule Nexus do """ defmacro parse do quote do + defstruct Enum.map(@commands, &{&1.name, nil}) + def __commands__, do: @commands - def run([name | args]) do - cmd = Enum.find(@commands, fn cmd -> to_string(cmd.name) == name end) - Nexus.CommandDispatcher.dispatch!(cmd, args) + def run(args) do + Nexus.CommandDispatcher.dispatch!(__MODULE__, args) end @spec parse(list(binary)) :: {:ok, Nexus.CLI.t()} | {:error, atom} @@ -147,22 +148,6 @@ defmodule Nexus do """ end - def parse_to(:string, value) do - to_string(value) - end - - def parse_to(:atom, value) do - String.to_existing_atom(value) - end - - def parse_to(:integer, value) do - String.to_integer(value) - end - - def parse_to(:float, value) do - String.to_float(value) - end - def __make_command__!(module, cmd_name, opts) do opts |> Keyword.put(:name, cmd_name) diff --git a/lib/nexus/cli.ex b/lib/nexus/cli.ex index 11791f0..4b4b1ca 100644 --- a/lib/nexus/cli.ex +++ b/lib/nexus/cli.ex @@ -4,6 +4,8 @@ defmodule Nexus.CLI do to be runned and also define helper functions to parse a single command againts a raw input. """ + alias Nexus.Command.Input + alias Nexus.Parser @callback version :: String.t() @callback banner :: String.t() @@ -21,16 +23,12 @@ defmodule Nexus.CLI do acc = {%{}, raw} {cli, _raw} = - Enum.reduce(cmds, acc, fn {cmd, spec}, {cli, raw} -> - {:ok, value} = parse_command({cmd, spec}, raw) - {Map.put(cli, cmd, value), raw} + Enum.reduce(cmds, acc, fn spec, {cli, raw} -> + {value, raw} = Parser.command_from_raw!(spec, raw) + input = Input.parse!(value, raw) + {Map.put(cli, spec.name, input), raw} end) - {:ok, cli} - end - - def parse_command({_cmd, spec}, raw) do - value = Nexus.parse_to(spec.type, raw) - {:ok, value} + {:ok, struct(module, cli)} end end diff --git a/lib/nexus/command/input.ex b/lib/nexus/command/input.ex new file mode 100644 index 0000000..3ecced9 --- /dev/null +++ b/lib/nexus/command/input.ex @@ -0,0 +1,16 @@ +defmodule Nexus.Command.Input do + @moduledoc """ + Define a structure to easy pattern matching the input + on commands dispatched + """ + + @type t :: %__MODULE__{value: term, raw: list(binary())} + + @enforce_keys ~w(value raw)a + defstruct value: nil, raw: nil + + @spec parse!(term, list(binary())) :: Nexus.Command.Input.t() + def parse!(value, raw) do + %__MODULE__{value: value, raw: raw} + end +end diff --git a/lib/nexus/command_dispatcher.ex b/lib/nexus/command_dispatcher.ex index a25bc2b..052e001 100644 --- a/lib/nexus/command_dispatcher.ex +++ b/lib/nexus/command_dispatcher.ex @@ -2,12 +2,31 @@ defmodule Nexus.CommandDispatcher do @moduledoc false alias Nexus.Command + alias Nexus.Command.Input + alias Nexus.Parser - @spec dispatch!(Nexus.command(), list(binary)) :: :ok - def dispatch!({cmd, %Command{} = spec}, raw) do - {:ok, cli} = Nexus.CLI.parse_command({cmd, spec}, raw) - spec.module.handle_input(cmd, cli) + @spec dispatch!(Nexus.command() | binary | list(binary), list(binary)) :: term - :ok + def dispatch!(%Command{} = spec, raw) do + {value, raw} = Parser.command_from_raw!(spec, raw) + input = Input.parse!(value, raw) + spec.module.handle_input(spec.name, input) + end + + def dispatch!(module, args) when is_binary(args) do + dispatch!(module, String.split(args, ~r/\s/)) + end + + def dispatch!(module, args) when is_list(args) do + cmd = + Enum.find(module.__commands__(), fn %{name: n} -> + to_string(n) == List.first(args) + end) + + if cmd do + dispatch!(cmd, args) + else + raise "Command #{hd(args)} not found in #{module}" + end end end diff --git a/lib/nexus/failed_command_parsing.ex b/lib/nexus/failed_command_parsing.ex new file mode 100644 index 0000000..a3a3e2e --- /dev/null +++ b/lib/nexus/failed_command_parsing.ex @@ -0,0 +1,9 @@ +# credo:disable-for-next-line +defmodule Nexus.FailedCommandParsing do + defexception [:message] + + @impl true + def exception(reason) do + %__MODULE__{message: "Error parsing command: #{reason}"} + end +end diff --git a/lib/nexus/parser.ex b/lib/nexus/parser.ex new file mode 100644 index 0000000..b2526a4 --- /dev/null +++ b/lib/nexus/parser.ex @@ -0,0 +1,41 @@ +defmodule Nexus.Parser do + @moduledoc "Should parse the command and return the value" + + alias Nexus.Command + alias Nexus.FailedCommandParsing, as: Error + + @spec command_from_raw!(Command.t(), binary | list(binary)) :: {term, list(binary)} + def command_from_raw!(cmd, raw) when is_binary(raw) do + command_from_raw!(cmd, String.split(raw, ~r/\s/)) + end + + def command_from_raw!(%Command{name: name, type: t}, args) when is_list(args) do + ns = to_string(name) + + case args do + [^ns, value | args] -> {string_to!(value, t), args} + args -> raise "Failed to parse command #{ns} with args #{inspect(args)}" + end + end + + defp string_to!(raw, :string), do: raw + + defp string_to!(raw, :integer) do + case Integer.parse(raw) do + {int, ""} -> int + _ -> raise Error, "#{raw} is not a valid integer" + end + end + + defp string_to!(raw, :float) do + case Float.parse(raw) do + {float, ""} -> float + _ -> raise Error, "#{raw} is not a valid float" + end + end + + # final user should not be used very often + defp string_to!(raw, :atom) do + String.to_atom(raw) + end +end diff --git a/mix.exs b/mix.exs index aafab4f..e021fc8 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Nexus.MixProject do use Mix.Project - @version "0.1.0" + @version "0.2.0" @source_url "https://github.com/zoedsoupe/nexus" def project do @@ -15,7 +15,8 @@ defmodule Nexus.MixProject do escript: [main_module: Escript.Example], package: package(), source_url: @source_url, - description: description() + description: description(), + elixirc_paths: elixirc_paths(Mix.env()) ] end @@ -23,6 +24,9 @@ defmodule Nexus.MixProject do [extra_applications: [:logger]] end + defp elixirc_paths(:dev), do: ["lib", "examples"] + defp elixirc_paths(_), do: ["lib"] + defp deps do [ {:ex_doc, "~> 0.27", only: :dev, runtime: false},