Module: Rhino::ArgumentBinder
- Defined in:
- lib/rhino/argument_binder.rb
Overview
Binds the arguments a client sent in the bracket query form (?scope[param]=value, ?attributes[param]=value) to the parameters a model declared, in declared order.
The algorithm is shared by named scopes and computed attributes so the two features cannot drift. Everything that differs between them is passed in:
subjectis the noun used in every error message ("Scope", "Computed attribute"), so each feature keeps its own wording;error_classis the exception raised, so each feature keeps its own controllerrescue_from;underscore_keysreproduces the scope binder's wire-name translation. Computed attributes match parameter names verbatim, exactly as Laravel and NestJS do, so they leave it off.
Nothing here decides whether a name may be used: callers MUST run the declared-check and the policy-check BEFORE binding, so an argument error can only ever be seen for a name the caller was already allowed to use.
Class Method Summary collapse
-
.bind(subject:, name:, spec:, raw:, error_class:, underscore_keys: false) ⇒ Object
Bind the raw value a client sent for one name to positional arguments, in the order the model declared them.
-
.coerce(value) ⇒ Object
Query-string values always arrive as strings; hand callables real booleans so a check cannot be fooled by the string "false".
-
.normalize_params(params, optional = []) ⇒ Object
Clean a declared parameter list: stringify the names, and drop any
optionalentry that is not actually a declared parameter. -
.normalize_raw_arguments(subject:, name:, params:, raw:, error_class:, underscore_keys: false) ⇒ Object
Turn the raw query-string value into a parameter name => value hash.
Class Method Details
.bind(subject:, name:, spec:, raw:, error_class:, underscore_keys: false) ⇒ Object
Bind the raw value a client sent for one name to positional arguments, in the order the model declared them.
raw is whatever the query string produced for the bracket key: nil or ""
(no arguments), a scalar (the single parameter), or a hash of parameter
name => value.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/rhino/argument_binder.rb', line 39 def bind(subject:, name:, spec:, raw:, error_class:, underscore_keys: false) params = Array(spec[:params]).map(&:to_s) optional = Array(spec[:optional]).map(&:to_s) given = normalize_raw_arguments( subject: subject, name: name, params: params, raw: raw, error_class: error_class, underscore_keys: underscore_keys ) given.each_key do |key| unless params.include?(key) raise error_class, "#{subject} '#{name}' does not accept parameter '#{key}'" end end args = params.map do |param| if given.key?(param) coerce(given[param]) elsif optional.include?(param) nil else raise error_class, "#{subject} '#{name}' requires parameter '#{param}'" end end # Drop trailing nils so an omitted optional parameter falls back to the # default in the callable's own signature. # # NOTE: `args.empty?` rather than `args.any?` — Array#any? without a block # is false for [nil], so the old form skipped the drop entirely when EVERY # argument was nil (an all-optional declaration with nothing sent), passing # [nil] where Laravel and NestJS pass []. Every other case is unchanged. args.pop until args.empty? || !args.last.nil? args end |
.coerce(value) ⇒ Object
Query-string values always arrive as strings; hand callables real booleans so a check cannot be fooled by the string "false".
115 116 117 118 119 120 121 122 123 |
# File 'lib/rhino/argument_binder.rb', line 115 def coerce(value) return value unless value.is_a?(String) case value.downcase when "true" then true when "false" then false else value end end |
.normalize_params(params, optional = []) ⇒ Object
Clean a declared parameter list: stringify the names, and drop any
optional entry that is not actually a declared parameter.
27 28 29 30 31 |
# File 'lib/rhino/argument_binder.rb', line 27 def normalize_params(params, optional = []) params = Array(params).map(&:to_s) { params: params, optional: Array(optional).map(&:to_s) & params } end |
.normalize_raw_arguments(subject:, name:, params:, raw:, error_class:, underscore_keys: false) ⇒ Object
Turn the raw query-string value into a parameter name => value hash.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/rhino/argument_binder.rb', line 76 def normalize_raw_arguments(subject:, name:, params:, raw:, error_class:, underscore_keys: false) # ?scope[archived]= (or a bare ?scope[archived]): no arguments. A name # with required parameters still fails, in bind, naming them. return {} if raw.nil? || raw == "" raw = raw.to_unsafe_h if raw.respond_to?(:to_unsafe_h) if raw.is_a?(Array) # A positional list (scope[between][]=a) names nothing. raise error_class, "#{subject} '#{name}' requires named parameters" end unless raw.is_a?(Hash) raise error_class, "#{subject} '#{name}' does not accept arguments" if params.empty? # A bare value binds to the single declared parameter. Two parameters can # never be guessed at from one value. if params.length > 1 raise error_class, "#{subject} '#{name}' requires named parameters" end return { params.first => raw } end raise error_class, "#{subject} '#{name}' does not accept arguments" if params.empty? raw.each_with_object({}) do |(key, value), out| unless value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.nil? raise error_class, "#{subject} '#{name}' requires named parameters" end key = key.to_s out[underscore_keys ? key.underscore : key] = value end end |