Skip to content

Commit

Permalink
feat: subcommands prototype implementation (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Sep 18, 2023
1 parent 37dc808 commit 48b39d4
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 10 deletions.
14 changes: 14 additions & 0 deletions examples/escript/example.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ defmodule Escript.Example do
defcommand :foo, required: true, type: :string, doc: "Command that receives a string as argument and prints it."
defcommand :fizzbuzz, type: {:enum, ~w(fizz buzz)a}, doc: "Fizz bUZZ", required: true

defcommand :foo_bar, type: :null, doc: "Teste" do
defcommand :foo, default: "hello", doc: "Hello"
defcommand :bar, default: "hello", doc: "Hello"
end

@impl true
def version, do: "0.1.0"
Expand All @@ -35,6 +39,16 @@ defmodule Escript.Example do
IO.puts("fizz")
end

def handle_input(:foo_bar, %{value: _, subcommand: :foo}) do
# do something wth "foo" value
:ok
end

def handle_input(:foo_bar, %{value: _, subcommand: :bar}) do
# do something wth "bar" value
:ok
end

Nexus.help()
Nexus.parse()

Expand Down
27 changes: 25 additions & 2 deletions lib/nexus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ defmodule Nexus do
defmacro __using__(_opts) do
quote do
Module.register_attribute(__MODULE__, :commands, accumulate: true)
Module.register_attribute(__MODULE__, :subcommands, accumulate: true)

import Nexus, only: [defcommand: 2]
import Nexus, only: [defcommand: 2, defcommand: 3]
require Nexus

@behaviour Nexus.CLI
Expand All @@ -65,11 +66,33 @@ defmodule Nexus do
"""
@spec defcommand(atom, keyword) :: Macro.t()
defmacro defcommand(cmd, opts) do
quote do
quote location: :keep do
@commands Nexus.__make_command__!(__MODULE__, unquote(cmd), unquote(opts))
end
end

defmacro defcommand(cmd, opts, do: ast) do
subcommands = build_subcommands(ast)

quote location: :keep do
@commands Nexus.__make_command__!(
__MODULE__,
unquote(cmd),
Keyword.put(unquote(opts), :subcommands, unquote(subcommands))
)
end
end

defp build_subcommands({:__block__, _, subs}) do
Enum.map(subs, &build_subcommands/1)
end

defp build_subcommands({:defcommand, _, [cmd, opts]}) do
quote do
Nexus.__make_command__!(__MODULE__, unquote(cmd), unquote(opts))
end
end

@doc """
Generates a default `help` command for your CLI. It uses the
optional `banner/0` callback from `Nexus.CLI` to complement
Expand Down
11 changes: 9 additions & 2 deletions lib/nexus/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ defmodule Nexus.Command do
required: boolean,
name: atom,
default: term,
doc: String.t()
doc: String.t(),
subcommands: [Nexus.Command.t()]
}

@enforce_keys ~w(module type name)a
defstruct module: nil, required: true, type: :string, name: nil, default: nil, doc: ""
defstruct module: nil,
required: true,
type: :string,
name: nil,
default: nil,
doc: "",
subcommands: []

@spec parse!(keyword) :: Nexus.Command.t()
def parse!(attrs) do
Expand Down
4 changes: 2 additions & 2 deletions lib/nexus/command/input.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ defmodule Nexus.Command.Input do
on commands dispatched
"""

@type t :: %__MODULE__{value: term, raw: binary}
@type t :: %__MODULE__{value: term, raw: binary, subcommand: atom}

@enforce_keys ~w(value raw)a
defstruct value: nil, raw: nil
defstruct value: nil, raw: nil, subcommand: nil

@spec parse!(term, binary) :: Nexus.Command.Input.t()
def parse!(value, raw) do
Expand Down
24 changes: 22 additions & 2 deletions lib/nexus/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,28 @@ defmodule Nexus.Parser do
|> String.trim_leading()
|> parse_command(cmd)
|> case do
{:ok, input} -> input
{:error, _} -> raise Error, "Failed to parse command #{inspect(cmd)}"
{:ok, input} ->
input

{:error, _} ->
raise Error, "Failed to parse command #{inspect(cmd)}"

:error ->
raise Error,
"Failed to parse command #{inspect(cmd)} with subcommands #{inspect(cmd.subcommands)}"
end
end

defp parse_subcommand(input, cmd) do
case parse_command(input, cmd) do
{:ok, input} -> {:halt, {:ok, Map.put(input, :subcommand, cmd.name)}}
{:error, _} -> {:cont, :error}
end
end

defp parse_command(input, %Command{type: :null, subcommands: [_ | _] = subs} = cmd) do
with {:ok, {_, rest}} <- literal(input, cmd.name) do
Enum.reduce_while(subs, :error, fn sub, _acc -> parse_subcommand(rest, sub) end)
end
end

Expand Down
4 changes: 2 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.3.1"
@version "0.4.0"
@source_url "https://github.com/zoedsoupe/nexus"

def project do
Expand Down Expand Up @@ -40,7 +40,7 @@ defmodule Nexus.MixProject do
licenses: ["WTFPL"],
contributors: ["zoedsoupe"],
links: %{"GitHub" => @source_url},
files: ~w(lib/nexus lib/nexus.ex LICENSE README.md mix.*)
files: ~w(lib/nexus lib/nexus.ex LICENSE README.md mix.* examples)
}
end

Expand Down

0 comments on commit 48b39d4

Please sign in to comment.