module Onyx::HTTP::Endpoint

Overview

An encapsulated HTTP endpoint.

You can modify the response as you want as you still have an access to the current context. However, it's a good practice to split business and rendering logic. For this, a endpoint should return a View instance or call the #view method.

Endpoint params can be defined with the Endpoint.params macro (param errors have code 400 for default endpoints and 4000 for Channels). Endpoint errors can be defined with the Endpoint.errors macro.

Endpoints also include Callbacks module, effectively allowing to define .before and .after callbacks, which would be invoked before and after #call. Read more about callbacks at https://github.com/vladfaust/callbacks.cr.

struct Endpoints::GetUser
  include Onyx::HTTP::Endpoint

  params do
    path do
      type id : Int32
    end
  end

  errors do
    type UserNotFound(404), id : Int32
  end

  def call
    user = find_user(path_params.id)
    raise UserNotFound.new(path_params.id) unless user
    return Views::User.new(user)
  end
end

Endpoints::GetUser.call(env) # => Views::User instance, if not raised either Params::Error or UserNotFound

Router example:

router = Onyx::HTTP::Router.new do |r|
  r.get "/", Endpoints::GetUser
  # Equivalent of
  r.get "/" do |context|
    view? = Endpoints::GetUser.call(context)

    if view = view?.as?(HTTP::View)
      context.response.view ||= view
    end
  end
end

Included Modules

Direct including types

Defined in:

onyx-http/endpoint/errors.cr
onyx-http/endpoint/params/form.cr
onyx-http/endpoint/params/json.cr
onyx-http/endpoint/params/path.cr
onyx-http/endpoint/params/query.cr
onyx-http/endpoint/params.cr
onyx-http/endpoint.cr

Constructors

Instance Method Summary

Macro Summary

Constructor Detail

def self.new(context : HTTP::Server::Context) #

[View source]

Instance Method Detail

def after #

[View source]
def before #

[View source]
abstract def call #

Where all the action takes place.


[View source]
def view(view : View) #

Set a view for this request. It takes precendence over the return value:

def call
  view(ViewA.new)
  view(ViewB.new)
  return ViewC.new
end

# The resulting view is ViewB

[View source]

Macro Detail

macro errors(&block) #

Optional errors definition macro. It's a DSL for defining expected errors, such as "User Not Found" -- it's not an exception, but it should halt the request execution. All defined errors will be ancestors of HTTP::Error, allowing them to be effectively handled in HTTP::Rescuers::HTTP. Note that Channels rescue HTTP::Error by themselves, you don't need a rescuer in this case.

struct MyAction
  include Onyx::HTTP::Action

  errors do
    type UserNotFound(404)

    # This error has variable `attributes` and custom block called on initialization
    type InvalidUser(409), attributes : Hash(String, String) do
      # All errors inherit from Exception, so `super` sets the error message
      super("User has invalid attributes: #{attributes}")
    end
  end

  def call
    raise UserNotFound.new
    raise InvalidUser.new({"name" => "too short"})
  rescue e : UserNotFound
    # NOTE: These rescue blocks are for example purposes only,
    # you should not rescue the errors *here* in real applications, leave it to Rescuers

    pp e.code    # => 404
    pp e.payload # => nil
    pp e.message # => nil
  rescue e : InvalidUser
    pp e.code    # => 409
    pp e.payload # => {attributes: {"name" => "too short"}}
    pp e.message # => "User has invalid attributes: {\"name\" => \"too short\"}"
  end
end

[View source]
macro form(require required = false, preserve_body = false, &block) #

Define form params which would be deserialzed from the request body only if its "Content-Type" header is "application/x-www-form-urlencoded". The serialization is powered by HTTP::Params::Serializable.

Options

  • require -- if set to true, will attempt to parse form params regardless of the "Content-Type" header and return a parameter error otherwise; the params.form getter becomes non-nilable
  • preserve_body -- if set to true, the request body would be copied and thus accessible after the parsing

Example

struct UpdateUser
  include Onyx::HTTP::Endpoint

  params do
    path do
      type id : Int32
    end

    form do
      type user do
        type email : String?
        type username : String?
      end
    end
  end

  def call
    if form = params.form
      pp! form.user.email
      pp! form.user.username
    end
  end
end
> curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "user[email][email protected]" http://localhost:5000/users/42
form.user.email    => "[email protected]"
form.user.username => nil

If your endpoint expects form params only, then it can be simplified a bit:

struct UpdateUser
  include Onyx::HTTP::Endpoint

  params do
    path do
      type id : Int32
    end

    form require: true do
      type user do
        type email : String?
        type username : String?
      end
    end
  end

  def call
    pp! params.form.user.email
    pp! params.form.user.username
  end
end
> curl -X POST -d "user[email][email protected]" http://localhost:5000/users/42

[View source]
macro json(require required = false, preserve_body = false, &block) #

Define JSON params which would be deserialized from the request body only if its "Content-Type" header is "application/json". The serialization is powered by stdlib's JSON::Serializable.

Options

  • require -- if set to true, will attempt to parse JSON params regardless of the "Content-Type" header and return a parameter error otherwise; the params.json getter becomes non-nilable
  • preserve_body -- if set to true, the request body would be copied and thus accessible after the parsing

Example

struct UpdateUser
  include Onyx::HTTP::Endpoint

  params do
    path do
      type id : Int32
    end

    json do
      type user do
        type email : String?
        type username : String?
      end
    end
  end

  def call
    if json = params.json
      pp! json.user.email
      pp! json.user.username
    end
  end
end
> curl -X POST -H "Content-Type: application/json" -d '{"user":{"email":"[email protected]"}}' http://localhost:5000/users/1
json.user.email    => "[email protected]"
json.user.username => nil

If your endpoint expects JSON params only, then it can be simplified a bit:

struct UpdateUser
  include Onyx::HTTP::Endpoint

  params do
    path do
      type id : Int32
    end

    json require: true do
      type user do
        type email : String?
        type username : String?
      end
    end
  end

  def call
    pp! params.json.user.email
    pp! params.json.user.username
  end
end
> curl -X POST -d '{"user":{"email":"[email protected]"}}' http://localhost:5000/users/1

[View source]
macro params(&block) #

Define endpoint params. You should call .path, .query, .form and .json macros within the block. Once .params is called, a #params getter would be set on every endpoint initialization. The #params variable would have according #path, #query, #form and #json getters itself.

It is powered by HTTP::Params::Serializable, and it can raise PathParamsError, QueryParamsError, FormBodyError or JSONBodyError, which all are HTTP::Errors with 400 code.


[View source]
macro path(&block) #

Define path params which are usually extracted from the request URL by Onyx::HTTP::Router. Serialization is powered by HTTP::Params::Serializable.

NOTE It does not extracts params from URL by itself, you need to have a router which extracts path params into the request.path_params variable, for example, Onyx::HTTP::Router; this code only deserializes them.

Path params do not support neither nested nor array values.

struct GetUser
  include Onyx::HTTP::Endpoint

  params do
    path do
      type id : Int32
    end
  end

  def call
    pp! params.path.id
  end
end
> curl http://localhost:5000/users/1
params.path.id => 1

[View source]
macro query(&block) #

Define query params for serialization powered by HTTP::Params::Serializable.

struct IndexUsers
  include Onyx::HTTP::Endpoint

  params do
    query do
      type limit : Int32? = 10
      type offset : Int32? = 0
    end
  end

  def call
    pp! params.query.limit
    pp! params.query.offset
  end
end
> curl http://localhost:5000/users?offset=5
params.query.limit  => 10
params.query.offset => 5

[View source]