A library inspired by plug and phoenix routers for parsing text messages like commands.
If available in Hex, the package can be installed
by adding stopsel
to your list of dependencies in mix.exs
:
def deps do
[
{:stopsel, "~> 0.1.0"}
]
end
Stopsel tries to be lightweight with its usage of Macros. Just import Stopsel.Builder
and start defining your router.
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, path: "+ :b"
command :subtract, path: "- :b"
command :multiply, path: "* :b"
command :divide, path: "/ :b"
end
end
end
Every command defined must be defined as a function in the currently aliased Module. Here's how the commands for MyApp
could be implemented.
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.
scope "calculator :a", Calculator do
command :add, path: "+ :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
.
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
A Stopsel.Message
resembles a Plug.Conn
. It has assigns and parameters that we can use in our plugs and commands.
The Stopsel.Router
module allows you to (un)load a router or (un)load routes at runtime.
# First we load the router
iex> Stopsel.Router.load_router(MyApp.Router)
true
# Then we can disable routes from the router
iex> Stopsel.Router.unload_route(MyApp.Router, ~w"hello")
true
# or enable them again
iex> Stopsel.Router.load_route(MyApp.Router, ~w"hello")
true
# and unload our router
iex> Stopsel.Router.unload_router(MyApp.Router)
true
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.
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}
- 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