Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/hanami/router

Ruby/Rack HTTP router
https://github.com/hanami/router

hanami http httprouter rest-api router ruby

Last synced: 3 months ago
JSON representation

Ruby/Rack HTTP router

Awesome Lists containing this project

README

        

# Hanami::Router

Rack compatible, lightweight, and fast HTTP Router for Ruby and [Hanami](http://hanamirb.org).

## Status

[![Gem Version](https://badge.fury.io/rb/hanami-router.svg)](https://badge.fury.io/rb/hanami-router)
[![CI](https://github.com/hanami/router/actions/workflows/ci.yml/badge.svg)](https://github.com/hanami/router/actions?query=workflow%3Aci+branch%3Amain)
[![Test Coverage](https://codecov.io/gh/hanami/router/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/router)
[![Depfu](https://badges.depfu.com/badges/5f6b8e8fa3b0d082539f0b0f84d55960/overview.svg)](https://depfu.com/github/hanami/router?project=Bundler)

## Contact

* Home page: http://hanamirb.org
* Mailing List: http://hanamirb.org/mailing-list
* API Doc: http://rubydoc.info/gems/hanami-router
* Bugs/Issues: https://github.com/hanami/router/issues
* Chat: http://chat.hanamirb.org

## Installation

__Hanami::Router__ supports Ruby (MRI) 3.1.+

Add this line to your application's Gemfile:

```ruby
gem "hanami-router"
```

And then execute:

```shell
$ bundle
```

Or install it yourself as:

```shell
$ gem install hanami-router
```

## Getting Started

Create a file named `config.ru`

```ruby
# 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:

```shell
$ bundle exec rackup
```

## Usage

__Hanami::Router__ is designed to work as a standalone framework or within a
context of a [Hanami](http://hanamirb.org) application.

For the standalone usage, it supports neat features:

### A Beautiful DSL:

```ruby
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:

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

### String matching with variables:

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

### Variables Constraints:

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

### String matching with globbing:

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

### String matching with optional tokens:

```ruby
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:

```ruby
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:

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

### Redirect:

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

### Named routes:

```ruby
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:

```ruby
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.

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

### Duck typed endpoints:

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

```ruby
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):

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

### Explicit Not Found:

```ruby
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:

```ruby
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

```ruby
# 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
```

```shell
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:

```ruby
Hanami::Middleware::BodyParser::BodyParsingError
```

##### `multi_json`

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

#### Custom Parsers

```ruby
# 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
```

```shell
curl http://localhost:2300/authors/1 \
-H "Content-Type: application/xml" \
-H "Accept: application/xml" \
-d 'LG' \
-X PATCH

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

## Testing

```ruby
# 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](http://semver.org)

## 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–2024 Hanami Team – Released under MIT License