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

MushroomObserver / mushroom-observer / 14217149209

02 Apr 2025 10:14AM UTC coverage: 93.77% (-0.04%) from 93.809%
14217149209

Pull #2856

github

nimmolo
Merge branch 'main' into query-scopes-am
Pull Request #2856: Query AR - final round PR branch

154 of 162 new or added lines in 29 files covered. (95.06%)

19 existing lines in 4 files now uncovered.

26492 of 28252 relevant lines covered (93.77%)

613.74 hits per line

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

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

3
# Validation of Query parameters, plus substitution of ids for instances.
4
module Query::Modules::Validation # rubocop:disable Metrics/ModuleLength
1✔
5
  attr_accessor :params, :params_cache, :subqueries, :valid, :validation_errors
1✔
6

7
  def clean_and_validate_params
1✔
8
    @validation_errors = []
4,455✔
9
    old_params = @params.dup&.deep_compact&.deep_symbolize_keys || {}
4,455✔
10
    new_params = {}
4,455✔
11
    permitted_params = parameter_declarations.slice(*old_params.keys)
4,455✔
12
    permitted_params.each do |param, param_type|
4,455✔
13
      val = old_params[param]
5,700✔
14
      val = validate_value(param_type, param, val) if val.present?
5,700✔
15
      new_params[param] = val
5,699✔
16
    end
17
    @params = new_params
4,454✔
18
    assign_attributes(**@params) if @params.present?
4,454✔
19
  end
20

21
  def validate_value(param_type, param, val)
1✔
22
    if param_type.is_a?(Array)
9,796✔
23
      result = array_validate(param, val, param_type.first).flatten
2,159✔
24
      result = result.uniq if positive_integers?(result)
2,159✔
25
      result
2,159✔
26
    else
27
      val = scalar_validate(param, val, param_type)
7,637✔
28
      [val].flatten.first
7,636✔
29
    end
30
  end
31

32
  def positive_integers?(list)
1✔
33
    list.all? { |item| item.is_a?(Integer) && item.positive? }
4,510✔
34
  end
35

36
  def array_validate(param, val, param_type)
1✔
37
    case val
2,159✔
38
    when Array
39
      val[0, MO.query_max_array].map! do |val2|
976✔
40
        scalar_validate(param, val2, param_type)
1,283✔
41
      end
42
    when ::API2::OrderedRange
43
      [scalar_validate(param, val.begin, param_type),
39✔
44
       scalar_validate(param, val.end, param_type)]
45
    else
46
      [scalar_validate(param, val, param_type)]
1,144✔
47
    end
48
  end
49

50
  def scalar_validate(param, val, param_type)
1✔
51
    case param_type
10,344✔
52
    when Symbol
53
      send(:"validate_#{param_type}", param, val)
6,563✔
54
    when Class
55
      validate_class_param(param, val, param_type)
2,250✔
56
    when Hash
57
      validate_hash_param(param, val, param_type)
1,531✔
58
    else
59
      @validation_errors <<
×
60
        "Invalid declaration of :#{param} for #{model} " \
61
        "query! (invalid type: #{param_type.class.name})"
62
      nil
63
    end
64
  end
65

66
  def validate_class_param(param, val, param_type)
1✔
67
    if param_type.respond_to?(:descends_from_active_record?)
2,250✔
68
      validate_record(param, val, param_type)
2,250✔
69
    else
70
      @validation_errors <<
×
71
        "Don't know how to parse #{param_type} :#{param} for #{model} query."
72
      nil
73
    end
74
  end
75

76
  def validate_hash_param(param, val, param_type)
1✔
77
    if [:string, :boolean].include?(param_type.keys.first)
1,531✔
78
      validate_enum(param, val, param_type)
202✔
79
    elsif param_type.keys.first == :subquery
1,329✔
80
      validate_subquery(param, val, param_type)
648✔
81
    else
82
      validate_nested_params(param, val, param_type)
681✔
83
    end
84
  end
85

86
  # For results, don't compact_blank, because sometimes we want `false`
87
  def validate_nested_params(_param, val, param_type)
1✔
88
    val2 = {}
681✔
89
    param_type.each do |key, arg_type|
681✔
90
      val2[key] = validate_value(arg_type, key, val[key])
4,459✔
91
    end
92
    val2.compact
680✔
93
  end
94

95
  # Validate the subquery's params by creating another Query instance
96
  # and save it in @subqueries to facilitate access
97
  def validate_subquery(param, val, param_type)
1✔
98
    if param_type.keys.length != 1
648✔
99
      @validation_errors <<
×
100
        "Invalid subquery declaration for :#{param} for #{model} " \
101
        "query! (wrong number of keys in hash)"
102
      return nil
×
103
    end
104
    submodel = param_type.values.first
648✔
105
    subquery = Query.new(submodel, val)
648✔
106
    @subqueries[param] = subquery
648✔
107
    subquery.params
648✔
108
  end
109

110
  def validate_enum(param, val, hash)
1✔
111
    if hash.keys.length != 1
202✔
112
      @validation_errors <<
×
113
        "Invalid enum declaration for :#{param} for #{model} " \
114
        "query! (wrong number of keys in hash)"
115
      return nil
×
116
    end
117

118
    arg_type = hash.keys.first
202✔
119
    set = hash.values.first
202✔
120
    unless set.is_a?(Array)
202✔
121
      @validation_errors <<
×
122
        "Invalid enum declaration for :#{param} for #{model} " \
123
        "query! (expected value to be an array of allowed values)"
124
      return nil
×
125
    end
126

127
    val2 = scalar_validate(param, val, arg_type)
202✔
128
    if (arg_type == :string) && set.include?(val2.to_s.to_sym)
202✔
129
      val2 = val2.to_s.to_sym
89✔
130
    elsif set.exclude?(val2)
113✔
131
      @validation_errors <<
3✔
132
        "Value for :#{param} should be one of the following: #{set.inspect}."
133
      val2 = nil
3✔
134
    end
135
    val2
202✔
136
  end
137

138
  # Disable cop because we do mean to symbols with boolean names
139
  # rubocop:disable Lint/BooleanSymbol
140
  def validate_boolean(param, val)
1✔
141
    case val
3,933✔
142
    when :true, :yes, :on, "true", "yes", "on", "1", 1, true
143
      true
1,186✔
144
    when :false, :no, :off, "false", "no", "off", "0", 0, false
145
      false
32✔
146
    when nil
147
      nil
148
    else
149
      @validation_errors <<
1✔
150
        "Value for :#{param} should be boolean, got: #{val}."
151
      nil
152
    end
153
  end
154
  # rubocop:enable Lint/BooleanSymbol
155

156
  # def validate_integer(param, val)
157
  #   if val.is_a?(Integer) || val.is_a?(String) && val.match(/^-?\d+$/)
158
  #     val.to_i
159
  #   elsif val.blank?
160
  #     nil
161
  #   else
162
  #     @validation_errors <<
163
  #       "Value for :#{param} should be an integer, got: #{val.inspect}")
164
  #   end
165
  # end
166

167
  def validate_float(param, val)
1✔
168
    if val.is_a?(Integer) || val.is_a?(Float) ||
199✔
169
       (val.is_a?(String) && val.match(/^-?(\d+(\.\d+)?|\.\d+)$/))
12✔
170
      val.to_f
197✔
171
    else
172
      @validation_errors <<
2✔
173
        "Value for :#{param} should be a float, got: #{val.inspect}."
174
      nil
175
    end
176
  end
177

178
  # This type of param accepts instances, ids, or strings. When the query is
179
  # executed, the string will be sent to the appropriate `Lookup` subclass.
180
  def validate_record(param, val, type = ActiveRecord::Base)
1✔
181
    if val.is_a?(type)
2,250✔
182
      unless val.id
367✔
183
        @validation_errors <<
1✔
184
          "Value for :#{param} is an unsaved #{type} instance."
185
        return nil
1✔
186
      end
187

188
      set_cached_parameter_instance(param, val)
366✔
189
      val.id
366✔
190
    elsif could_be_record_id?(param, val)
1,883✔
191
      val.to_i
1,692✔
192
    elsif val.is_a?(String)
191✔
193
      validate_string_for_record(param, val, type)
187✔
194
    else
195
      @validation_errors <<
4✔
196
        "Value for :#{param} should be id, string " \
197
        "or #{type} instance, got: #{val.inspect}."
198
      nil
199
    end
200
  end
201

202
  def validate_string_for_record(param, val, type)
1✔
203
    return val unless param == :id_in_set
187✔
204

205
    @validation_errors <<
4✔
206
      "Value for :id_in_set should be an array of ids " \
207
      "or #{type} instances, got: '#{val}'."
208
    nil
209
  end
210

211
  def validate_string(param, val)
1✔
212
    if val.is_any?(Integer, Float, String, Symbol)
2,235✔
213
      val.to_s
2,229✔
214
    else
215
      @validation_errors <<
6✔
216
        "Value for :#{param} should be a string or symbol, " \
217
        "got a #{val.class}: #{val.inspect}."
218
      nil
219
    end
220
  end
221

222
  def validate_date(param, val)
1✔
223
    if val.blank? || val.to_s == "0"
69✔
224
      nil
225
    elsif val.acts_like?(:date)
68✔
226
      format_date(val)
16✔
227
    elsif /^\d\d\d\d(-\d\d?){0,2}$/i.match?(val.to_s) ||
52✔
228
          /^\d\d?(-\d\d?)?$/i.match?(val.to_s)
229
      val
49✔
230
    elsif (val2 = parse_date(val)).acts_like?(:date)
3✔
231
      format_date(val2)
1✔
232
    else
233
      @validation_errors <<
2✔
234
        "Value for :#{param} should be a date (YYYY-MM-DD or MM-DD), " \
235
        "got: #{val}."
236
      nil
237
    end
238
  end
239

240
  def parse_date(val)
1✔
241
    Date.parse(val)
3✔
242
  rescue Date::Error
243
    nil
2✔
244
  end
245

246
  def format_date(val)
1✔
247
    format("%04d-%02d-%02d", val.year, val.mon, val.day)
17✔
248
  end
249

250
  def validate_time(param, val)
1✔
251
    if val.blank? || val.to_s == "0"
127✔
252
      nil
253
    elsif val.acts_like?(:time)
126✔
254
      format_time(val)
56✔
255
    elsif /^\d\d\d\d(-\d\d?){0,5}$/i.match?(val.to_s)
70✔
256
      val
69✔
257
    elsif (val2 = parse_time(val)).acts_like?(:time)
1✔
258
      format_time(val2)
1✔
259
    else
260
      @validation_errors <<
×
261
        "Value for :#{param} should be a UTC time (YYYY-MM-DD-HH-MM-SS), " \
262
        "got: #{val.class.name}::#{val}."
263
      nil
264
    end
265
  end
266

267
  def parse_time(val)
1✔
268
    DateTime.parse(val)
1✔
269
  rescue ArgumentError
270
    nil
×
271
  end
272

273
  def format_time(val)
1✔
274
    format("%04d-%02d-%02d-%02d-%02d-%02d",
57✔
275
           val.year, val.mon, val.day, val.hour, val.min, val.sec)
276
  end
277

278
  def find_cached_parameter_instance(model, param)
1✔
UNCOV
279
    return @params_cache[param] if @params_cache && @params_cache[param]
×
280

UNCOV
281
    val = params[param]
×
UNCOV
282
    instance = if could_be_record_id?(param, val)
×
UNCOV
283
                 model.find(val)
×
UNCOV
284
               elsif val.present?
×
UNCOV
285
                 lookup_record_by_name(param, val, model)
×
286
               end
UNCOV
287
    set_cached_parameter_instance(param, instance)
×
288
  end
289

290
  # Cache the instance for later use, in case we both instantiate and
291
  # execute query in the same action.
292
  def set_cached_parameter_instance(param, instance)
1✔
293
    @params_cache ||= {}
366✔
294
    @params_cache[param] = instance
366✔
295
  end
296

297
  def could_be_record_id?(param, val)
1✔
298
    val.is_a?(Integer) ||
1,883✔
299
      val.is_a?(String) && val.match(/^[1-9]\d*$/) ||
300
      # (blasted admin user has id = 0!)
301
      val.is_a?(String) && (val == "0") && (param == :user)
187✔
302
  end
303

304
  # Requires a unique identifying string and will return [only_one_record].
305
  def lookup_record_by_name(param, val, type, **args)
1✔
UNCOV
306
    method = args[:method] || :instances
×
UNCOV
307
    lookup = lookup_class(param, val, type)
×
308

UNCOV
309
    results = lookup.new(val).send(method)
×
UNCOV
310
    unless results
×
311
      @validation_errors << "Couldn't find an id for : #{val.inspect}."
×
312
    end
313

UNCOV
314
    results.first
×
315
  end
316

317
  def lookup_class(param, val, type)
1✔
318
    # We're only validating the projects passed as the param.
319
    # Projects' species_lists will be looked up later.
UNCOV
320
    lookup = if param == :project_lists
×
321
               Lookup::Projects
×
322
             else
UNCOV
323
               "Lookup::#{type.name.pluralize}".constantize
×
324
             end
UNCOV
325
    unless defined?(lookup)
×
326
      @validation_errors << "#{lookup} not defined for : #{val.inspect}."
×
327
    end
UNCOV
328
    lookup
×
329
  end
330
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc