/hanami-router

Ruby/Rack HTTP router

Primary LanguageRubyMIT LicenseMIT

Hanami::Router

Rack compatible, lightweight and fast HTTP Router for Ruby and Hanami.

Version

This branch contains the code for hanami-router 2.x.

Status

Gem Version CI Test Coverage Depfu Inline Docs

Contact

Rubies

Hanami::Router supports Ruby (MRI) 3.0+

Installation

Add this line to your application's Gemfile:

gem "hanami-router"

And then execute:

$ bundle

Or install it yourself as:

$ gem install hanami-router

Getting Started

Create a file named config.ru

# frozen_string_literal: true
require "hanami/router"

app = Hanami::Router.new do
  get "/", to: ->(env) { [200, {}, ["Welcome to Hanami!"]] }
end

run app

From the shell:

$ bundle exec rackup

Usage

Hanami::Router is designed to work as a standalone framework or within a context of a Hanami application.

For the standalone usage, it supports neat features:

A Beautiful DSL:

Hanami::Router.new do
  root                to: ->(env) { [200, {}, ["Hello"]] }
  get "/lambda",      to: ->(env) { [200, {}, ["World"]] }
  get "/dashboard",   to: Dashboard::Index
  get "/rack-app",    to: RackApp.new

  redirect "/legacy", to: "/"

  mount Api::App, at: "/api"

  scope "admin" do
    get "/users", to: Users::Index
  end
end

Fixed string matching:

Hanami::Router.new do
  get "/hanami", to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
end

String matching with variables:

Hanami::Router.new do
  get "/flowers/:id", to: ->(env) { [200, {}, ["Hello from Flower no. #{ env["router.params"][:id] }!"]] }
end

Variables Constraints:

Hanami::Router.new do
  get "/flowers/:id", id: /\d+/, to: ->(env) { [200, {}, [":id must be a number!"]] }
end

String matching with globbing:

Hanami::Router.new do
  get "/*match", to: ->(env) { [200, {}, ["This is catch all: #{ env["router.params"].inspect }!"]] }
end

String matching with optional tokens:

Hanami::Router.new do
  get "/hanami(.:format)" to: ->(env) { [200, {}, ["You"ve requested #{ env["router.params"][:format] }!"]] }
end

Support for the most common HTTP methods:

endpoint = ->(env) { [200, {}, ["Hello from Hanami!"]] }

Hanami::Router.new do
  get     "/hanami", to: endpoint
  post    "/hanami", to: endpoint
  put     "/hanami", to: endpoint
  patch   "/hanami", to: endpoint
  delete  "/hanami", to: endpoint
  trace   "/hanami", to: endpoint
  options "/hanami", to: endpoint
end

Root:

Hanami::Router.new do
  root to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
end

Redirect:

Hanami::Router.new do
  get "/redirect_destination", to: ->(env) { [200, {}, ["Redirect destination!"]] }
  redirect "/legacy",          to: "/redirect_destination"
end

Named routes:

router = Hanami::Router.new(scheme: "https", host: "hanamirb.org") do
  get "/hanami", to: ->(env) { [200, {}, ["Hello from Hanami!"]] }, as: :hanami
end

router.path(:hanami) # => "/hanami"
router.url(:hanami)  # => "https://hanamirb.org/hanami"

Scopes:

router = Hanami::Router.new do
  scope "animals" do
    scope "mammals" do
      get "/cats", to: ->(env) { [200, {}, ["Meow!"]] }, as: :cats
    end
  end
end

# and it generates:

router.path(:animals_mammals_cats) # => "/animals/mammals/cats"

Mount Rack applications:

Mounting a Rack application will forward all kind of HTTP requests to the app, when the request path matches the at: path.

Hanami::Router.new do
  mount MyRackApp.new, at: "/foo"
end

Duck typed endpoints:

Everything that responds to #call is invoked as it is:

Hanami::Router.new do
  get "/hanami",     to: ->(env) { [200, {}, ["Hello from Hanami!"]] }
  get "/rack-app",   to: RackApp.new
  get "/method",     to: ActionControllerSubclass.action(:new)
end

Implicit Not Found (404):

router = Hanami::Router.new
router.call(Rack::MockRequest.env_for("/unknown")).status # => 404

Explicit Not Found:

router = Hanami::Router.new(not_found: ->(_) { [499, {}, []]})
router.call(Rack::MockRequest.env_for("/unknown")).status # => 499

Body Parsers

Rack ignores request bodies unless they come from a form submission. If we have a JSON endpoint, the payload isn't available in the params hash:

Rack::Request.new(env).params # => {}

This feature enables body parsing for specific MIME Types. It comes with a built-in JSON parser and allows to pass custom parsers.

JSON Parsing

# frozen_string_literal: true

require "hanami/router"
require "hanami/middleware/body_parser"

app = Hanami::Router.new do
  patch "/books/:id", to: ->(env) { [200, {}, [env["router.params"].inspect]] }
end

use Hanami::Middleware::BodyParser, :json
run app
curl http://localhost:2300/books/1    \
  -H "Content-Type: application/json" \
  -H "Accept: application/json"       \
  -d '{"published":"true"}'           \
  -X PATCH

# => [200, {}, ["{:published=>\"true\",:id=>\"1\"}"]]

If the json can't be parsed an exception is raised:

Hanami::Middleware::BodyParser::BodyParsingError
multi_json

If you want to use a different JSON backend, include multi_json in your Gemfile.

Custom Parsers

# frozen_string_literal: true

require "hanami/router"
require "hanami/middleware/body_parser"

# See Hanami::Middleware::BodyParser::Parser
class XmlParser < Hanami::Middleware::BodyParser::Parser
  def mime_types
    ["application/xml", "text/xml"]
  end

  # Parse body and return a Hash
  def parse(body)
    # parse xml
  rescue SomeXmlParsingError => e
    raise Hanami::Middleware::BodyParser::BodyParsingError.new(e)
  end
end

app = Hanami::Router.new do
  patch "/authors/:id", to: ->(env) { [200, {}, [env["router.params"].inspect]] }
end

use Hanami::Middleware::BodyParser, XmlParser
run app
curl http://localhost:2300/authors/1 \
  -H "Content-Type: application/xml" \
  -H "Accept: application/xml"       \
  -d '<name>LG</name>'               \
  -X PATCH

# => [200, {}, ["{:name=>\"LG\",:id=>\"1\"}"]]

Testing

# frozen_string_literal: true

require "hanami/router"

router = Hanami::Router.new do
  get "/books/:id", to: "books.show", as: :book
end

route = router.recognize("/books/23")
route.verb      # "GET"
route.endpoint  # => "books.show"
route.params    # => {:id=>"23"}
route.routable? # => true

route = router.recognize(:book, id: 23)
route.verb      # "GET"
route.endpoint  # => "books.show"
route.params    # => {:id=>"23"}
route.routable? # => true

route = router.recognize("/books/23", {}, method: :post)
route.verb      # "POST"
route.routable? # => false

Versioning

Hanami::Router uses Semantic Versioning 2.0.0

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Copyright

Copyright © 2014-2022 Hanami Team – Released under MIT License