Class: ForestLiana::FiltersParser

Inherits:
Object
  • Object
show all
Defined in:
app/services/forest_liana/filters_parser.rb

Constant Summary collapse

AGGREGATOR_OPERATOR =
%w(and or)

Instance Method Summary collapse

Constructor Details

#initialize(filters, resource, timezone) ⇒ FiltersParser

Returns a new instance of FiltersParser.


5
6
7
8
9
10
11
12
13
14
15
# File 'app/services/forest_liana/filters_parser.rb', line 5

def initialize(filters, resource, timezone)
  begin
    @filters = JSON.parse(filters)
  rescue JSON::ParserError
    raise ForestLiana::Errors::HTTP422Error.new('Invalid filters JSON format')
  end

  @resource = resource
  @operator_date_parser = OperatorDateIntervalParser.new(timezone)
  @joins = []
end

Instance Method Details

#apply_filtersObject


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'app/services/forest_liana/filters_parser.rb', line 17

def apply_filters
  return @resource unless @filters

  where = parse_aggregation(@filters)
  return @resource unless where

  @joins.each do |join|
    current_resource = @resource.reflect_on_association(join.name).klass
    current_resource.include(ArelHelpers::Aliases)
    current_resource.aliased_as(join.name) do |aliased_resource|
      @resource = @resource.joins(ArelHelpers.join_association(@resource, join.name, Arel::Nodes::OuterJoin, aliases: [aliased_resource]))
    end
  end

  @resource.where(where)
end

#apply_filters_on_previous_interval(previous_condition) ⇒ Object


239
240
241
242
243
244
# File 'app/services/forest_liana/filters_parser.rb', line 239

def apply_filters_on_previous_interval(previous_condition)
  # Ressource should have already been joined
  where = parse_aggregation_on_previous_interval(@filters, previous_condition)

  @resource.where(where)
end

#ensure_valid_aggregation(node) ⇒ Object

Raises:

  • (ForestLiana::Errors::HTTP422Error)

284
285
286
287
# File 'app/services/forest_liana/filters_parser.rb', line 284

def ensure_valid_aggregation(node)
  raise ForestLiana::Errors::HTTP422Error.new('Filters cannot be a raw value') unless node.is_a?(Hash)
  raise_empty_condition_in_filter_error if node.empty?
end

#ensure_valid_condition(condition) ⇒ Object

Raises:

  • (ForestLiana::Errors::HTTP422Error)

289
290
291
292
293
294
295
# File 'app/services/forest_liana/filters_parser.rb', line 289

def ensure_valid_condition(condition)
  raise_empty_condition_in_filter_error if condition.empty?
  raise ForestLiana::Errors::HTTP422Error.new('Condition cannot be a raw value') unless condition.is_a?(Hash)
  unless condition['field'].is_a?(String) and condition['operator'].is_a?(String)
    raise ForestLiana::Errors::HTTP422Error.new('Invalid condition format')
  end
end

#get_association_field_and_resource(field_name) ⇒ Object


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'app/services/forest_liana/filters_parser.rb', line 70

def get_association_field_and_resource(field_name)
  if is_belongs_to(field_name)
    association = field_name.partition(':').first.to_sym
    association_field = field_name.partition(':').last

    unless @resource.reflect_on_association(association)
      raise ForestLiana::Errors::HTTP422Error.new("Association '#{association}' not found")
    end

    current_resource = @resource.reflect_on_association(association).klass

    return association_field, current_resource
  else
    return field_name, @resource
  end
end

#get_association_name_for_condition(field) ⇒ Object


199
200
201
202
203
204
205
206
207
208
# File 'app/services/forest_liana/filters_parser.rb', line 199

def get_association_name_for_condition(field)
  field, subfield = field.split(':')

  association = @resource.reflect_on_association(field.to_sym)
  return nil if association.blank?

  @joins << association unless @joins.include? association

  association.name
end

#get_previous_interval_conditionObject

NOTICE: Look for a previous interval condition matching the following:

- If the filter is a simple condition at the root the check is done right away.
- There can't be a previous interval condition if the aggregator is 'or' (no meaning).
- The condition's operator has to be elligible for a previous interval.
- There can't be two previous interval condition.

215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/services/forest_liana/filters_parser.rb', line 215

def get_previous_interval_condition
  current_previous_interval = nil
  # NOTICE: Leaf condition at root
  unless @filters['aggregator']
    return @filters if @operator_date_parser.has_previous_interval?(@filters['operator'])
  end

  if @filters['aggregator'] === 'and'
    @filters['conditions'].each do |condition|
      # NOTICE: Nested conditions
      return nil if condition['aggregator']

      if @operator_date_parser.has_previous_interval?(condition['operator'])
        # NOTICE: There can't be two previous_interval.
        return nil if current_previous_interval

        current_previous_interval = condition
      end
    end
  end

  current_previous_interval
end

#is_belongs_to(field) ⇒ Object


195
196
197
# File 'app/services/forest_liana/filters_parser.rb', line 195

def is_belongs_to(field)
  field.include?(':')
end

#parse_aggregation(node) ⇒ Object


34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/services/forest_liana/filters_parser.rb', line 34

def parse_aggregation(node)
  ensure_valid_aggregation(node)

  return parse_condition(node) unless node['aggregator']

  conditions = []
  node['conditions'].each do |condition|
    conditions.push(parse_aggregation(condition))
  end

  operator = parse_aggregation_operator(node['aggregator'])

  conditions.empty? ? nil : "(#{conditions.join(" #{operator} ")})"
end

#parse_aggregation_on_previous_interval(node, previous_condition) ⇒ Object


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'app/services/forest_liana/filters_parser.rb', line 246

def parse_aggregation_on_previous_interval(node, previous_condition)
  raise_empty_condition_in_filter_error unless node

  return parse_previous_interval_condition(node) unless node['aggregator']

  conditions = []
  node['conditions'].each do |condition|
    if condition == previous_condition
      conditions.push(parse_previous_interval_condition(condition))
    else
      conditions.push(parse_aggregation(condition))
    end
  end

  operator = parse_aggregation_operator(node['aggregator'])

  conditions.empty? ? nil : "(#{conditions.join(" #{operator} ")})"
end

#parse_aggregation_operator(aggregator_operator) ⇒ Object


115
116
117
118
119
120
121
# File 'app/services/forest_liana/filters_parser.rb', line 115

def parse_aggregation_operator(aggregator_operator)
  unless AGGREGATOR_OPERATOR.include?(aggregator_operator)
    raise_unknown_operator_error(aggregator_operator)
  end

  aggregator_operator.upcase
end

#parse_condition(condition) ⇒ Object


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'app/services/forest_liana/filters_parser.rb', line 49

def parse_condition(condition)
  where = parse_condition_without_smart_field(condition)

  field_name = condition['field']

  if ForestLiana::SchemaHelper.is_smart_field?(@resource, field_name)
    schema = ForestLiana.schema_for_resource(@resource)
    field_schema = schema.fields.find do |field|
      field[:field].to_s == field_name
    end

    unless field_schema.try(:[], :filter)
      raise ForestLiana::Errors::NotImplementedMethodError.new("method filter on smart field '#{field_name}' not found")
    end

    return field_schema[:filter].call(condition, where)
  end

  where
end

#parse_condition_without_smart_field(condition) ⇒ Object


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'app/services/forest_liana/filters_parser.rb', line 87

def parse_condition_without_smart_field(condition)
  ensure_valid_condition(condition)

  operator = condition['operator']
  value = condition['value']
  field_name = condition['field']

  if @operator_date_parser.is_date_operator?(operator)
    condition = @operator_date_parser.get_date_filter(operator, value)
    return "#{parse_field_name(field_name)} #{condition}"
  end

  association_field, current_resource = get_association_field_and_resource(field_name)

  # NOTICE: Set the integer value instead of a string if "enum" type
  # NOTICE: Rails 3 do not have a defined_enums method
  if current_resource.respond_to?(:defined_enums) && current_resource.defined_enums.has_key?(association_field)
    value = current_resource.defined_enums[association_field][value]
  end

  parsed_field = parse_field_name(field_name)
  parsed_operator = parse_operator(operator)
  parsed_value = parse_value(operator, value)
  field_and_operator = "#{parsed_field} #{parsed_operator}"

  sanitize_condition(field_and_operator, operator, parsed_value)
end

#parse_field_name(field) ⇒ Object


172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'app/services/forest_liana/filters_parser.rb', line 172

def parse_field_name(field)
  if is_belongs_to(field)
    current_resource = @resource.reflect_on_association(field.split(':').first.to_sym)&.klass
    raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found") unless current_resource

    association = get_association_name_for_condition(field)
    quoted_table_name = ActiveRecord::Base.connection.quote_column_name(association)
    field_name = field.split(':')[1]
  else
    quoted_table_name = @resource.quoted_table_name
    current_resource = @resource
    field_name = field
  end
  quoted_field_name = ActiveRecord::Base.connection.quote_column_name(field_name)

  column_found = current_resource.columns.find { |column| column.name == field.split(':').last }
  if column_found.nil? && !ForestLiana::SchemaHelper.is_smart_field?(current_resource, field_name)
    raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found")
  end

  "#{quoted_table_name}.#{quoted_field_name}"
end

#parse_operator(operator) ⇒ Object


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'app/services/forest_liana/filters_parser.rb', line 123

def parse_operator(operator)
  case operator
  when 'not'
    'NOT'
  when 'greater_than', 'after'
    '>'
  when 'less_than', 'before'
    '<'
  when 'contains', 'starts_with', 'ends_with'
    'LIKE'
  when 'not_contains'
    'NOT LIKE'
  when 'not_equal'
    '!='
  when 'equal'
    '='
  when 'blank'
    'IS'
  when 'present'
    'IS NOT'
  when 'in'
    'IN'
  else
    raise_unknown_operator_error(operator)
  end
end

#parse_previous_interval_condition(condition) ⇒ Object


265
266
267
268
269
270
271
272
273
274
# File 'app/services/forest_liana/filters_parser.rb', line 265

def parse_previous_interval_condition(condition)
  raise_empty_condition_in_filter_error unless condition

  parsed_condition = @operator_date_parser.get_date_filter_for_previous_interval(
    condition['operator'],
    condition['value']
  )

  "#{parse_field_name(condition['field'])} #{parsed_condition}"
end

#parse_value(operator, value) ⇒ Object


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'app/services/forest_liana/filters_parser.rb', line 150

def parse_value(operator, value)
  case operator
  when 'not', 'greater_than', 'less_than', 'not_equal', 'equal', 'before', 'after'
    value
  when 'contains', 'not_contains'
    "%#{value}%"
  when 'starts_with'
    "#{value}%"
  when 'ends_with'
    "%#{value}"
  when 'in'
    if value.kind_of?(String)
      value.split(',').map { |val| val.strip() }
    else
      value
    end
  when 'present', 'blank'
  else
    raise_unknown_operator_error(operator)
  end
end

#raise_empty_condition_in_filter_errorObject

Raises:

  • (ForestLiana::Errors::HTTP422Error)

280
281
282
# File 'app/services/forest_liana/filters_parser.rb', line 280

def raise_empty_condition_in_filter_error
  raise ForestLiana::Errors::HTTP422Error.new('Empty condition in filter')
end

#raise_unknown_operator_error(operator) ⇒ Object

Raises:

  • (ForestLiana::Errors::HTTP422Error)

276
277
278
# File 'app/services/forest_liana/filters_parser.rb', line 276

def raise_unknown_operator_error(operator)
  raise ForestLiana::Errors::HTTP422Error.new("Unknown provided operator '#{operator}'")
end