kemal-controller

Version, currently 0.2.05 versions

github.com/hugopl/kemal-controller

Controller classes for your kemal application.

7 stars
0 dependents
License: MIT

Nothing has been indexed for 0.2.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.

Installation

# Add this to your shard.yml
dependencies:
  kemal-controller:
    github: hugopl/kemal-controller
    version: ~> 0.2.0

Then run:

shards install

shard.yml

No shard.yml has been indexed for 0.2.0. You can read it on the repository.

Dependencies

Unknown: the shard.yml for this version has not been read yet.

README

This README is the one indexed from the repository at its latest ref, not from the tag for this version.

# kemal-controller

Kemal is awesome, but sometimes you need (or just want) a bit more structure in
your web applications, kemal-controller is here to help you with that by
providing a simple way to declare all your endpoints into controller classes
where the method parameters will map to GET/PATCH/.../POST/URL parameters automatically.

Online documentation can be found at: <https://hugopl.github.io/kemal-controller/>.

Controllers are structs, so the overhead is minimal and you can still use all
Kemal features as you would normally do.

```Crystal
struct UsersController < Kemal::Controller
  @[Get("/users")]
  def index
    "Listing all users"
  end

  @[Get("/users/:id")]
  def show(id : Int32)
    "Showing user with ID: #{id}"
  end

  @[Post("/users")]
  def create(name : String, age : Int32, description : String?))
    "Creating user with name: #{name}, age: #{age} and description: #{description}"
  end
end
```

Kemal-controller also supports arrays and named tuples in arguments, so you get a type safe way to handle the endpoint parameters.

```Crystal
struct ProductsController < Kemal::Controller
  @[Get("/products")]
  def filter(categories : Array(String), price_range : NamedTuple(min : Float64, max : Float64))
    "Filtering products in categories: #{categories.join(", ")} with price between #{price_range[:min]} and #{price_range[:max]}"
  end
end
```
It supports nested named tuples/arrays in any combination as well.

```Crystal
struct OrdersController < Kemal::Controller
  @[Post("/orders")]
  def create(items : Array(NamedTuple(id : Int32, quantity : Int32)),
             shipping_address : NamedTuple(street : String, city : String, zip : String))
    "Creating order with items: #{items.inspect} to be shipped to #{shipping_address[:street]}, #{shipping_address[:city]}, #{shipping_address[:zip]}"
  end
end
```

Default values are supported — the parameter must have an explicit type annotation:

```Crystal
struct UsersController < Kemal::Controller
  @[Get("/greet")]
  def greet(name : String = "World", times : Int32 = 1)
    "Hello, #{name}! " * times
  end
end
```

If the parameter is absent from the request the default value is used. Explicit type annotations are required; omitting the type is a compile-time error.

### How the parameters are mapped?

Kemal-controller interprets the form keys almost like Rails does:

- `item[foo]=bar` becomes `item : NamedTuple(foo : String)`
- `items[]=1&items[]=2` becomes `items : Array(Int32)`
- `items[][id]=1&items[][quantity]=2&items[][id]=3&items[][quantity]=4` becomes `items : Array(NamedTuple(id : Int32, quantity : Int32))`
- `name=John` becomes `name : String`

### Supported types

- String
- Int32
- Int64
- Enums
- Bool
- NamedTuple (with nested support)
- Array (with nested support)
- Nilable versions of the above types

More types may be added in the future, feel free to open an issue or a PR if you need something specific.

### Error handling

Two exceptions are raised for bad request parameters:

- `Kemal::MissingParameterError` — a required (non-nilable, no default) parameter was not present in the request.
- `Kemal::InvalidParameterError` — the parameter was present but its value could not be coerced to the declared type (e.g. `"foo"` for an `Int32`, an unrecognised enum member, or an invalid boolean literal).

Both inherit from `Exception`. You can handle them with Kemal's exception-specific error handlers:

```Crystal
error Kemal::MissingParameterError do |env, ex|
  env.response.status_code = 400
  ex.message
end

error Kemal::InvalidParameterError do |env, ex|
  env.response.status_code = 422
  ex.message
end
```

### Enums

Enums are supported as method parameters as well, anything accepted by `Enum.new` or `Enum.parse` is recognized.

### Stripping parameters

If you need to strip all parameters (like leading/trailing spaces) before they
reach your controller methods, you can use the `strip` flag on method annotation.

To strip specific parameters use an array of symbols instead of true.

```Crystal
struct UsersController < Kemal::Controller
    @[Post("/users", strip: true)]
    def create(name : String, description : String?)
      "Creating user with name: '#{name}', description: '#{description}'"
    end

    @[Get("/users/edit", strip: [:email])]
    def login(email : String, password : String)
      "Logging in user with email: '#{email}'"
    end
end
```

### Authenticated/protected routes

If you need to protect some routes with authentication you must set the `auth`
flag to true in the method annotation and implement the `authenticate! : Bool`
method in your controller.

If `authenticate!` returns false the request will be halted and no further
processing will be done, status code is set to 401 (Unauthorized).

```Crystal
struct AdminController < Kemal::Controller
  @[Get("/admin/dashboard", auth: true)]
  def dashboard
    "Welcome to the admin dashboard!"
  end

  def authenticate! : Bool
    if !current_user.try(&.current_user.admin?)
      redirect("/login")
      return false
    end
    true
  end
end
```

### WebSocket routes

WebSocket endpoints are declared with `@[WebSocket]`, taking advantage of Kemal's
own WebSocket support. The method is called once, right after the handshake
completes; use the `socket` getter to register `on_message`/`on_close`/etc.
handlers. Parameters are extracted from the handshake request the same way `Get` does.

```Crystal
struct ChatController < Kemal::Controller
  @[WebSocket("/chat/:room")]
  def chat(room : String)
    socket.send("Welcome to #{room}!")
    socket.on_message do |message|
      socket.send("#{room}: #{message}")
    end
  end
end
```

`strip` works the same as with HTTP routes. `auth` works too, but with one
difference: by the time the method runs the handshake response has already
been sent, so a failed `authenticate!` can't reply with a 401 — the socket is
closed instead with `HTTP::WebSocket::CloseCode::PolicyViolation`.

### Printing routes

You can print all registered routes by calling the `Kemal.print_routes` method,
useful for debugging purposes.

```Crystal
Kemal.config.extra_options do |parser|
  parser.on("--routes", "Show all routes") do
    Kemal.print_routes
    exit(0)
  end
end
```

On `--routes` your app will print something like:

```
   GET  /area51                    TestController#area51()
  POST  /array_of_named_tuples     TestController#array_of_named_tuples(items : Array(NamedTuple(name: String, age: Int32)))
   GET  /hello                     TestController#hello(name : String)
  POST  /hello                     TestController#post_hello(name : String)
   GET  /regular_kemal_route       ?
    WS  /chat/:room                ChatController#chat(room : String)

5 routes
```

## Installation

1. Add the dependency to your `shard.yml`:

   ```yaml
   dependencies:
     kemal-controller:
       github: hugopl/kemal-controller
   ```

2. Run `shards install`


## Contributing

1. Fork it (<https://github.com/hugopl/kemal-controller/fork>)
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 a new Pull Request

## Contributors

- [Hugo Parente Lima](https://github.com/hugopl) - creator and maintainer