Module: Rhino::ScopeSpec

Defined in:
lib/rhino/scope_spec.rb

Overview

Parses the rhino_scopes declaration and binds the arguments a client sent for ?scope[param]=value to the scope's parameters.

Declaration forms (all may be mixed in one call):

rhino_scopes :archived,                                   # no parameters
           since: { params: [:date] },                  # one parameter
           window: { params: %i[min max] },             # two, both required
           titled: { params: %i[title status], optional: [:status] },
           mine:   ->(relation, user) { ... },          # legacy proc
           fresh:  Scopes::FreshScope                    # legacy scope class

A scope with no declared parameters never receives arguments: sending any is a 403, so a scope written without client input can never be handed some.

Class Method Summary collapse

Class Method Details

.bind(name, spec, raw) ⇒ Object

Bind the raw value a client sent for one scope to positional arguments, in the order the model declared them.

raw is whatever the query string produced for scope: nil or "" (no arguments), a scalar (the single parameter), or a hash of parameter name => value.

Raises Rhino::InvalidScopeArgumentsError.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rhino/scope_spec.rb', line 50

def bind(name, spec, raw)
  params = spec[:params]
  given = normalize_raw_arguments(name, params, raw)

  given.each_key do |key|
    unless params.include?(key)
      raise Rhino::InvalidScopeArgumentsError, "Scope '#{name}' does not accept parameter '#{key}'"
    end
  end

  args = params.map do |param|
    if given.key?(param)
      coerce(given[param])
    elsif spec[:optional].include?(param)
      nil
    else
      raise Rhino::InvalidScopeArgumentsError, "Scope '#{name}' requires parameter '#{param}'"
    end
  end

  # Drop trailing nils so an omitted optional parameter falls back to the
  # default in the scope's own signature.
  args.pop while args.any? && args.last.nil?
  args
end

.coerce(value) ⇒ Object

Query-string values always arrive as strings; hand scope bodies real booleans so a check cannot be fooled by the string "false".



114
115
116
117
118
119
120
121
122
# File 'lib/rhino/scope_spec.rb', line 114

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(declared) ⇒ Object

Normalize a raw allowed_scopes hash into name => { target:, params:, optional: }.



23
24
25
26
27
28
# File 'lib/rhino/scope_spec.rb', line 23

def normalize(declared)
  (declared || {}).each_with_object({}) do |(name, value), out|
    key = name.to_s
    out[key] = normalize_entry(key, value)
  end
end

.normalize_entry(name, value) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/rhino/scope_spec.rb', line 30

def normalize_entry(name, value)
  if value.is_a?(Hash) || value.is_a?(ActiveSupport::HashWithIndifferentAccess)
    spec = value.symbolize_keys
    params = Array(spec[:params]).map(&:to_s)
    optional = Array(spec[:optional]).map(&:to_s) & params

    { target: spec[:with] || name.to_sym, params: params, optional: optional }
  else
    { target: value, params: [], optional: [] }
  end
end

.normalize_raw_arguments(name, params, raw) ⇒ Object



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
# File 'lib/rhino/scope_spec.rb', line 76

def normalize_raw_arguments(name, params, raw)
  # ?scope[archived]= (or a bare ?scope[archived]): no arguments. A scope
  # 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 Rhino::InvalidScopeArgumentsError, "Scope '#{name}' requires named parameters"
  end

  unless raw.is_a?(Hash)
    raise Rhino::InvalidScopeArgumentsError, "Scope '#{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 Rhino::InvalidScopeArgumentsError, "Scope '#{name}' requires named parameters"
    end

    return { params.first => raw }
  end

  raise Rhino::InvalidScopeArgumentsError, "Scope '#{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 Rhino::InvalidScopeArgumentsError, "Scope '#{name}' requires named parameters"
    end

    out[key.to_s.underscore] = value
  end
end