Parameters are taken from the Rack env and passed as an argument to #call. They are similar to a Ruby Hash, but they offer an expanded set of features.

Sources

Params can come from:

  • Router variables (eg. /books/:id)
  • Query string (eg. /books?title=Hanami)
  • Request body (eg. a POST request to /books)

Access

To access the value of a param, we can use the subscriber operator #[].

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. def call(params)
  8. self.body = "Query string: #{ params[:q] }"
  9. end
  10. end
  11. end
  12. end
  13. end

If we visit /dashboard?q=foo, we should see Query string: foo.

Symbol Access

Params and nested params can be referenced only via symbols.

  1. params[:q]
  2. params[:book][:title]

Now, what happens if the parameter :book is missing from the request? Because params[:book] is nil, we can’t access :title. In this case Ruby will raise a NoMethodError.

We have a safe solution for our problem: #get. It accepts a list of symbols, where each symbol represents a level in our nested structure.

  1. params.get(:book, :title) # => "Hanami"
  2. params.get(:unknown, :nested, :param) # => nil instead of NoMethodError

Whitelisting

In order to show how whitelisting works, let’s create a new action:

  1. bundle exec hanami generate action web signup#create

We want to provide self-registration for our users. We build a HTML form which posts to an action that accepts the payload and stores it in the users table. That table has a boolean column admin to indicate whether a person has administration permissions.

A malicious user can exploit this scenario by sending this extra parameter to our application, thereby making themselves an administrator.

We can easily fix this problem by filtering the allowed parameters that are permitted inside our application. Please always remember that params represent untrusted input.

We use .params to map the structure of the (nested) parameters.

  1. # apps/web/controllers/signup/create.rb
  2. module Web::Controllers::Signup
  3. class Create
  4. include Web::Action
  5. params do
  6. required(:email).filled
  7. required(:password).filled
  8. required(:address).schema do
  9. required(:country).filled
  10. end
  11. end
  12. def call(params)
  13. puts params[:email] # => "alice@example.org"
  14. puts params[:password] # => "secret"
  15. puts params[:address][:country] # => "Italy"
  16. puts params[:admin] # => nil
  17. end
  18. end
  19. end

Even if admin is sent inside the body of the request, it isn’t accessible from params.

Validations & Coercion

Use Cases

In our example (called “Signup”), we want to make password a required param.

Imagine we introduce a second feature: “Invitations”. An existing user can ask someone to join. Because the invitee will decide a password later on, we want to persist that User record without that value.

If we put password validation in User, we need to handle these two use cases with a conditional. But in the long term this approach is painful from a maintenance perspective.

  1. # Example of poor style for validations
  2. class User
  3. attribute :password, presence: { if: :password_required? }
  4. private
  5. def password_required?
  6. !invited_user? && !admin_password_reset?
  7. end
  8. end

We can see validations as the set of rules for data correctness that we want for a specific use case. For us, a User can be persisted with or without a password, depending on the workflow and the route through which the User is persisted.

Boundaries

The second important aspect is that we use validations to prevent invalid inputs to propagate in our system. In an MVC architecture, the model layer is the farthest from the input. It’s expensive to check the data right before we create a record in the database.

If we consider correct data as a precondition before starting our workflow, we should stop unacceptable inputs as soon as possible.

Think of the following method. We don’t want to continue if the data is invalid.

  1. def expensive_computation(argument)
  2. return if argument.nil?
  3. # ...
  4. end

Usage

We can coerce the Ruby type, validate if a param is required, determine if it is within a range of values, etc..

  1. # apps/web/controllers/signup/create.rb
  2. module Web
  3. module Controllers
  4. module Signup
  5. class Create
  6. include Web::Action
  7. MEGABYTE = 1024 ** 2
  8. params do
  9. required(:name).filled(:str?)
  10. required(:email).filled(:str?, format?: /@/).confirmation
  11. required(:password).filled(:str?).confirmation
  12. required(:terms_of_service).filled(:bool?)
  13. required(:age).filled(:int?, included_in?: 18..99)
  14. optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
  15. end
  16. def call(params)
  17. if params.valid?
  18. # ...
  19. else
  20. # ...
  21. end
  22. end
  23. end
  24. end
  25. end
  26. end

Parameter validations are delegated, under the hood, to Hanami::Validations. Please check the related documentation for a complete list of options and how to share code between validations.

Concrete Classes

The params DSL is really quick and intuitive but it has the drawback that it can be visually noisy and makes it hard to unit test. An alternative is to extract a class and pass it as an argument to .params.

  1. # apps/web/controllers/signup/my_params.rb
  2. module Web
  3. module Controllers
  4. module Signup
  5. class MyParams < Web::Action::Params
  6. MEGABYTE = 1024 ** 2
  7. params do
  8. required(:name).filled(:str?)
  9. required(:email).filled(:str?, format?: /@/).confirmation
  10. required(:password).filled(:str?).confirmation
  11. required(:terms_of_service).filled(:bool?)
  12. required(:age).filled(:int?, included_in?: 18..99)
  13. optional(:avatar).filled(size?: 1..(MEGABYTE * 3)
  14. end
  15. end
  16. end
  17. end
  18. end
  1. # apps/web/controllers/signup/create.rb
  2. require_relative './my_params'
  3. module Web
  4. module Controllers
  5. module Signup
  6. class Create
  7. include Web::Action
  8. params MyParams
  9. def call(params)
  10. if params.valid?
  11. # ...
  12. else
  13. # ...
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end

Inline predicates

In case there is a predicate that is needed only for the current params, you can define inline predicates:

  1. # apps/web/controllers/books/create.rb
  2. module Web
  3. module Controllers
  4. module Books
  5. class Create
  6. include Web::Action
  7. params Class.new(Hanami::Action::Params) {
  8. predicate(:cool?, message: "is not cool") do |current|
  9. current.match(/cool/)
  10. end
  11. validations do
  12. required(:book).schema do
  13. required(:title) { filled? & str? & cool? }
  14. end
  15. end
  16. }
  17. def call(params)
  18. if params.valid?
  19. self.body = 'OK'
  20. else
  21. self.body = params.error_messages.join("\n")
  22. end
  23. end
  24. end
  25. end
  26. end
  27. end

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 params.

  1. # apps/web/controllers/books/create.rb
  2. module Web
  3. module Controllers
  4. module Books
  5. class Create
  6. include Web::Action
  7. accept :json
  8. def call(params)
  9. puts params.to_h # => {}
  10. end
  11. end
  12. end
  13. end
  14. end
  1. $ curl http://localhost:2300/books \
  2. -H "Content-Type: application/json" \
  3. -H "Accept: application/json" \
  4. -d '{"book":{"title":"Hanami"}}' \
  5. -X POST

In order to make book payload available in params, we should enable this feature:

  1. # config/environment.rb
  2. require "hanami/middleware/body_parser"
  3. Hanami.configure do
  4. # ...
  5. middleware.use Hanami::Middleware::BodyParser, :json
  6. end

Now params.get(:book, :title) returns "Hanami".

In case there is no suitable body parser for your format in Hanami, it is possible to declare a new one:

  1. # lib/foo_parser.rb
  2. class FooParser
  3. def mime_types
  4. ['application/foo']
  5. end
  6. def parse(body)
  7. # manually parse body
  8. end
  9. end

and subsequently register it:

  1. # config/environment.rb
  2. require "hanami/middleware/body_parser"
  3. Hanami.configure do
  4. # ...
  5. middleware.use Hanami::Middleware::BodyParser, FooParser.new
  6. end