• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MushroomObserver / mushroom-observer / 13132561742

04 Feb 2025 09:41AM UTC coverage: 93.307% (-0.01%) from 93.318%
13132561742

Pull #2696

github

nimmolo
Add more hash tests
Pull Request #2696: Update logic in `Query::Validation`

84 of 86 new or added lines in 10 files covered. (97.67%)

20 existing lines in 6 files now uncovered.

27368 of 29331 relevant lines covered (93.31%)

583.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.51
/app/classes/query/modules/validation.rb
1
# frozen_string_literal: true
2

3
# validation of Query parameters
4
module Query::Modules::Validation
1✔
5
  attr_accessor :params, :params_cache
1✔
6

7
  def validate_params
1✔
8
    old_params = @params.dup.compact.symbolize_keys
2,922✔
9
    new_params = {}
2,922✔
10
    permitted_params = parameter_declarations.slice(*old_params.keys)
2,922✔
11
    permitted_params.each do |param, param_type|
2,922✔
12
      val = old_params[param]
5,015✔
13
      val = validate_value(param_type, param, val) if val.present?
5,015✔
14
      new_params[param] = val
4,995✔
15
    end
16
    check_for_unexpected_params(old_params)
2,902✔
17
    @params = new_params
2,901✔
18
  end
19

20
  def check_for_unexpected_params(old_params)
1✔
21
    unexpected_params = old_params.except(*parameter_declarations.keys)
2,902✔
22
    return if unexpected_params.keys.empty?
2,902✔
23

24
    str = unexpected_params.keys.map(&:to_s).join("', '")
1✔
25
    raise("Unexpected parameter(s) '#{str}' for #{model} query.")
1✔
26
  end
27

28
  def validate_value(param_type, param, val)
1✔
29
    if param_type.is_a?(Array)
4,907✔
30
      array_validate(param, val, param_type.first).flatten
1,543✔
31
    else
32
      # scalar_validate with ambiguous lookup could return an array
33
      val = scalar_validate(param, val, param_type)
3,364✔
34
      val = val.first if val.is_a?(Array)
3,349✔
35
      val
3,349✔
36
    end
37
  end
38

39
  def array_validate(param, val, param_type)
1✔
40
    case val
1,543✔
41
    when Array
42
      # Lookup in scalar_validate could return multiple matches per val
43
      # so the returned array could contain nested arrays.
44
      val[0, MO.query_max_array].map do |val2|
464✔
45
        scalar_validate(param, val2, param_type)
799✔
46
      end
47
    when ::API2::OrderedRange
48
      [scalar_validate(param, val.begin, param_type),
39✔
49
       scalar_validate(param, val.end, param_type)]
50
    else
51
      [scalar_validate(param, val, param_type)]
1,040✔
52
    end
53
  end
54

55
  def scalar_validate(param, val, param_type)
1✔
56
    case param_type
5,452✔
57
    when Symbol
58
      send(:"validate_#{param_type}", param, val)
3,435✔
59
    when Class
60
      validate_class_param(param, val, param_type)
1,848✔
61
    when Hash
62
      validate_hash_param(param, val, param_type)
169✔
63
    else
64
      raise("Invalid declaration of :#{param} for #{model} " \
×
65
            "query! (invalid type: #{param_type.class.name})")
66
    end
67
  end
68

69
  def validate_class_param(param, val, param_type)
1✔
70
    # :names may come with modifier "flag" params that indicate synonyms, etc.
71
    # Immediately look those up and add any new ids to the :names array.
72
    if param == :names
1,848✔
73
      validate_names_record(param, val, param_type)
627✔
74
    elsif param_type.respond_to?(:descends_from_active_record?)
1,221✔
75
      validate_record(param, val, param_type)
1,221✔
76
    else
NEW
77
      raise(
×
78
        "Don't know how to parse #{param_type} :#{param} for #{model} query."
79
      )
80
    end
81
  end
82

83
  def validate_hash_param(param, val, param_type)
1✔
84
    if [:string, :boolean].include?(param_type.keys.first)
169✔
85
      validate_enum(param, val, param_type)
165✔
86
    else
87
      validate_nested_params(param, val, param_type)
4✔
88
    end
89
  end
90

91
  def validate_nested_params(_param, val, hash)
1✔
92
    val2 = {}
4✔
93
    hash.each do |key, arg_type|
4✔
94
      val2[key] = scalar_validate(key, val[key], arg_type)
7✔
95
    end
96
    val2
1✔
97
  end
98

99
  def validate_enum(param, val, hash)
1✔
100
    if hash.keys.length != 1
165✔
101
      raise(
×
102
        "Invalid enum declaration for :#{param} for #{model} " \
103
        "query! (wrong number of keys in hash)"
104
      )
105
    end
106

107
    arg_type = hash.keys.first
165✔
108
    set = hash.values.first
165✔
109
    unless set.is_a?(Array)
165✔
110
      raise(
×
111
        "Invalid enum declaration for :#{param} for #{model} " \
112
        "query! (expected value to be an array of allowed values)"
113
      )
114
    end
115

116
    val2 = scalar_validate(param, val, arg_type)
165✔
117
    if (arg_type == :string) && set.include?(val2.to_sym)
164✔
118
      val2 = val2.to_sym
89✔
119
    elsif set.exclude?(val2)
75✔
120
      raise("Value for :#{param} should be one of the following: " \
2✔
121
            "#{set.inspect}.")
122
    end
123
    val2
162✔
124
  end
125

126
  def validate_boolean(param, val)
1✔
127
    case val
1,090✔
128
    # Disable cop because we do mean to symbols with boolean names
129
    # rubocop:disable Lint/BooleanSymbol
130
    when :true, :yes, :on, "true", "yes", "on", "1", 1, true
131
      true
1,086✔
132
    when :false, :no, :off, "false", "no", "off", "0", 0, false, nil
133
      false
4✔
134
    # rubocop:enable Lint/BooleanSymbol
135
    else
136
      raise("Value for :#{param} should be boolean, got: #{val.inspect}")
×
137
    end
138
  end
139

140
  def validate_integer(param, val)
1✔
141
    if val.is_a?(Integer) || val.is_a?(String) && val.match(/^-?\d+$/)
×
142
      val.to_i
×
143
    elsif val.blank?
×
144
      nil
145
    else
146
      raise("Value for :#{param} should be an integer, got: #{val.inspect}")
×
147
    end
148
  end
149

150
  def validate_float(param, val)
1✔
151
    if val.is_a?(Integer) || val.is_a?(Float) ||
86✔
152
       (val.is_a?(String) && val.match(/^-?(\d+(\.\d+)?|\.\d+)$/))
7✔
153
      val.to_f
84✔
154
    else
155
      raise("Value for :#{param} should be a float, got: #{val.inspect}")
2✔
156
    end
157
  end
158

159
  def validate_record(param, val, type = ActiveRecord::Base)
1✔
160
    if val.is_a?(type)
1,370✔
161
      raise("Value for :#{param} is an unsaved #{type} instance.") unless val.id
335✔
162

163
      set_cached_parameter_instance(param, val)
335✔
164
      val.id
335✔
165
    elsif could_be_record_id?(param, val)
1,035✔
166
      val.to_i
977✔
167
    elsif val.is_a?(String) && param != :ids
58✔
168
      # Lookups for each val may return more than one record, though the lookup
169
      # string is generally unique. For an example, search `two_agaricus_bug`.
170
      lookup_records_by_name(param, val, type, method: :ids, all: true)
51✔
171
    else
172
      raise("Value for :#{param} should be id, string " \
7✔
173
            "or #{type} instance, got: #{val.inspect}")
174
    end
175
  end
176

177
  def validate_string(param, val)
1✔
178
    if val.is_any?(Integer, Float, String, Symbol)
2,055✔
179
      val.to_s
2,047✔
180
    else
181
      raise("Value for :#{param} should be a string or symbol, " \
8✔
182
            "got a #{val.class}: #{val.inspect}")
183
    end
184
  end
185

186
  def validate_date(param, val)
1✔
187
    if val.acts_like?(:date)
55✔
188
      format("%04d-%02d-%02d", val.year, val.mon, val.day)
19✔
189
    elsif /^\d\d\d\d(-\d\d?){0,2}$/i.match?(val.to_s) ||
36✔
190
          /^\d\d?(-\d\d?)?$/i.match?(val.to_s)
191
      val
36✔
192
    elsif val.blank? || val.to_s == "0"
×
193
      nil
194
    else
195
      raise("Value for :#{param} should be a date (YYYY-MM-DD or MM-DD), " \
×
196
            "got: #{val.inspect}")
197
    end
198
  end
199

200
  def validate_time(param, val)
1✔
201
    if val.acts_like?(:time)
104✔
202
      val = val.utc
62✔
203
      format("%04d-%02d-%02d-%02d-%02d-%02d",
62✔
204
             val.year, val.mon, val.day, val.hour, val.min, val.sec)
205
    elsif /^\d\d\d\d(-\d\d?){0,5}$/i.match?(val.to_s)
42✔
206
      val
42✔
207
    elsif val.blank? || val.to_s == "0"
×
208
      nil
209
    else
210
      raise(
×
211
        "Value for :#{param} should be a UTC time (YYYY-MM-DD-HH-MM-SS), " \
212
        "got: #{val.class.name}::#{val.inspect}"
213
      )
214
    end
215
  end
216

217
  def validate_query(param, val)
1✔
218
    case val
45✔
219
    when Query::Base
220
      val.record.id
9✔
221
    when Integer
222
      val
36✔
223
    else
224
      raise(
×
225
        "Value for :#{param} should be a Query class, got: #{val.inspect}"
226
      )
227
    end
228
  end
229

230
  def find_cached_parameter_instance(model, param)
1✔
231
    val = if could_be_record_id?(param, params[param])
279✔
232
            model.find(params[param])
279✔
233
          else
NEW
234
            lookup_records_by_name(param, params[param], model)
×
235
          end
236
    set_cached_parameter_instance(param, val)
279✔
237
  end
238

239
  def get_cached_parameter_instance(param)
1✔
240
    @params_cache ||= {}
×
241
    @params_cache[param]
×
242
  end
243

244
  # Cache the instance for later use, in case we both instantiate and
245
  # execute query in the same action.
246
  def set_cached_parameter_instance(param, val)
1✔
247
    @params_cache ||= {}
614✔
248
    @params_cache[param] = val
614✔
249
  end
250

251
  def could_be_record_id?(param, val)
1✔
252
    val.is_a?(Integer) ||
1,314✔
253
      val.is_a?(String) && val.match(/^[1-9]\d*$/) ||
254
      # (blasted admin user has id = 0!)
255
      val.is_a?(String) && (val == "0") && (param == :user)
54✔
256
  end
257

258
  def validate_names_record(param, val, type)
1✔
259
    names_params = *names_parameter_declarations.except(:names).keys
627✔
260
    lookup_params = @params.slice(*names_params).compact
627✔
261
    if lookup_params.blank?
627✔
262
      validate_record(param, val, type)
149✔
263
    else
264
      lookup_records_by_name(param, val, type,
478✔
265
                             lookup_params:, method: :ids, all: true)
266
    end
267
  end
268

269
  def lookup_records_by_name(param, val, type, **args)
1✔
270
    lookup_params = args[:lookup_params] || {}
529✔
271
    method = args[:method] || :instances
529✔
272
    all = args[:all] || false
529✔
273
    lookup = lookup_class(param, val, type)
529✔
274

275
    results = lookup.new(val, lookup_params).send(method)
529✔
276
    raise("Couldn't find an id for : #{val.inspect}") unless results
529✔
277

278
    if !all || results.size == 1
529✔
279
      results.first
187✔
280
    else
281
      results
342✔
282
    end
283
  end
284

285
  def lookup_class(param, val, type)
1✔
286
    # We're only validating the projects passed as the param.
287
    # Projects' species_lists will be looked up later.
288
    lookup = if param == :project_lists
529✔
289
               Lookup::Projects
2✔
290
             else
291
               "Lookup::#{type.name.pluralize}".constantize
527✔
292
             end
293
    raise("#{lookup} not defined for : #{val.inspect}") unless defined?(lookup)
529✔
294

295
    lookup
529✔
296
  end
297
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc