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

MushroomObserver / mushroom-observer / 14168617228

31 Mar 2025 09:39AM UTC coverage: 93.978% (-0.007%) from 93.985%
14168617228

Pull #2858

github

web-flow
Merge f033413a6 into 5d3d18b2b
Pull Request #2858: Query validation: messages, not RuntimeErrors

157 of 173 new or added lines in 35 files covered. (90.75%)

15 existing lines in 2 files now uncovered.

27684 of 29458 relevant lines covered (93.98%)

597.89 hits per line

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

87.5
/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, :subqueries, :valid, :validation_errors
1✔
6

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

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

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

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

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

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

75
  def validate_hash_param(param, val, param_type)
1✔
76
    if [:string, :boolean].include?(param_type.keys.first)
1,536✔
77
      validate_enum(param, val, param_type)
199✔
78
    elsif param_type.keys.first == :subquery
1,337✔
79
      validate_subquery(param, val, param_type)
652✔
80
    else
81
      validate_nested_params(param, val, param_type)
685✔
82
    end
83
  end
84

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

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

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

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

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

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

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

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

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

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

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

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

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

221
  def validate_date(param, val)
1✔
222
    if val.acts_like?(:date)
65✔
223
      format("%04d-%02d-%02d", val.year, val.mon, val.day)
16✔
224
    elsif /^\d\d\d\d(-\d\d?){0,2}$/i.match?(val.to_s) ||
49✔
225
          /^\d\d?(-\d\d?)?$/i.match?(val.to_s)
226
      val
49✔
227
    elsif val.blank? || val.to_s == "0"
×
228
      nil
229
    else
NEW
UNCOV
230
      @validation_errors <<
×
231
        "Value for :#{param} should be a date (YYYY-MM-DD or MM-DD), " \
232
        "got: #{val}."
233
      nil
234
    end
235
  end
236

237
  def validate_time(param, val)
1✔
238
    if val.acts_like?(:time)
125✔
239
      val = val.utc
56✔
240
      format("%04d-%02d-%02d-%02d-%02d-%02d",
56✔
241
             val.year, val.mon, val.day, val.hour, val.min, val.sec)
242
    elsif /^\d\d\d\d(-\d\d?){0,5}$/i.match?(val.to_s)
69✔
243
      val
69✔
244
    elsif val.blank? || val.to_s == "0"
×
245
      nil
246
    else
NEW
247
      @validation_errors <<
×
248
        "Value for :#{param} should be a UTC time (YYYY-MM-DD-HH-MM-SS), " \
249
        "got: #{val.class.name}::#{val}."
250
      nil
251
    end
252
  end
253

254
  def find_cached_parameter_instance(model, param)
1✔
255
    return @params_cache[param] if @params_cache && @params_cache[param]
46✔
256

257
    val = params[param]
12✔
258
    instance = if could_be_record_id?(param, val)
12✔
259
                 model.find(val)
10✔
260
               elsif val.present?
2✔
261
                 lookup_record_by_name(param, val, model)
2✔
262
               end
263
    set_cached_parameter_instance(param, instance)
12✔
264
  end
265

266
  # Cache the instance for later use, in case we both instantiate and
267
  # execute query in the same action.
268
  def set_cached_parameter_instance(param, instance)
1✔
269
    @params_cache ||= {}
378✔
270
    @params_cache[param] = instance
378✔
271
  end
272

273
  def could_be_record_id?(param, val)
1✔
274
    val.is_a?(Integer) ||
1,903✔
275
      val.is_a?(String) && val.match(/^[1-9]\d*$/) ||
276
      # (blasted admin user has id = 0!)
277
      val.is_a?(String) && (val == "0") && (param == :user)
186✔
278
  end
279

280
  # Requires a unique identifying string and will return [only_one_record].
281
  def lookup_record_by_name(param, val, type, **args)
1✔
282
    method = args[:method] || :instances
2✔
283
    lookup = lookup_class(param, val, type)
2✔
284

285
    results = lookup.new(val).send(method)
2✔
286
    unless results
2✔
NEW
UNCOV
287
      @validation_errors << "Couldn't find an id for : #{val.inspect}."
×
288
    end
289

290
    results.first
2✔
291
  end
292

293
  def lookup_class(param, val, type)
1✔
294
    # We're only validating the projects passed as the param.
295
    # Projects' species_lists will be looked up later.
296
    lookup = if param == :project_lists
2✔
UNCOV
297
               Lookup::Projects
×
298
             else
299
               "Lookup::#{type.name.pluralize}".constantize
2✔
300
             end
301
    unless defined?(lookup)
2✔
NEW
UNCOV
302
      @validation_errors << "#{lookup} not defined for : #{val.inspect}."
×
303
    end
304
    lookup
2✔
305
  end
306
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