NOTE
If you are reading this first time, please don't forget to read chapter about Router behaviour, because without that information you most likely will stumble on some unexpected behaviour, without knowing first how routes are processed.
Routing in Kanal is written using handy DSL like this:
...
core.router.configure do
on :body, contains: "my name is" do
respond do
body "Hey good to know!"
end
end
on :body, contains: "what is the weather today?" do
respond do
weather = core.services.get(:weather_service).get_weather
body "Todays weather is: #{weather}"
end
end
end
...
NOTE
If you are wondering what is the core.services and why is there core available,
please check more in respond block
documentation and page about Services.
When you pass block of code into the router.configure
method, you
are presented with a DSL methods that helps you to write your routes.
At the moment these DSL methods are:
This method accepts 3 arguments. Well, 2 arguments and a block
...
# When condition does not require argument
on :condition_pack_name, :condition_name do
end
# When condition requires argument
on :condition_pack_name, condition_name: you_argument do
end
...
Default response is a vital part of a router and it should be provided.
Without default response - Kanal won't start, its router will raise an exception asking you to provide default response.
You can specify default response like this:
core.router.default_response do
# Here you can write code like you would normally write code in respond block.
# With access to input, core and setting output parameters
end
NOTE
Default response is required for router to operate, because if not suitable route found, router will refer to the provided default response.
Usually default response looks like "Unfortunately, I did not quite get this. I can understand these commands: ... ... ..."
You can have nested routes. It means you can write something like this:
core.router.configure do
on :body, starts_with: "tell me" do
on :body, ends_with: "news" do
respond do
body "News today as good as they get!"
end
end
on :body, ends_with: "joke" do
body core.services.get(:jokes_service).random_joke
end
end
on :body, contains: "what is the weather today?" do
respond do
weather = core.services.get(:weather_service).get_weather
body "Todays weather is: #{weather}"
end
end
end
You can have as many nesting layers as you want.
ATTENTION
Beware, when you are using nested routes: if route contains at least 1 nested route, you can no
longer use respond
block inside.
Your route can contains one of the 2: nested route(s) OR respond block
If you wonder about the default response in the route, if no nested routes were suitable with their conditions, please read further on this page how router machinery works and how to control your input flow in routes.
In your route, you can specify respond block
Like this:
...
on :body, ends_with: "news" do
# This is the respond block we are talking about
respond do
body "News today as good as they get!"
end
end
...
Respond block allows you to set registered in output parameters. Meaning if you registered parameter
block
, it will be available as DSL method block
inside. You specify parameter value by using method syntax,
but not variable syntax.
# Wrong
body = "Somebody"
# Right
body "Somebody"
Kanal provides you with input
and core
inside respond block.
Using input
, you can have additional checks and validations inside and give response to user accordingly.
Like this:
on :body, ends_with: "news" do
# This is the respond block we are talking about
respond do
user_said = input.body
body "You said: #{user_said} and my response is: News today as good as they get!"
end
end
Why there is core
available? So you can access anything that can core provide.
For example, if you registered service named :weather_provider, you can acces it there and use it
on :body, contains: "weather" do
# This is the respond block we are talking about
respond do
weather = core.services.get(:weather_provider).get_current_weather
body "Current weather is: #{weather}"
end
end
NOTE
Please be aware of examples like this weather example, that usually when someone wants to know weather at their location, it is required to get users location in one way or another.
It is of course possible, if user sends location via some bot or writes location by hand (lat/long or name of city), but this is topic for another documentation page about integrations with bots etc.
For the sake of example let's pretend that user wants to know weather at some kind of default location for everyone, without providing the location manually.
Basically router stores routes in a tree. Well, for some people it will be obvious, looking at how you write your routes.
What is a tree? Imagine, there is a root with branches coming out of it, and then there are branches coming out of branches etc. etc.
When input is provided to the method core.router.create_output_for_input
router goes from root to all the branches (routes),
test input against conditions in those routes and finally finds a suitable route or refers to the default response.
Consider this router configuration:
core.router.default_response do
body "I can't understand you!"
end
core.router.configure do
on :body, contains: "hey" do
respond do
body "Hello to you!"
end
end
on :body, contains: "tell me" do
on :body, contains: "weather" do
respond do
body "Weather is fine!"
end
end
on :body, contains: "news" do
respond do
body "News are great!"
end
end
end
end
Now, let's imagine this code with comments, showing which input will get which output
input = core.create_input
input.body = "Hello"
output = core.router.create_output_for_input input
# output.body will be "I can't understand you!"
input = core.create_input
input.body = "tell me weather"
output = core.router.create_output_for_input input
# output.body will be "Weather is fine!"
input = core.create_input
input.body = "tell me something"
output = core.router.create_output_for_input input
# output.body will be "I can't understand you!"
So far so good. But what happens, if there are 2 routes sitting next to each other that have suitable condition for our input? But, only second route contains suitable condition inside.
Let's take this example configuration:
core.router.default_response do
body "I can't understand you!"
end
core.router.configure do
on :body, contains: "tell me" do
on :body, contains: "weather" do
respond do
body "Weather is fine!"
end
end
end
on :body, contains: "never" do
on :body, contains: "forever" do
respond do
body "This is really interesting"
end
end
end
end
This doesn't seem so bad, we see different conditions for these 2 routes, but...
Imagine this code with input:
input = core.create_input
input.body = "tell me never"
output = core.router.create_output_for_input input
# output.body will be "This is really interesting"
As you can see, router successfully found the route for this input!
How did it find it?
Well, first it tried route with condition contains: "weather"
. It got inside this route,
and tried inner route for contains: weather
and got condition check failed.
What happened next? Router got back up in the routes branches and started checking next routes!
And next route and inner route had condition that successfully validated our input!
Remember this behaviour, router does not stop in any inner route branches if it does not find suitable route. It traverses back up and tests all other adjacent branches.
NOTE
Remember, routes are traversed and tested for condition in the order they were written in the configure block.
Meaning first defined - first tested for.
Powered by Doctave