• Stars
    star
    144
  • Rank 255,590 (Top 6 %)
  • Language
    Crystal
  • License
    MIT License
  • Created over 8 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Tree oriented routing

Toro

Toro

CI

Tree Oriented Routing

Usage

Here's a hello world app that you can copy and paste to get a sense of how Toro works:

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:

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:

$ 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:

App.run do |server|
  server.listen "0.0.0.0", 80
end

The following example shows how to configure SSL certificates:

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:

  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:

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:

dependencies:
  toro:
    github: soveran/toro
    branch: master

More Repositories

1

cuba

Rum based microframework for web development.
Ruby
1,438
star
2

ohm

Object-Hash Mapping for Redis
Ruby
1,390
star
3

micromachine

Minimal Finite State Machine
Ruby
522
star
4

clac

Command-line, stack-based calculator with postfix notation
C
350
star
5

map

Map lines from stdin to commands
C
221
star
6

mote

Minimum Operational Template
Ruby
217
star
7

nest

Generate nested namespaced keys for key-value databases.
Ruby
185
star
8

ost

Redis based queues and workers.
Ruby
166
star
9

syro

Simple router for web applications
Ruby
135
star
10

cargo

Require libraries without cluttering your namespace.
Ruby
127
star
11

scrivener

Validation frontend for models.
Ruby
124
star
12

clap

Command line argument parsing
Ruby
90
star
13

ohm-crystal

Ohm for Crystal
Crystal
71
star
14

disque-rb

Disque client for Ruby
Ruby
68
star
15

gs

Gemset management
Ruby
67
star
16

totp

Time-based One-Time Passwords
Ruby
44
star
17

mailcat

Fake SMTP server that prints emails to stdout
C
38
star
18

chen

Change directory entries with your text editor
C
37
star
19

spawn

A ridiculously simple fixtures replacement for your web framework of choice.
Ruby
36
star
20

finist

Redis based Finite State Machine
Ruby
36
star
21

redisurl

Connect to Redis using a REDIS_URL and the redigo client.
Go
33
star
22

hart

Hash router
Ruby
24
star
23

stal-ruby

Set algebra solver for Redis
Ruby
22
star
24

redisent

Sentinels aware Redis client.
Ruby
22
star
25

relay

Relay commands over SSH
Ruby
21
star
26

resp

Lightweight RESP client for Lua
Lua
21
star
27

lomo

Apply a lomo filter to your pictures from the command line using ImageMagick.
Ruby
19
star
28

terco

Obstinate DNS
Ruby
16
star
29

override

The as-simple-as-possible-but-not-simpler stubbing library.
Ruby
16
star
30

ox

Skeleton for a Cuba-based JSON API.
Ruby
16
star
31

nido

Structured keys helper
Ruby
14
star
32

syro-demo

Demo application with Syro
Ruby
14
star
33

resp-crystal

Lightweight RESP client
Crystal
14
star
34

mt

Mail tester daemon.
Ruby
13
star
35

stringent

Generate a string with a target entropy
Ruby
12
star
36

rediscan

Scanner for Redis keyspace
Ruby
12
star
37

hypertext

Hypertext authoring with Ruby
Ruby
11
star
38

filmo

A single page presentation tool.
11
star
39

cuba-book

Cuba Book
11
star
40

sc

List of HTTP status codes.
Ruby
11
star
41

finist.lua

Redis based Finite State Machine
Lua
10
star
42

drawer

Ultra slim file-based cache
Ruby
10
star
43

nest-crystal

Object Oriented Keys for Redis
Crystal
9
star
44

basica

Basic authentication library.
Ruby
8
star
45

rino

Remove a file by its inode number
C
7
star
46

seg.rb

Segment matcher for paths
Ruby
7
star
47

seg

Segment matcher for paths
Crystal
7
star
48

trim

Read from stdin and remove a prefix from each line
C
7
star
49

tas

Trees as strings
Ruby
6
star
50

stal

Set algebra solver for Redis
Lua
6
star
51

pac

Package management for Lua libraries.
Shell
6
star
52

prep

Read from stdin and prepend a string to each line while preserving identation.
C
6
star
53

m4s2

Static Site Generator
HTML
5
star
54

contract

Contract helper
Ruby
5
star
55

stal-crystal

Set algebra solver for Redis
Crystal
5
star
56

loco

Lines of code counter
C
4
star
57

ook

Object Oriented Keys for Redis
Ruby
4
star
58

walk

Walk a directory tree and print the name of every regular file
C
4
star
59

rel

Ruby client for the Bandicoot database.
Ruby
4
star
60

textorama

Text slides for the terminal
Shell
4
star
61

homebrew-tools

Formulae for Homebrew
Ruby
4
star
62

rediscan.lua

Scanner for Redis keyspace in Lua
Lua
3
star
63

seg.go

Segment matcher for paths
Go
3
star
64

look

Add a vendor directory to your load path.
Ruby
3
star
65

app

Cuba application template for gn, the file generator.
Ruby
2
star
66

packer

Require different versions of the same Cargo-compatible gem
Ruby
2
star
67

tele.sh

2
star
68

ers

AWK script easy replacements in templates
Awk
2
star
69

soveran.github.io

2
star
70

filter

Workflow template for gn.
Ruby
1
star
71

remoto

Ruby
1
star
72

gem

Gem template for gn, the file generator.
Ruby
1
star
73

ohm-scripts

Lua scripts for Ohm compatible libraries
Lua
1
star
74

ostgo

Ost client.
Go
1
star
75

miga

Breadcrumb name of the working directory
C
1
star