Skip to content

Commit

Permalink
Implements a Command input (#9)
Browse files Browse the repository at this point in the history
* correctly parses command and emit errors

* correctly builds a CLI struct

* split parser error
  • Loading branch information
Zoey de Souza Pessanha committed Aug 26, 2023
1 parent ae5c7fe commit a58e238
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 42 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: lint

on: [push, pull_request]
on: pull_request

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
name: Run tests
name: Check format and lint
strategy:
matrix:
otp: ['25.1.2.1']
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: test

on: [push, pull_request]
on: pull_request

jobs:
test:
Expand All @@ -18,4 +18,3 @@ jobs:
elixir-version: ${{matrix.elixir}}
- run: mix deps.get
- run: mix test

4 changes: 2 additions & 2 deletions examples/escript/example.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
25 changes: 5 additions & 20 deletions lib/nexus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 7 additions & 9 deletions lib/nexus/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
16 changes: 16 additions & 0 deletions lib/nexus/command/input.ex
Original file line number Diff line number Diff line change
@@ -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
29 changes: 24 additions & 5 deletions lib/nexus/command_dispatcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions lib/nexus/failed_command_parsing.ex
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions lib/nexus/parser.ex
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,14 +15,18 @@ 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

def application 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},
Expand Down

0 comments on commit a58e238

Please sign in to comment.