class Hanami::Action::Params::DefaultContract::Result

Attributes

_contract[R]

@api private @since 2.2.0

Public Class Methods

new(attrs) click to toggle source
# File lib/hanami/action/params.rb, line 41
      def initialize(attrs) = @attrs = Utils::Hash.deep_symbolize(attrs)
      def to_h = @attrs
      def errors = {}
    end
  end

  # Params errors
  #
  # @since 1.1.0
  class Errors < SimpleDelegator
    # @since 1.1.0
    # @api private
    def initialize(errors = {})
      super(errors.dup)
    end

    # Add an error to the param validations
    #
    # This has a semantic similar to `Hash#dig` where you use a set of keys
    # to get a nested value, here you use a set of keys to set a nested
    # value.
    #
    # @param args [Array<Symbol, String>] an array of arguments: the last
    #   one is the message to add (String), while the beginning of the array
    #   is made of keys to reach the attribute.
    #
    # @raise [ArgumentError] when try to add a message for a key that is
    #   already filled with incompatible message type.
    #   This usually happens with nested attributes: if you have a `:book`
    #   schema and the input doesn't include data for `:book`, the messages
    #   will be `["is missing"]`. In that case you can't add an error for a
    #   key nested under `:book`.
    #
    # @since 1.1.0
    #
    # @example Basic usage
    #   require "hanami/controller"
    #
    #   class MyAction < Hanami::Action
    #     params do
    #       required(:book).schema do
    #         required(:isbn).filled(:str?)
    #       end
    #     end
    #
    #     def handle(req, res)
    #       # 1. Don't try to save the record if the params aren't valid
    #       return unless req.params.valid?
    #
    #       BookRepository.new.create(req.params[:book])
    #     rescue Hanami::Model::UniqueConstraintViolationError
    #       # 2. Add an error in case the record wasn't unique
    #       req.params.errors.add(:book, :isbn, "is not unique")
    #     end
    #   end
    #
    # @example Invalid argument
    #   require "hanami/controller"
    #
    #   class MyAction < Hanami::Action
    #     params do
    #       required(:book).schema do
    #         required(:title).filled(:str?)
    #       end
    #     end
    #
    #     def handle(req, *)
    #       puts req.params.to_h   # => {}
    #       puts req.params.valid? # => false
    #       puts req.params.error_messages # => ["Book is missing"]
    #       puts req.params.errors         # => {:book=>["is missing"]}
    #
    #       req.params.errors.add(:book, :isbn, "is not unique") # => ArgumentError
    #     end
    #   end
    def add(*args)
      *keys, key, error = args
      _nested_attribute(keys, key) << error
    rescue TypeError
      raise ArgumentError.new("Can't add #{args.map(&:inspect).join(', ')} to #{inspect}")
    end

    private

    # @since 1.1.0
    # @api private
    def _nested_attribute(keys, key)
      if keys.empty?
        self
      else
        keys.inject(self) { |result, k| result[k] ||= {} }
        dig(*keys)
      end[key] ||= []
    end
  end

  # Defines validations for the params, using the `params` schema of a dry-validation contract.
  #
  # @param block [Proc] the schema definition
  #
  # @see https://dry-rb.org/gems/dry-validation/
  #
  # @api public
  # @since 0.7.0
  def self.params(&block)
    unless defined?(Dry::Validation::Contract)
      message = %(To use `.params`, please add the "hanami-validations" gem to your Gemfile)
      raise NoMethodError, message
    end

    @_contract = Class.new(Dry::Validation::Contract) { params(&block || -> {}) }.new
  end

  class << self
    # @api private
    # @since 2.2.0
    attr_reader :_contract
  end

  # @attr_reader env [Hash] the Rack env
  #
  # @since 0.7.0
  # @api private
  attr_reader :env

  # @attr_reader raw [Hash] the raw params from the request
  #
  # @since 0.7.0
  # @api private
  attr_reader :raw

  # Returns structured error messages
  #
  # @return [Hash]
  #
  # @since 0.7.0
  #
  # @example
  #   params.errors
  #     # => {
  #            :email=>["is missing", "is in invalid format"],
  #            :name=>["is missing"],
  #            :tos=>["is missing"],
  #            :age=>["is missing"],
  #            :address=>["is missing"]
  #          }
  attr_reader :errors

  # Initialize the params and freeze them.
  #
  # @param env [Hash] a Rack env or an hash of params.
  #
  # @return [Params]
  #
  # @since 0.1.0
  # @api private
  def initialize(env:, contract: nil)
    @env = env
    @raw = _extract_params

    # Fall back to the default contract here, rather than in the `._contract` method itself.
    # This allows `._contract` to return nil when there is no user-defined contract, which is
    # important for the backwards compatibility behavior in `Validatable::ClassMethods#params`.
    contract ||= self.class._contract || DefaultContract
    validation = contract.call(raw)

    @params = validation.to_h
    @errors = Errors.new(validation.errors.to_h)

    freeze
  end

  # Returns the value for the given params key.
  #
  # @param key [Symbol] the key
  #
  # @return [Object,nil] the associated value, if found
  #
  # @since 0.7.0
  # @api public
  def [](key)
    @params[key]
  end

  # Returns an value associated with the given params key.
  #
  # You can access nested attributes by listing all the keys in the path. This uses the same key
  # path semantics as `Hash#dig`.
  #
  # @param keys [Array<Symbol,Integer>] the key
  #
  # @return [Object,NilClass] return the associated value, if found
  #
  # @example
  #   require "hanami/controller"
  #
  #   module Deliveries
  #     class Create < Hanami::Action
  #       def handle(req, *)
  #         req.params.get(:customer_name)     # => "Luca"
  #         req.params.get(:uknown)            # => nil
  #
  #         req.params.get(:address, :city)    # => "Rome"
  #         req.params.get(:address, :unknown) # => nil
  #
  #         req.params.get(:tags, 0)           # => "foo"
  #         req.params.get(:tags, 1)           # => "bar"
  #         req.params.get(:tags, 999)         # => nil
  #
  #         req.params.get(nil)                # => nil
  #       end
  #     end
  #   end
  #
  # @since 0.7.0
  # @api public
  def get(*keys)
    @params.dig(*keys)
  end

  # This is for compatibility with Hanami::Helpers::FormHelper::Values
  #
  # @api private
  # @since 0.8.0
  alias_method :dig, :get

  # Returns flat collection of full error messages
  #
  # @return [Array]
  #
  # @since 0.7.0
  #
  # @example
  #   params.error_messages
  #     # => [
  #            "Email is missing",
  #            "Email is in invalid format",
  #            "Name is missing",
  #            "Tos is missing",
  #            "Age is missing",
  #            "Address is missing"
  #          ]
  def error_messages(error_set = errors)
    error_set.each_with_object([]) do |(key, messages), result|
      k = Utils::String.titleize(key)

      msgs = if messages.is_a?(::Hash)
               error_messages(messages)
             else
               messages.map { |message| "#{k} #{message}" }
             end

      result.concat(msgs)
    end
  end

  # Returns true if no validation errors are found,
  # false otherwise.
  #
  # @return [TrueClass, FalseClass]
  #
  # @since 0.7.0
  #
  # @example
  #   params.valid? # => true
  def valid?
    errors.empty?
  end

  # Iterates over the params.
  #
  # Calls the given block with each param key-value pair; returns the full hash of params.
  #
  # @yieldparam key [Symbol]
  # @yieldparam value [Object]
  #
  # @return [to_h]
  #
  # @since 0.7.1
  # @api public
  def each(&blk)
    to_h.each(&blk)
  end

  # Serialize validated params to Hash
  #
  # @return [::Hash]
  #
  # @since 0.3.0
  def to_h
    @params
  end
  alias_method :to_hash, :to_h

  # Pattern-matching support
  #
  # @return [::Hash]
  #
  # @since 2.0.2
  def deconstruct_keys(*)
    to_hash
  end

  private

  # @since 0.7.0
  # @api private
  def _extract_params
    result = {}

    if env.key?(Action::RACK_INPUT)
      result.merge! ::Rack::Request.new(env).params
      result.merge! _router_params
    else
      result.merge! _router_params(env)
      env[Action::REQUEST_METHOD] ||= Action::DEFAULT_REQUEST_METHOD
    end

    result
  end

  # @since 0.7.0
  # @api private
  def _router_params(fallback = {})
    env.fetch(ROUTER_PARAMS) do
      if session = fallback.delete(Action::RACK_SESSION)
        fallback[Action::RACK_SESSION] = Utils::Hash.deep_symbolize(session)
      end

      fallback
    end
  end
end
params(&block) click to toggle source

Defines validations for the params, using the ‘params` schema of a dry-validation contract.

@param block [Proc] the schema definition

@see dry-rb.org/gems/dry-validation/

@api public @since 0.7.0

# File lib/hanami/action/params.rb, line 145
def self.params(&block)
  unless defined?(Dry::Validation::Contract)
    message = %(To use `.params`, please add the "hanami-validations" gem to your Gemfile)
    raise NoMethodError, message
  end

  @_contract = Class.new(Dry::Validation::Contract) { params(&block || -> {}) }.new
end

Public Instance Methods

[](key) click to toggle source

Returns the value for the given params key.

@param key [Symbol] the key

@return [Object,nil] the associated value, if found

@since 0.7.0 @api public

# File lib/hanami/action/params.rb, line 221
def [](key)
  @params[key]
end
_extract_params() click to toggle source

@since 0.7.0 @api private

# File lib/hanami/action/params.rb, line 348
def _extract_params
  result = {}

  if env.key?(Action::RACK_INPUT)
    result.merge! ::Rack::Request.new(env).params
    result.merge! _router_params
  else
    result.merge! _router_params(env)
    env[Action::REQUEST_METHOD] ||= Action::DEFAULT_REQUEST_METHOD
  end

  result
end
_router_params(fallback = {}) click to toggle source

@since 0.7.0 @api private

# File lib/hanami/action/params.rb, line 364
def _router_params(fallback = {})
  env.fetch(ROUTER_PARAMS) do
    if session = fallback.delete(Action::RACK_SESSION)
      fallback[Action::RACK_SESSION] = Utils::Hash.deep_symbolize(session)
    end

    fallback
  end
end
deconstruct_keys(*) click to toggle source

Pattern-matching support

@return [::Hash]

@since 2.0.2

# File lib/hanami/action/params.rb, line 340
def deconstruct_keys(*)
  to_hash
end
each(&blk) click to toggle source

Iterates over the params.

Calls the given block with each param key-value pair; returns the full hash of params.

@yieldparam key [Symbol] @yieldparam value [Object]

@return [to_h]

@since 0.7.1 @api public

# File lib/hanami/action/params.rb, line 321
def each(&blk)
  to_h.each(&blk)
end
error_messages(error_set = errors) click to toggle source

Returns flat collection of full error messages

@return [Array]

@since 0.7.0

@example

params.error_messages
  # => [
         "Email is missing",
         "Email is in invalid format",
         "Name is missing",
         "Tos is missing",
         "Age is missing",
         "Address is missing"
       ]
# File lib/hanami/action/params.rb, line 283
def error_messages(error_set = errors)
  error_set.each_with_object([]) do |(key, messages), result|
    k = Utils::String.titleize(key)

    msgs = if messages.is_a?(::Hash)
             error_messages(messages)
           else
             messages.map { |message| "#{k} #{message}" }
           end

    result.concat(msgs)
  end
end
errors(= {}) click to toggle source
# File lib/hanami/action/params.rb, line 43
  def errors = {}
end
get(*keys) click to toggle source

Returns an value associated with the given params key.

You can access nested attributes by listing all the keys in the path. This uses the same key path semantics as ‘Hash#dig`.

@param keys [Array<Symbol,Integer>] the key

@return [Object,NilClass] return the associated value, if found

@example

require "hanami/controller"

module Deliveries
  class Create < Hanami::Action
    def handle(req, *)
      req.params.get(:customer_name)     # => "Luca"
      req.params.get(:uknown)            # => nil

      req.params.get(:address, :city)    # => "Rome"
      req.params.get(:address, :unknown) # => nil

      req.params.get(:tags, 0)           # => "foo"
      req.params.get(:tags, 1)           # => "bar"
      req.params.get(:tags, 999)         # => nil

      req.params.get(nil)                # => nil
    end
  end
end

@since 0.7.0 @api public

# File lib/hanami/action/params.rb, line 257
def get(*keys)
  @params.dig(*keys)
end
to_h(= @attrs) click to toggle source
# File lib/hanami/action/params.rb, line 42
    def to_h = @attrs
    def errors = {}
  end
end
valid?() click to toggle source

Returns true if no validation errors are found, false otherwise.

@return [TrueClass, FalseClass]

@since 0.7.0

@example

params.valid? # => true
# File lib/hanami/action/params.rb, line 306
def valid?
  errors.empty?
end