Orion
A powerful, simple, rails-esque routing library for HTTP::Server.
router MyApp do
# Use as a simple one-file app
root do |context|
context.response.puts "I am home"
end
get "/login" do |context|
context.response.puts "Login"
end
ws "/socket" do |socket, context|
socket.send "Hello"
end
# Use it the mvc/rails pattern (shorthand)
root to: "application#home"
get "/login", to: "auth#new"
ws "/socket", to: "socket#main"
# Use it with the mvc/rails pattern (longhand)
root controller: ApplicationController, action: home
get "/login", controller: LoginController, action: login
ws "/socket", controller: WebSocketController, action: channel
end
# Run your web app
MyApp.listen(port: 3000)
Orion allows you to easily add routes, groups, and middleware in order to construct your application’s routing layer.
Purpose
The purpose of the Orion router is to connect URLs to code. It provides a flexible and comprehensive DSL that will allow you to cover a variety of use cases. In addition, Orion will also generate a series of helpers to easily reference the defined paths within your application.
Installation
Add this to your application’s shard.yml:
dependencies:
orion:
github: obsidian/orion
- and require Orion in your project.
require "orion"
Usage
Defining a router
You can define a router by using the router macro with a constant name.
router MyApplicationRouter do
# ...
end
Generic route arguments
There are a variety of ways that you can interact with basic routes. Below are some examples and guidelines on the different ways you can interact with the router.
Using to: String to target a controller and action
One of the most common ways we will be creating routes in this guide is to use
the to argument supplied with a controller and action in the form of a string.
In the example below Users#create will map to UsersController.new(cxt : HTTP::Server::Context).create.
router MyApplicationRouter do
post "users", to: "Users#create"
end
Non-constant
When passing a lowercased string, it still camelcase the string and add Controller.
In the example below users#create will map to UsersController.new(cxt : HTTP::Server::Context).create.
router MyApplicationRouter do
post "users", to: "users#create"
end
Using controller: Type and action: Method
A longer form of the to argument strategy above allows us to pass the controller and action
independently.
router MyApplicationRouter do
post "users", controller: UsersController, action: create
end
Using block syntax
Sometimes, we may want a more kemal or
sinatra like approach. To accomplish this, we can
simply pass a block that yields HTTP::Server::Context.
router MyApplicationRouter do
post "users" do |context|
context.response.puts "foo"
end
end
Using a call able object
Lastly a second argument can be any
object that responds to #call(cxt : HTTP::Server::Context).
router MyApplicationRouter do
post "users", ->(context : HTTP::Server::Context) {
context.response.puts "foo"
}
end
Basic Routing
Base route using root
Let’s define the routers’ root route. root is simply an alias for get '/', action.
All routes can either be a String pointing to a Controller action or a Proc
accepting HTTP::Server::Context as a single argument. If a String is used like controller#action, it will expand into Controller.new(context : HTTP::Server::Context).action, therefor A controller must
have an initializer that takes HTTP::Server::Context as an argument, and the
specified action must not contain arguments.
router MyApplicationRouter do
root to: "home#index"
end
HTTP verb based routes
A common way to interact with the router is to use standard HTTP verbs. Orion supports all the standard HTTP verbs:
get, head, post, put, delete, connect, options, trace, and patch
You can use one of the methods within the router and pass it’s route and any variation of the Generic Route Arguments.
router MyApplicationRouter do
post "users", to: "users#create"
end
Catch-all routes using match
In some instances, you may just want to redirect all verbs to a particular controller and action.
You can use the match method within the router and pass it’s route and
any variation of the Generic Route Arguments.
router MyApplicationRouter do
match "404", controller: ErrorsController, action: error_404
end
Websocket routes
Orion has WebSocket support.
You can use the ws method within the router and pass it’s route and
any variation of the Generic Route Arguments.
router MyApplicationRouter do
ws "/socket", controller: WebSocketController, action: main
end
Resource-Based Routing
A common way in Orion to route is to do so against a known resource. This method will create a series of routes targeted at a specific controller.
The following is an example controller definition and the matching resources definition.
class PostsController < MyRouter::BaseController
def index
@posts = Post.all
render :index
end
def new
@post = Post.new
render :new
end
def create
post = Post.create(request)
redirect to: post_path post_id: post.id
end
def show
@post = Post.find(request.path_params["post_id"])
end
def edit
@post = Post.find(request.path_params["post_id"])
render :edit
end
def update
post = Post.find(request.path_params["post_id"])
HTTP::FormData.parse(request) do |part|
post.attributes[part.name] = part.body.gets_to_end
end
redirect to: post_path post_id: post.id
end
def delete
post = Post.find(request.path_params["post_id"])
post.delete
redirect to: posts_path
end
end
router MyApplication do
resources :posts
end
Including/Excluding Actions
By default, the actions index, new, create, show, edit, update, delete
are included. You may include or exclude explicitly by using the only and except params.
NOTE The index action is not added for singular resources.
router MyApplication do
resources :posts, except: [:edit, :update]
resources :users, only: [:new, :create, :show]
end
Nested Resources and Routes
You can add nested resources and member routes by providing a block to the
resources definition.
router MyApplication do
resources :posts do
post "feature", action: feature
resources :likes
resources :comments
end
end
Singular Resources
In addition to using the collection of resources method, You can also add
singular resources which do not provide a id_param or index action.
router MyApplication do
resource :profile
end
Customizing ID
You can customize the ID path parameter by passing the id_param parameter.
router MyApplication do
resources :posts, id_param: :article_id
end
Constraining the ID
You can set constraints on the ID parameter by passing the id_constraint parameter.
see param constraints for more details
router MyApplication do
resources :posts, id_constraint: /^\d{4}$/
end
Constraints
Similar to basic routes, resource and resources support the
format, accept,
content_type, and type
constraints.
Defining Controllers
There are a few ways to define controllers within your application. Controllers are a useful way to separate concerns from your application.
You may inherit or extend from the routers BaseController or WebSocketBaseController, this will expose the helper methods from the router to all inherited controllers from the base. Caveat: Ensure that controllers are required
after your router is defined.
class ApplicationController < MyApp::ControllerBase
def home
response.puts "you are home"
end
end
Instrumenting handlers (a.k.a. middleware)
Instances or Classes implementing
HTTP::Handler (a.k.a. middleware)
can be inserted directly in your routes by using the use method.
Handlers will only apply to the routes specified below them, so be sure to place your handlers near the top of your route.
router MyApplicationRouter do
use HTTP::ErrorHandler
use HTTP::LogHandler.new(File.open("tmp/application.log"))
end
Nested Routes using scope
Scopes are a method in which you can nest routes under a common path. This prevents the need for duplicating paths and allows a developer to easily change the parent of a set of child paths.
router MyApplicationRouter do
scope "users" do
root to: "Users#index"
get ":id", to: "Users#show"
delete ":id", to: "Users#destroy"
end
end
Handlers within nested routes
Instances of HTTP::Handler can be
used within a scope block and will only apply to the subsequent routes within that scope.
It is important to note that the parent context’s handlers will also be used.
Handlers will only apply to the routes specified below them, so be sure to place your handlers near the top of your scope.
router MyApplicationRouter do
scope "users" do
use AuthorizationHandler.new
root to: "Users#index"
get ":id", to: "Users#show"
delete ":id", to: "Users#destroy"
end
end
Route Helper prefixes
When using Helpers, you may want a prefix to be appended so that you don’t have to
repeat it within each individual route. For example a scope with helper_prefix: "users"
containing a route with helper: "show" will generate a helper method of users_show.
router MyApplicationRouter do
scope "users", helper_prefix: "users" do
use AuthorizationHandler.new
get ":id", to: "Users#show", helper: "show"
end
end
Caveats
When considering helpers within scopes you may want to use a longer form of the
helper to get a better name. You can pass a named tuple with the fields name,
prefix, and/or suffix.
router MyApplicationRouter do
scope "users", helper_prefix: "user" do
use AuthorizationHandler.new
get ":id", to: "Users#show", helper: { prefix: "show" }
end
end
The above example will expand into show_user instead of user_show.
Concerns – Reusable code in your routers
In some instances, you may want to create a pattern or concern that you wish to repeat across scopes or resources in your router.
Defining a concern
To define a concern call concern with a Symbol for the name.
router MyApplicationRouter do
concern :authenticated do
use Authentication.new
end
end
Using concerns
Once a concern is defined you can call implements with a named concern from
anywhere in your router.
router MyApplicationRouter do
concern :authenticated do
use Authentication.new
end
scope "users" do
implements :authenticated
get ":id"
end
end
Method Overrides
In some situations, certain environments may not support certain HTTP methods,
when in these environments, there are a few methods to force a different method
in the router. In either of the methods below, if you intend to pass a body, you
should be using the POST HTTP method when you make the request.
Header Overrides
If your client has the ability to set headers you can use the built-in ability to
pass the X-HTTP-Method-Override: [METHOD] method with the method you wish to invoke on
the router.
Parameter & Form Overrides
If your client has the ability to set headers you can use the
Orion::Handlers::MethodOverrideParam to pass a _method=[METHOD] parameter as
a query parameter or form field with the method you wish to invoke on the router.
router MyRouter do
use Orion::Handlers::MethodOverrideParam.new
# ... routes
end
Constraints - More advanced rules for your routes
Constraints can be used to further determine if a route is hit beyond just it’s path. Routes have some predefined constraints you can specify, but you can also pass in a custom constraint.
Parameter constraints
When defining a route, you can pass in parameter constraints. The path params will be checked against the provided regex before the route is chosen as a valid route.
router MyApplicationRouter do
get "users/:id", constraints: { id: /[0-9]{4}/ }
end
Format constraints
You can constrain the request to a certain format. Such as restricting the extension of the URL to '.json'.
router MyApplicationRouter do
get "api/users/:id", format: "json"
end
Request Mime-Type constraints
You can constrain the request to a certain mime-type by using the content_type param
on the route. This will ensure that if the request has a body, it will provide the proper
content type.
router MyApplicationRouter do
put "api/users/:id", content_type: "application/json"
end
Response Mime-Type constraints
You can constrain the response to a certain mime-type by using the accept param
on the route. This is similar to the format constraint but allows clients to
specify the Accept header rather than the extension.
Orion will automatically add mime-type headers for requests with no Accept header and a specified extension.
router MyApplicationRouter do
get "api/users/:id", accept: "application/json"
end
Combined Mime-Type constraints
You can constrain the request and response to a certain mime-type by using the type param
on the route. This will ensure that if the request has a body, it will provide the proper
content type. In addition, it will also validate that the client provides a proper
accept header for the response.
Orion will automatically add mime-type headers for requests with no Accept header and a specified extension.
router MyApplicationRouter do
put "api/users/:id", type: "application/json"
end
Host constraints
You can constrain the request to a specific host by wrapping routes
in a host block. In this method, any routes within the block will be
matched at that constraint.
You may also choose to limit the request to a certain format. Such as restricting the extension of the URL to '.json'.
router MyApplicationRouter do
host "example.com" do
get "users/:id", format: "json"
end
end
Subdomain constraints
You can constrain the request to a specific subdomain by wrapping routes
in a subdomain block. In this method, any routes within the block will be
matched at that constraint.
You may also choose to limit the request to a certain format. Such as restricting the extension of the URL to '.json'.
router MyApplicationRouter do
subdomain "api" do
get "users/:id", format: "json"
end
end
Custom Constraints
You can also pass in your own constraints by just passing a class/struct that
implements the Orion::Constraint module.
struct MyConstraint
def matches?(req : HTTP::Request)
true
end
end
router MyApplicationRouter do
constraint MyConstraint.new do
get "users/:id", format: "json"
end
end
Route Helpers
Route helpers provide type-safe methods to generate paths and URLs to defined routes
in your application. By including the Helpers module on the router (i.e. MyApplicationRouter::Helpers)
you can access any helper defined in the router by {{name}}_path to get its corresponding
route. In addition, when you have a @context : HTTP::Server::Context instance var,
you will also be able to access a {{name}}_url to get the full URL.
router MyApplicationRouter do
scope "users", helper_prefix: "user" do
get "/new", to: "Users#new", helper: "new"
end
end
class UsersController
def new
end
end
class MyController
include MyApplicationRouter::Helpers
delegate request, response, to: @context
def initialize(@context : HTTP::Server::Context)
end
def new
File.open("new.html") { |f| IO.copy(f, response) }
end
def show
user = User.find(request.path_params["id"])
response.headers["Location"] = new_user_path
response.status_code = 301
response.close
end
end
Making route helpers from your routes
In order to make a helper from your route, you can use the helper named argument in your route.
router MyApplicationRouter do
scope "users" do
get "/new", to: "Users#new", helper: "new"
end
end
Using route helpers in your code
As you add helpers they are added to the nested Helpers module of your router.
you may include this module anywhere in your code to get access to the methods,
or call them on the module directly.
If @context : HTTP::Server::Context is present in the class, you will also be
able to use the {helper}_url versions of the helpers.
router MyApplicationRouter do
resources :users
end
class User
include MyApplicationRouter::Helpers
def route
user_path user_id: self.id
end
end
puts MyApplicationRouter::Helpers.users_path
Contributing
-
Create your feature branch (git checkout -b my-new-feature)
-
Commit your changes (git commit -am 'Add some feature')
-
Push to the branch (git push origin my-new-feature)
-
Create a new Pull Request
Contributors
- Jason Waldrip (jwaldrip) - creator, maintainer