lib/metaractor/parameters.rb in metaractor-2.1.1 vs lib/metaractor/parameters.rb in metaractor-3.0.0
- old
+ new
@@ -1,76 +1,178 @@
+# Special thanks to the `hashie` and `active_attr` gems for code and inspiration.
+
module Metaractor
module Parameters
def self.included(base)
base.extend ClassMethods
base.class_eval do
include Metaractor::HandleErrors
class << self
- attr_writer :_required_parameters
- attr_writer :_optional_parameters
- attr_writer :_allow_blank
+ attr_writer :requirement_trees
end
before :remove_blank_values
+ before :apply_defaults
before :validate_required_parameters
end
end
+ class Parameter
+ include Comparable
+
+ attr_reader :name
+
+ def initialize(name, **options)
+ @name = name.to_sym
+ @options = options
+ end
+
+ def <=>(other)
+ return nil unless other.instance_of? self.class
+ return nil if name == other.name && options != other.options
+ self.name.to_s <=> other.name.to_s
+ end
+
+ def [](key)
+ @options[key]
+ end
+
+ def dig(name, *names)
+ @options.dig(name, *names)
+ end
+
+ def merge!(**options)
+ @options.merge!(**options)
+ end
+
+ def to_s
+ name.to_s
+ end
+
+ def to_sym
+ name
+ end
+
+ protected
+ attr_reader :options
+ end
+
module ClassMethods
- def _required_parameters
- @_required_parameters ||= []
+ def parameter(name, **options)
+ if param = self.parameter_hash[name.to_sym]
+ param.merge!(**options)
+ else
+ Parameter.new(name, **options).tap do |parameter|
+ self.parameter_hash[parameter.name] = parameter
+ end
+ end
end
- def required(*params)
- self._required_parameters += params
+ def parameters(*names, **options)
+ names.each do |name|
+ parameter(name, **options)
+ end
end
- alias_method :required_parameters, :required
- def _optional_parameters
- @_optional_parameters ||= []
+ def parameter_hash
+ @parameters ||= {}
end
- def optional(*params)
- self._optional_parameters += params
+ def requirement_trees
+ @requirement_trees ||= []
end
- def _allow_blank
- @_allow_blank ||= []
+ def required(*params, **options)
+ if params.empty?
+ tree = options
+ self.requirement_trees << tree
+ parameters(*parameters_in_tree(tree), required: tree)
+ else
+ parameters(*params, required: true, **options)
+ end
end
- def allow_blank(*params)
- self._allow_blank += params
+ def optional(*params, **options)
+ parameters(*params, **options)
end
def validate_parameters(*hooks, &block)
hooks << block if block
hooks.each {|hook| validate_hooks.push(hook) }
end
def validate_hooks
@validate_hooks ||= []
end
+
+ def parameters_in_tree(tree)
+ if tree.respond_to?(:to_h)
+ tree.to_h.values.first.to_a.flat_map {|t| parameters_in_tree(t)}
+ else
+ [tree]
+ end
+ end
end
+ def parameters
+ self.class.parameter_hash
+ end
+
+ def requirement_trees
+ self.class.requirement_trees
+ end
+
+ def requirement_trees=(trees)
+ self.class.requirement_trees=(trees)
+ end
+
def remove_blank_values
to_delete = []
- context.each_pair do |k,v|
- next if self.class._allow_blank.include?(k)
+ context.each_pair do |name, value|
+ next if parameters.dig(name, :allow_blank)
# The following regex is borrowed from Rails' String#blank?
- to_delete << k if (v.is_a?(String) && /\A[[:space:]]*\z/ === v) || v.nil?
+ to_delete << name if (value.is_a?(String) && /\A[[:space:]]*\z/ === value) || value.nil?
end
- to_delete.each do |k|
- context.delete_field k
+
+ to_delete.each do |name|
+ context.delete_field name
end
end
+ def apply_defaults
+ parameters.each do |name, parameter|
+ next unless parameter[:default]
+
+ unless context.has_key?(name)
+ context[name] = _parameter_default(name)
+ end
+ end
+ end
+
+ def _parameter_default(name)
+ default = self.parameters[name][:default]
+
+ case
+ when default.respond_to?(:call) then instance_exec(&default)
+ when default.respond_to?(:dup) then default.dup
+ else default
+ end
+ end
+
def validate_required_parameters
context.errors ||= []
- self.class._required_parameters.each do |param|
- require_parameter param
+ parameters.each do |name, parameter|
+ next if !parameter[:required] ||
+ parameter[:required].is_a?(Hash)
+
+ require_parameter name
+ end
+
+ requirement_trees.each do |tree|
+ require_parameter tree
end
run_validate_hooks
context.fail! unless context.errors.empty?