require 'hanami/action/base_params' require 'hanami/validations/form' module Hanami class Action # A set of params requested by the client # # It's able to extract the relevant params from a Rack env of from an Hash. # # There are three scenarios: # * When used with Hanami::Router: it contains only the params from the request # * When used standalone: it contains all the Rack env # * Default: it returns the given hash as it is. It's useful for testing purposes. # # @since 0.1.0 class Params < BaseParams include Hanami::Validations::Form # Params errors # # @since 1.1.0 class Errors < SimpleDelegator # @since 1.1.0 # @api private def initialize(errors = {}) super(errors) 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] 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 # include Hanami::Action # # params do # required(:book).schema do # required(:isbn).filled(:str?) # end # end # # def call(params) # # 1. Don't try to save the record if the params aren't valid # return unless params.valid? # # BookRepository.new.create(params[:book]) # rescue Hanami::Model::UniqueConstraintViolationError # # 2. Add an error in case the record wasn't unique # params.errors.add(:book, :isbn, "is not unique") # end # end # # @example Invalid argument # require "hanami/controller" # # class MyAction # include Hanami::Action # # params do # required(:book).schema do # required(:title).filled(:str?) # end # end # # def call(params) # puts params.to_h # => {} # puts params.valid? # => false # puts params.error_messages # => ["Book is missing"] # puts params.errors # => {:book=>["is missing"]} # # 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 # This is a Hanami::Validations extension point # # @since 0.7.0 # @api private def self._base_rules lambda do optional(:_csrf_token).filled(:str?) end end # Define params validations # # @param blk [Proc] the validations definitions # # @since 0.7.0 # # @see https://guides.hanamirb.org/validations/overview # # @example # class Signup # MEGABYTE = 1024 ** 2 # include Hanami::Action # # params do # required(:first_name).filled(:str?) # required(:last_name).filled(:str?) # required(:email).filled?(:str?, format?: /\A.+@.+\z/) # required(:password).filled(:str?).confirmation # required(:terms_of_service).filled(:bool?) # required(:age).filled(:int?, included_in?: 18..99) # optional(:avatar).filled(size?: 1..(MEGABYTE * 3)) # end # # def call(params) # halt 400 unless params.valid? # # ... # end # end def self.params(&blk) validations(&blk || ->() {}) end # 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) @env = env super(_extract_params) @result = validate @params = _params @errors = Errors.new(@result.messages) freeze end # Returns raw params from Rack env # # @return [Hash] # # @since 0.3.2 def raw @input end # 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 # 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) _messages = if messages.is_a?(::Hash) error_messages(messages) else messages.map { |message| "#{k} #{message}" } end result.concat(_messages) 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 # Serialize params to Hash # # @return [::Hash] # # @since 0.3.0 def to_h @params end alias_method :to_hash, :to_h private # @api private def _params _router_params.merge(@result.output) end end end end