Skip to content

Commit

Permalink
improve documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Zoey de Souza Pessanha committed Jul 24, 2023
1 parent cf83695 commit dae9c2a
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 7 deletions.
75 changes: 70 additions & 5 deletions lib/nexus.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
defmodule Nexus do
@moduledoc """
Use this module in another module to mark it for
command and documentation formatting.
Nexus can be used to define simple to complex CLI applications.
The main component of Nexus is the macro `defcommand/2`, used
to register CLI commands. Notice that the module that uses `Nexus`
is defined as a complete CLI, with own commands and logic.
To define a command you need to name it and pass some options:
- `:type`: the argument type to be parsed to. It can be `:string` (default),
`:integer`, `:float`, `{:enum, list(option)}`. The absense of this option
will define a command without arguments, which can be used to define a subcommand
group.
- `:required?`: defines if the presence of the command is required or not. All commands are required by default.
## Usage
defmodule MyCLI do
use Nexus
defcommand :foo, type: :string, required?: true
@impl true
def handle_input(:foo, _args) do
IO.puts("Hello :foo command!")
end
Nexus.parse()
__MODULE__.run(System.argv())
end
"""

@type command :: {atom, Nexus.Command.t()}
Expand All @@ -18,14 +46,15 @@ defmodule Nexus do
end

@doc """
Like `def/2`, but the generates a function that can be invoked
Like `def/2`, but registers a command that can be invoked
from the command line. The `@doc` module attribute and the
arguments metadata are used to generate the CLI options.
Each defined command produces events that can be handled using
the `Nexus.Handle` behaviour, where the event is the command
name as an atom.
the `Nexus.CLI` behaviour, where the event is the command
name as an atom and the second argument is a list of arguments.
"""
@spec defcommand(atom, keyword) :: Macro.t()
defmacro defcommand(cmd, opts) do
quote do
command =
Expand All @@ -37,6 +66,14 @@ defmodule Nexus do
end
end

@doc """
Generates a default `help` command for your CLI. It uses the
optional `banner/0` callback from `Nexus.CLI` to complement
description.
You can also define your own `help` command, copying the `quote/2`
block of this macro.
"""
defmacro help do
quote do
Nexus.defcommand(:help, type: :string, required?: false)
Expand All @@ -48,6 +85,34 @@ defmodule Nexus do
end
end

@doc """
Generates three functions that can be used to manage and run
your CLI.
### `__commands__/0`
Return all commands that were defined into your CLI module.
### `run/1`
Run your CLI against argv content. Notice that this function only runs
a single command and returns `:ok`. It can be used to easily define
mix tasks.
Also this function expects that the `handle_input/2` callback from `Nexus.CLI`
would have some implementation for the a comand `N` that would be parsed.
### `parse/1`
Build a CLI based on argv content. It can be used if you want to manage
your CLI or decide how you want to execute functions. It builds a map
where given commands and options parsed will be keys and those values.
#### Example
{:ok, cli} = MyCLI.parse(System.argv)
cli.mycommand # `arg` to `mycommand`
"""
defmacro parse do
quote do
def __commands__, do: @commands
Expand Down
6 changes: 5 additions & 1 deletion lib/nexus/cli.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
defmodule Nexus.CLI do
@moduledoc false
@moduledoc """
Define callback that a CLI module needs to follow to be able
to be runned and also define helper functions to parse a single
command againts a raw input.
"""

@callback version :: String.t()
@callback banner :: String.t()
Expand Down
5 changes: 4 additions & 1 deletion lib/nexus/command.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
defmodule Nexus.Command do
@moduledoc false
@moduledoc """
Defines a command entry for a CLI module. It also
implements some basic validations.
"""

require Logger

Expand Down
8 changes: 8 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Nexus.MixProject do
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps(),
docs: docs(),
escript: [main_module: Escript.Example],
package: package(),
source_url: @source_url,
Expand Down Expand Up @@ -38,6 +39,13 @@ defmodule Nexus.MixProject do
}
end

defp docs do
[
main: "Nexus",
extras: ["README.md"]
]
end

defp description do
"""
An `Elixir` library to write command line apps in a cleaner and elegant way!
Expand Down
26 changes: 26 additions & 0 deletions test/nexus/runtime_storage_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Nexus.RuntimeStorageTest do
use ExUnit.Case, async: true

setup do
start_supervised!({Nexus.RuntimeStorage, :nexus_test_storage})
:ok
end

describe "read/1" do
test "reading a non existing value should return nil" do
refute Nexus.RuntimeStorage.read(:do_not_exists)
end

test "reading an existing value should return it" do
assert :ets.insert(:nexus_test_storage, {:teste, true})
assert Nexus.RuntimeStorage.read(:teste)
end
end

describe "insert/2" do
test "inserting a value should persist it into ETS" do
assert :ok = Nexus.RuntimeStorage.insert(:teste, true)
assert Nexus.RuntimeStorage.read(:teste)
end
end
end
38 changes: 38 additions & 0 deletions test/nexus_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule NexusTest do
use ExUnit.Case, async: true

import Nexus

setup do
start_supervised!({Nexus.RuntimeStorage, :nexus_test_storage})
:ok
end

describe "defcommand/2" do
test "defining a command should insert it into ETS" do
defcommand(:hello, required: false, type: :atom)
assert Nexus.RuntimeStorage.read(:hello)
end

test "defining multiple command should insert all into ETS" do
defcommand(:foo, required: false, type: :string)
defcommand(:bar, [])
assert length(Nexus.fetch_cli_commands(__MODULE__)) == 2
end
end

describe "fetch_cli_commands/1" do
test "when there's no command for supplied module" do
assert Enum.empty?(Nexus.fetch_cli_commands(DoNotExist))
end

test "when module exists but do not define any command" do
assert Enum.empty?(Nexus.fetch_cli_commands(Nexus))
end

test "when module exists and define some command" do
defcommand(:teste, type: :string, required: false)
assert length(Nexus.fetch_cli_commands(__MODULE__)) == 1
end
end
end

0 comments on commit dae9c2a

Please sign in to comment.