# Stopsel
A library inspired by [plug](https://hex.pm/packages/plug) and [phoenix routers](https://hex.pm/packages/phoenix) for parsing text messages like commands.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `stopsel` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:stopsel, "~> 0.1.0"}
]
end
```
## Define a router
Stopsel tries to be lightweight with its usage of Macros. Just import `Stopsel.Builder` and start defining your router.
```elixir
defmodule MyApp.Router do
import Stopsel.Builder
import MyApp.NumberUtils,
only: [parse_number: 2],
warn: false
# All of our commands are defined within this router block "MyApp" will
# be the initial scope of all of our commands.
router MyApp do
# This defines the command "hello".
# Defining the command ":hello" here aliases the command under the
# module "MyApp" and demands that the function "&MyApp.hello/2" exists.
# This command will match against the text-message "hello"
command :hello
# We can scope commands using a path and alias them further.
# In this case all following commands will be defined under the path
# "calculator|:a" and aliased to the module "MyApp.Calculator".
# Segments of a command path are seperated with "|".
# A segment that starts with ":" will not be used as Text to match
# against, but as a parameter that will we can use later on.
scope "calculator|:a", Calculator do
# A stopsel is similar to a plug.
# If you are not familiar with the library plug, a "plug" is a module
# or a function that your request will pass through. In this case the
# text message runs through the plug "parse_message" twice, with different
# configurations, before the matching command will be executed.
# A stopsel generally expects a module or the name of an imported function.
stopsel :parse_number, :a
stopsel :parse_number, :b
# Here we aliased the commands to not match against "add", "subtract", ...
# but against "+", "-", ... and have an aditional parameter called "b"
command :add, "+|:b"
command :subract, "-|:b"
command :multiply, "*|:b"
command :divide, "/|:b"
end
end
end
```
## Implementing the command
Every command defined must be defined as a function in the currently aliased Module. Here's how the commands for `MyApp` could be implemented.
```elixir
defmodule MyApp do
def hello(_message, _params) do
IO.puts "Hello world!"
end
end
```
As seen above, a function that handles a command must accept 2 arguments
* A message (`Stopsel.Message` struct)
* a map that contains the parameters defined for the matched route.
In the example above we have created a route like this.
```elixir
scope "calculator|:a", Calculator do
command :add, "+|:b"
...
end
```
We therefore have a guarantee that the parameters `:a` and `:b` are valid parameters when our command `:add` is called.
So we can match against them directly when defining `MyApp.Calculator.add/2`.
```elixir
defmodule MyApp.Calculator do
# Note: we can assume that a and b are valid numbers,
# because we added the stopsel `parse_number`.
def add(_message, %{a: a, b: b}) do
IO.puts("#{a} + #{b} = #{a + b}")
end
end
```
## Stopsel message
A `Stopsel.Message` resembles a `Plug.Conn`. It has assigns and parameters that we can use in our plugs and commands.
## Stopsel.Router
The `Stopsel.Router` module allows you to (un)load a router or (un)load routes at runtime.
```elixir
# First we load the router
iex> Stopsel.Router.load_router(MyApp.Router)
:ok
# Then we can disable routes from the router
iex> Stopsel.Router.unload_route(MyApp.Router, ~w"hello")
:ok
# or enable them again
iex> Stopsel.Router.load_route(MyApp.Router, ~w"hello")
:ok
# and unload our router
iex> Stopsel.Router.unload_router(MyApp.Router)
:ok
```
## Stopsel.Invoker
After all this, let's route out messages!
`Stopsel.Invoker` allows us to route our messages through our defined routers.
The invoker will also consider which routes are load/unloaded and respond accordingly.
```elixir
iex> Stopsel.Router.load_router(MyApp.Router)
:ok
# A message can either be a %Stopsel.Message{} or
# anything implements the `Stopsel.Message.Protocol`.
# Strings implement this protocol
iex> Stopsel.Invoker.invoke("hello", MyApp.Router)
"Hello world!"
{:ok, :ok}
iex> Stopsel.Invoker.invoke(%Stopsel.Message{content: "hello"}, MyApp.Router)
"Hello world!"
{:ok, :ok}
# When no route matches
iex> Stopsel.Invoker.invoke("helloooo", MyApp.Router)
{:error, :no_match}
# A prefix can also be used
iex> Stopsel.Invoker.invoke("!hello", MyApp.Router, "!")
"Hello world!"
{:ok, :ok}
# When the prefix doesn't match
iex> Stopsel.Invoker.invoke("hello", MyApp.Router, "!")
{:error, :wrong_prefix}
```
### Roadmap
* [ ] Improve documentation (ongoing effort)
* [ ] Add Tests
* [ ] Improve invoker message parsing
* [ ] Do not use captured functions internally for routes
* [ ] Turn routes into structs
* [ ] Add attributes to scopes and commands such as help descriptions
* [ ] Make it possible to use locally defined functions for stopsel
* [ ] Make it possible to use functions from aliased modules for stopsel
* [ ] Find a way to avoid warnings from imported functions that are used as stopsel
* [ ] Remove dependency `:router`