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

MushroomObserver / mushroom-observer / 14145379140

29 Mar 2025 11:58AM UTC coverage: 84.27% (-9.7%) from 93.985%
14145379140

Pull #2808

github

nimmolo
Handle rss_log content filters with subqueries
Pull Request #2808: Convert Query to AR scopes

477 of 2306 new or added lines in 78 files covered. (20.69%)

1381 existing lines in 27 files now uncovered.

26562 of 31520 relevant lines covered (84.27%)

536.24 hits per line

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

78.95
/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
1✔
6

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

20
  # def check_for_unexpected_params(old_params)
21
  #   unexpected_params = old_params.except(*parameter_declarations.keys)
22
  #   return if unexpected_params.keys.empty?
23

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

28
  def validate_value(param_type, param, val)
1✔
29
    if param_type.is_a?(Array)
9,751✔
30
      result = array_validate(param, val, param_type.first).flatten
2,143✔
31
      result = result.uniq if positive_integers?(result)
2,138✔
32
      result
2,138✔
33
    else
34
      val = scalar_validate(param, val, param_type)
7,608✔
35
      [val].flatten.first
7,595✔
36
    end
37
  end
38

39
  def positive_integers?(list)
1✔
40
    list.all? { |item| item.is_a?(Integer) && item.positive? }
4,468✔
41
  end
42

43
  def array_validate(param, val, param_type)
1✔
44
    case val
2,143✔
45
    when Array
46
      val[0, MO.query_max_array].map! do |val2|
975✔
47
        scalar_validate(param, val2, param_type)
1,282✔
48
      end
49
    when ::API2::OrderedRange
50
      [scalar_validate(param, val.begin, param_type),
39✔
51
       scalar_validate(param, val.end, param_type)]
52
    else
53
      [scalar_validate(param, val, param_type)]
1,129✔
54
    end
55
  end
56

57
  def scalar_validate(param, val, param_type)
1✔
58
    case param_type
10,296✔
59
    when Symbol
60
      send(:"validate_#{param_type}", param, val)
6,527✔
61
    when Class
62
      validate_class_param(param, val, param_type)
2,243✔
63
    when Hash
64
      validate_hash_param(param, val, param_type)
1,526✔
65
    else
66
      raise("Invalid declaration of :#{param} for #{model} " \
×
67
            "query! (invalid type: #{param_type.class.name})")
68
    end
69
  end
70

71
  def validate_class_param(param, val, param_type)
1✔
72
    if param_type.respond_to?(:descends_from_active_record?)
2,243✔
73
      validate_record(param, val, param_type)
2,243✔
74
    else
75
      raise(
×
76
        "Don't know how to parse #{param_type} :#{param} for #{model} query."
77
      )
78
    end
79
  end
80

81
  def validate_hash_param(param, val, param_type)
1✔
82
    if [:string, :boolean].include?(param_type.keys.first)
1,526✔
83
      validate_enum(param, val, param_type)
199✔
84
    elsif param_type.keys.first == :subquery
1,327✔
85
      validate_subquery(param, val, param_type)
648✔
86
    else
87
      validate_nested_params(param, val, param_type)
679✔
88
    end
89
  end
90

91
  # For results, don't compact_blank, because sometimes we want `false`
92
  def validate_nested_params(_param, val, param_type)
1✔
93
    val2 = {}
679✔
94
    param_type.each do |key, arg_type|
679✔
95
      val2[key] = validate_value(arg_type, key, val[key])
4,445✔
96
    end
97
    val2.compact
676✔
98
  end
99

100
  # Validate the subquery's params by creating another Query instance
101
  # and save it in @subqueries to facilitate access
102
  def validate_subquery(param, val, param_type)
1✔
103
    unless param_type.key?(:subquery)
648✔
NEW
104
      str = "Invalid subquery declaration for :#{param} for #{model} query."
×
NEW
105
      raise(str)
×
106
    end
107
    submodel = param_type.values.first
648✔
108
    subquery = Query.new(submodel, val)
648✔
109
    @subqueries[param] = subquery
648✔
110
    subquery.params
648✔
111
  end
112

113
  def validate_enum(param, val, hash)
1✔
114
    if hash.keys.length != 1
199✔
115
      raise(
×
116
        "Invalid enum declaration for :#{param} for #{model} " \
117
        "query! (wrong number of keys in hash)"
118
      )
119
    end
120

121
    arg_type = hash.keys.first
199✔
122
    set = hash.values.first
199✔
123
    unless set.is_a?(Array)
199✔
124
      raise(
×
125
        "Invalid enum declaration for :#{param} for #{model} " \
126
        "query! (expected value to be an array of allowed values)"
127
      )
128
    end
129

130
    val2 = scalar_validate(param, val, arg_type)
199✔
131
    if (arg_type == :string) && set.include?(val2.to_sym)
198✔
132
      val2 = val2.to_sym
89✔
133
    elsif set.exclude?(val2)
109✔
134
      raise("Value for :#{param} should be one of the following: " \
2✔
135
            "#{set.inspect}.")
136
    end
137
    val2
196✔
138
  end
139

140
  # Disable cop because we do mean to symbols with boolean names
141
  # rubocop:disable Lint/BooleanSymbol
142
  def validate_boolean(param, val)
1✔
143
    case val
3,925✔
144
    when :true, :yes, :on, "true", "yes", "on", "1", 1, true
145
      true
1,180✔
146
    when :false, :no, :off, "false", "no", "off", "0", 0, false
147
      false
31✔
148
    when nil
149
      nil
150
    else
151
      raise("Value for :#{param} should be boolean, got: #{val.inspect}")
×
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
  #     raise("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) ||
184✔
168
       (val.is_a?(String) && val.match(/^-?(\d+(\.\d+)?|\.\d+)$/))
7✔
169
      val.to_f
182✔
170
    else
171
      raise("Value for :#{param} should be a float, got: #{val.inspect}")
2✔
172
    end
173
  end
174

175
  # This type of param accepts instances, ids, or strings. When the query is
176
  # executed, the string will be sent to the appropriate `Lookup` subclass.
177
  def validate_record(param, val, type = ActiveRecord::Base)
1✔
178
    if val.is_a?(type)
2,243✔
179
      raise("Value for :#{param} is an unsaved #{type} instance.") unless val.id
366✔
180

181
      set_cached_parameter_instance(param, val)
366✔
182
      val.id
366✔
183
    elsif could_be_record_id?(param, val)
1,877✔
184
      val.to_i
1,692✔
185
    elsif val.is_a?(String) && param != :id_in_set
185✔
186
      val
180✔
187
    else
188
      raise("Value for :#{param} should be id, string " \
5✔
189
            "or #{type} instance, got: #{val.inspect}")
190
    end
191
  end
192

193
  def validate_string(param, val)
1✔
194
    if val.is_any?(Integer, Float, String, Symbol)
2,228✔
195
      val.to_s
2,222✔
196
    else
197
      raise("Value for :#{param} should be a string or symbol, " \
6✔
198
            "got a #{val.class}: #{val.inspect}")
199
    end
200
  end
201

202
  def validate_date(param, val)
1✔
203
    if val.acts_like?(:date)
65✔
204
      format("%04d-%02d-%02d", val.year, val.mon, val.day)
16✔
205
    elsif /^\d\d\d\d(-\d\d?){0,2}$/i.match?(val.to_s) ||
49✔
206
          /^\d\d?(-\d\d?)?$/i.match?(val.to_s)
207
      val
49✔
208
    elsif val.blank? || val.to_s == "0"
×
209
      nil
210
    else
211
      raise("Value for :#{param} should be a date (YYYY-MM-DD or MM-DD), " \
×
212
            "got: #{val.inspect}")
213
    end
214
  end
215

216
  def validate_time(param, val)
1✔
217
    if val.acts_like?(:time)
125✔
218
      val = val.utc
56✔
219
      format("%04d-%02d-%02d-%02d-%02d-%02d",
56✔
220
             val.year, val.mon, val.day, val.hour, val.min, val.sec)
221
    elsif /^\d\d\d\d(-\d\d?){0,5}$/i.match?(val.to_s)
69✔
222
      val
69✔
223
    elsif val.blank? || val.to_s == "0"
×
224
      nil
225
    else
226
      raise(
×
227
        "Value for :#{param} should be a UTC time (YYYY-MM-DD-HH-MM-SS), " \
228
        "got: #{val.class.name}::#{val.inspect}"
229
      )
230
    end
231
  end
232

233
  # def validate_query(param, val)
234
  #   case val
235
  #   when Query::Base
236
  #     val.record.id
237
  #   when Integer
238
  #     val
239
  #   else
240
  #     raise(
241
  #       "Value for :#{param} should be a Query class, got: #{val.inspect}"
242
  #     )
243
  #   end
244
  # end
245

246
  def find_cached_parameter_instance(model, param)
1✔
UNCOV
247
    return @params_cache[param] if @params_cache && @params_cache[param]
×
248

UNCOV
249
    val = params[param]
×
UNCOV
250
    instance = if could_be_record_id?(param, val)
×
UNCOV
251
                 model.find(val)
×
UNCOV
252
               elsif val.present?
×
UNCOV
253
                 lookup_record_by_name(param, val, model)
×
254
               end
UNCOV
255
    set_cached_parameter_instance(param, instance)
×
256
  end
257

258
  # Cache the instance for later use, in case we both instantiate and
259
  # execute query in the same action.
260
  def set_cached_parameter_instance(param, instance)
1✔
261
    @params_cache ||= {}
366✔
262
    @params_cache[param] = instance
366✔
263
  end
264

265
  def could_be_record_id?(param, val)
1✔
266
    val.is_a?(Integer) ||
1,877✔
267
      val.is_a?(String) && val.match(/^[1-9]\d*$/) ||
268
      # (blasted admin user has id = 0!)
269
      val.is_a?(String) && (val == "0") && (param == :user)
183✔
270
  end
271

272
  # Requires a unique identifying string and will return [only_one_record].
273
  def lookup_record_by_name(param, val, type, **args)
1✔
UNCOV
274
    method = args[:method] || :instances
×
UNCOV
275
    lookup = lookup_class(param, val, type)
×
276

UNCOV
277
    results = lookup.new(val).send(method)
×
UNCOV
278
    raise("Couldn't find an id for : #{val.inspect}") unless results
×
279

UNCOV
280
    results.first
×
281
  end
282

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

UNCOV
293
    lookup
×
294
  end
295
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