ManageIQ/manageiq-api

View on GitHub
lib/api/filter.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
98%
module Api
  class Filter
    OPERATOR_REGEXP = /^ *([^=!>< ]+) *(!==?|[=!]~|[<>=]=?) *(.*?) *$/.freeze
    OPERATORS = {
      "!="  => {:default => "!=", :regex => "REGULAR EXPRESSION DOES NOT MATCH", :null => "IS NOT NULL"},
      "<="  => {:default => "<="},
      ">="  => {:default => ">="},
      "<"   => {:default => "<", :datetime => "BEFORE"},
      ">"   => {:default => ">", :datetime => "AFTER"},
      "="   => {:default => "=", :datetime => "IS", :regex => "REGULAR EXPRESSION MATCHES", :string_set => "includes all", :array => "=", :null => "IS NULL"},

      # string-only matching, use quotes
      "=="  => {:default => "="},
      "!==" => {:default => "!="},

      # regex-only matching without mangling, use slashes and optionally /i
      "=~"  => {:default => "REGULAR EXPRESSION MATCHES"},
      "!~"  => {:default => "REGULAR EXPRESSION DOES NOT MATCH"},
    }.freeze

    attr_reader :filters, :model, :and_expressions, :or_expressions

    def self.parse(filters, model)
      new(filters, model).parse
    end

    def initialize(filters, model)
      @filters         = filters
      @model           = model
      @and_expressions = []
      @or_expressions  = []
    end

    def parse
      filters.select(&:present?).each do |filter|
        parsed_filter = parse_filter(filter)
        field = parsed_filter[:field]
        if field.associations.size > 1
          raise BadRequestError, "Filtering of attributes with more than one association away is not supported"
        end
        unless field.valid?
          raise BadRequestError, "attribute #{field} does not exist"
        end

        op    = parsed_filter[:operator]
        expr  = if parsed_filter[:value].kind_of?(Array)
                  {"OR" => parsed_filter[:value].map { |val| single_expression(field, op, val) }}
                else
                  single_expression(field, op, parsed_filter[:value])
                end

        if parsed_filter[:logical_or]
          or_expressions << expr
        else
          and_expressions << expr
        end
      end

      MiqExpression.new(composite_expression).tap do |expression|
        raise BadRequestError, "Must filter on valid attributes for resource" unless expression.valid?
      end
    end

    private

    def parse_filter(filter)
      logical_or = filter.gsub!(/^or /i, '').present?
      filter_field, operator, filter_value = split_filter_string(filter)
      methods = OPERATORS[operator]

      is_regex = filter_value =~ /%|\*/ && methods[:regex]

      filter_value, method = case filter_value
                             when /^\[(.*)\]$/
                               unless methods[:array]
                                 raise BadRequestError, "Unsupported operator for arrays: #{operator}"
                               end

                               array_value = $1.split(",")
                               [array_value, methods[:array]]
                             when /^'(.*)'$/, /^"(.*)"$/
                               unquoted_filter_value = $1
                               if filter_field.column_type == :string_set && methods[:string_set]
                                 [unquoted_filter_value, methods[:string_set]]
                               elsif is_regex
                                 [unquoted_filter_value, methods[:regex]]
                               else
                                 [unquoted_filter_value, methods[:default]]
                               end
                             when /^(NULL|nil)$/i
                               [nil, methods[:null] || methods[:default]]
                             else
                               if filter_field.datetime?
                                 unless methods[:datetime]
                                   raise BadRequestError, "Unsupported operator for datetime: #{operator}"
                                 end
                                 unless Time.zone.parse(filter_value)
                                   raise BadRequestError, "Bad format for datetime: #{filter_value}"
                                 end

                                 [filter_value, methods[:datetime]]
                               else
                                 [filter_value, methods[:default]]
                               end
                             end

      if is_regex
        filter_value = "/\\A#{Regexp.escape(filter_value)}\\z/"
        filter_value.gsub!(/%|\\\*/, ".*")
      end

      {:logical_or => logical_or, :operator => method, :field => filter_field, :value => filter_value}
    end

    def composite_expression
      and_part = and_expressions.one? ? and_expressions.first : {"AND" => and_expressions}
      or_expressions.empty? ? and_part : {"OR" => [and_part, *or_expressions]}
    end

    def single_expression(field, operator, value)
      {operator => {"field" => field.to_s, "value" => value}}
    end

    def split_filter_string(filter)
      filter_match = OPERATOR_REGEXP.match(filter)
      filter_attr, operator, filter_value = filter_match&.captures

      unless OPERATORS.key?(operator)
        raise BadRequestError, "Unknown operator specified in filter #{filter}"
      end

      *associations, attr_name = filter_attr.split(".")
      [MiqExpression::Field.new(model, associations, attr_name), operator, filter_value]
    end
  end
end