# Toro   Tree Oriented Routing ## Usage Here's a `hello world` app that you can copy and paste to get a sense of how Toro works: ```crystal require "toro" class App < Toro::Router def routes get do text "hello world" end end end App.run do |server| server.listen "0.0.0.0", 8080 end ``` Save it to a file called `hello_world.cr` and run it with `crystal run hello_world.cr`. Then access your `hello world` application with your browser, or simply by calling `curl http://localhost:8080/` from the command line. What follows is an example that showcases some basic routing features: ```crystal require "toro" class App < Toro::Router # You must define the `routes` methods. It will be the # entry point to your web application. def routes # The `get` matcher will execute the block when two conditions # are met: the `REQUEST_METHOD` is equal to "GET", and there are # no more path segments to match. In this case, as we haven't # consumed any path segment, the only way for this block to run # would be to have a "GET" request to "/". Check the API section # to see all available matchers. get do # The text method sets the Content-Type to "text/plain", and # prints the string to the response. text "hello world" end # A `String` matcher will run the block only if its content is equal # to the next segment in the current path. In this example, it will # match the request if the first segment is equal to "users". # You can always inspect the current path by looking at `path.curr`. on "users" do # If we get here it's because the previous matcher succeeded. It # means we were able to consume a segment off the current path. More # specifically, we consumed the "users" segment, and if we now # inspect the `path.prev` string we will find its value is "/users". # # With the next matcher we want to capture a segment. Let's say a # request is made to "/users/42". When we arrive at this point, this # symbol will match the number "42" and store it in the inbox. on :id do # If there are no more segments in the request path and if the # request method is "GET", this block will run. get do # Now, `inbox[:id]` has the value "42". The templates have access # to the inbox and to any other variables defined here. # # The `html` macro expects a path to a template. It automatically # appends the `.ecr` extension, which stands for Embedded Crystal # and is part of the standard library. It also sets the content # type to "text/html". For the html example to work, you need to # create the file ./views/users/show.ecr with the following content: # # hello user <%= inbox[:id] %> # # # Once you have created the file, uncomment the line below. # # html "views/users/show" # As a placeholder, the following directive renders the same message # as plain text. Once you have the HTML template in place, you can # comment or remove both this comment and the `text` directive. # text "hello user #{inbox[:id]}" end end end # The `default` matcher always succeeds, but it doesn't mean the program's # flow will always reach this point. Once a matcher succeeds and runs a # block, the control is never returned. There's an implicit return at the # end of every block, which stops the processing of the request and # returns the response immediately. # # This route will match all the requests that don't have "users" as the # first segment (because of the previous matcher), and it will pass the # control to the `Guests` application, which has to be an instance of # `Toro::Router`. This illustrates how you can compose your applications # and split the logic among different routers. default do mount Guests end end end # This is another Toro application. You can mount apps on top of other Toro # in order to achieve a modular design. class Guests < Toro::Router def routes on "about" do get do text "about this site" end end end end # Start the app on port 8080. App.run do |server| server.listen "0.0.0.0", 8080 end ``` Once you have this application running, try the requests below: ```shell $ curl http://localhost:8080/ $ curl http://localhost:8080/about $ curl http://localhost:8080/users/42 ``` The routes are evaluated in a sandbox where the following methods are available: `context`, `path`, `inbox`, `mount`, `basic_auth`, `root`, `root?`, `default`, `on`, `get`, `put`, `head`, `post`, `patch`, `delete`, `options`, `text`, `html`, `json`, `write` and `render`. ## API `context`: Environment variables for the request. `path`: Helper object that tracks the previous and current path. `inbox`: Hash with captures and potentially other variables local to the request. `mount`: Mounts a sub app. `basic_auth`: Yields a username and password from the Authorization header, and returns whatever the block returns or nil. `root?`: Returns true if the path yet to be consumed is empty. `root`: Receives a block and calls it only if `root?` is true. `default`: Receives a block that will be executed inconditionally. `on`: Receives a value to be matched, and a block that will be executed only if the request is matched. `get`: Receives a block and calls it only if `root?` and `get?` are true. `put`: Receives a block and calls it only if `root?` and `put?` are true. `head`: Receives a block and calls it only if `root?` and `head?` are true. `post`: Receives a block and calls it only if `root?` and `post?` are true. `patch`: Receives a block and calls it only if `root?` and `patch?` are true. `delete`: Receives a block and calls it only if `root?` and `delete?` are true. `options`: Receives a block and calls it only if `root?` and `options?` are true. ## Matchers The `on` method can receive a `String` to perform path matches; a `Symbol` to perform path captures; and a boolean to match any true values. Each time `on` matches or captures a segment of the PATH, that part of the path is consumed. The current and previous paths can be queried by calling `prev` and `curr` on the `path` object: `path.prev` returns the part of the path already consumed, and `path.curr` provides the current version of the path. Any expression that evaluates to a boolean can also be used as a matcher. Captures -------- When a symbol is provided, `on` will try to consume a segment of the path. A segment is defined as any sequence of characters after a slash and until either another slash or the end of the string. The captured value is stored in the `inbox` hash under the key that was provided as the argument to `on`. For example, after a call to `on(:user_id)`, the value for the segment will be stored at `inbox[:user_id]`. Security -------- There are no security features built into this routing library. A framework using this library should implement the security layer. Rendering --------- The most basic way of returning a string is by calling the method `text`. It sets the `Content-Type` header to `text/plain` and writes the passed string to the response. A similar helper is called `html`: it takes as an argument the path to an `ECR` template and renders its content. A lower level `render` macro is available: it also expects the path to a template, but it doesn't modify the headers. There's a `json` helper method expecting a Crystal generic Object. It will call the `to_json` serializer on the generic object. Please note that you need to require JSON from the standard library in order to use this helper (adding `require "json"` to your app should suffice). The lower level `write` method writes a string to the response object. It is used internally by `text` and `json`. Running the server ------------------ If `App` is an instance of `Toro`, then you can start the server by calling `App.run`. It yields an instance of `HTTP://Server` that you can configure: For example, you can start the server on port 80: ```crystal App.run do |server| server.listen "0.0.0.0", 80 end ``` The following example shows how to configure SSL certificates: ```crystal App.run do |server| ssl = OpenSSL::SSL::Context::Server.new ssl.private_key = "path/to/private_key" ssl.certificate_chain = "path/to/certificate_chain" server.tls = ssl server.listen "0.0.0.0", 443 end ``` Refer to Crystal's documentation for more options. Status codes ------------ The default status code is `404`. It can be changed and queried with the `status` method: ```crystal status #=> 404 status 200 status #=> 200 ``` When a request method matcher succeeds, the status code for the request is changed to `200`. Basic Auth ---------- The `basic_auth` method checks the `Authentication` header and, if present, yields to the block the values for username and password. Here's an example of how you can use it: ```crystal class A < Toro::Router def users(user : User) get do text "Hello #{user.name}" end end def users(user : Nil) get do text "Hello guest!" end end def routes user = basic_auth do |name, pass| User.authenticate(name, pass) end users(user) end end ``` The example overloads the `users` method so that it can deal both with instances of `User` and with `nil`. The flow of your router will naturally continue in one of those methods. You are free to define any other methods like `users` in order to split the logic of your application. To illustrate the `basic_auth` feature we used an imaginary `User` class that responds to the `authenticate` method and returns either an instance of `User` or nil. ## Installation Add this to your application's `shard.yml`: ```yaml dependencies: toro: github: soveran/toro branch: master ```