class Praxis::Extensions::AttributeFiltering::FilteringParams

Constants

AVAILABLE_OPERATORS
NOVALUE_OPERATORS
VALUE_OPERATORS

Attributes

allowed_filters[R]
allowed_leaves[R]
media_type[R]
parsed_array[R]

Private Class Methods

add_any(name, operators:, fuzzy:) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 90
def add_any(name, operators:, fuzzy:)
  raise 'Invalid set of operators passed' unless AVAILABLE_OPERATORS.superset?(operators)

  @allowed_leaves[name] = {
    operators: operators,
    fuzzy_match: fuzzy
  }
end
add_filter(name, operators:, fuzzy:) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 78
def add_filter(name, operators:, fuzzy:)
  components = name.to_s.split('.').map(&:to_sym)
  attribute, _enclosing_type = find_filter_attribute(components, media_type)
  raise 'Invalid set of operators passed' unless AVAILABLE_OPERATORS.superset?(operators)

  @allowed_filters[name] = {
    value_type: attribute.type,
    operators: operators,
    fuzzy_match: fuzzy
  }
end
construct(definition, **options) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 120
def self.construct(definition, **options)
  return self if definition.nil?

  DSLCompiler.new(self, **options).parse(*definition)
  self
end
constructable?() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 116
def self.constructable?
  true
end
describe(_root = false, example: nil) click to toggle source
Calls superclass method
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 208
def self.describe(_root = false, example: nil)
  hash = super
  if allowed_filters
    hash[:filters] = allowed_filters.each_with_object({}) do |(name, spec), accum|
      accum[name] = { operators: spec[:operators].to_a }
      accum[name][:fuzzy] = true if spec[:fuzzy_match]
    end
  end

  hash
end
display_name() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 108
def self.display_name
  'Filtering'
end
dump(value, **_opts) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 204
def self.dump(value, **_opts)
  load(value).dump
end
example(_context = Attributor::DEFAULT_ROOT_CONTEXT, **_options) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 138
def self.example(_context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
  fields = if media_type
             mt_example = media_type.example
             pickable_fields = mt_example.object.keys & allowed_filters.keys
             pickable_fields.sample(2).each_with_object([]) do |filter_name, arr|
               op = allowed_filters[filter_name][:operators].to_a.sample(1).first

               # Switch this to pick the right example attribute from the mt example
               filter_components = filter_name.to_s.split('.').map(&:to_sym)
               mapped_attribute, _enclosing_type = find_filter_attribute(filter_components, media_type)
               unless mapped_attribute
                 raise "filter with name #{filter_name} does not correspond to an existing field inside " \
                       " MediaType #{media_type.name}"
               end
               if NOVALUE_OPERATORS.include?(op)
                 arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
               else
                 attr_example = filter_components.inject(mt_example) do |last, name|
                   # we can safely do sends, since we've verified the components are valid
                   last.send(name)
                 end
                 arr << "#{filter_name}#{op}#{attr_example}"
               end
             end.join('&')
           else
             'name=Joe&date>2017-01-01'
           end
  load(fields)
end
family() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 112
def self.family
  'string'
end
find_filter_attribute(name_components, type) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 127
def self.find_filter_attribute(name_components, type)
  type = type.member_type if type < Attributor::Collection
  first, *rest = name_components
  first_attr = type.attributes[first]
  raise "Error, you've requested to filter by field '#{first}' which does not exist in the #{type.name} mediatype!\n" unless first_attr

  return find_filter_attribute(rest, first_attr.type) if rest.present?

  [first_attr, type] # Return the attribute and associated enclosing type
end
for(media_type, **_opts) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 61
def for(media_type, **_opts)
  unless media_type < Praxis::MediaType
    raise ArgumentError, "Invalid type: #{media_type.name} for Filters. " \
      'Using the .for method for defining a filter, requires passing a subclass of a MediaType'
  end

  ::Class.new(self) do
    @media_type = media_type
    @allowed_filters = {}
    @allowed_leaves = {}
  end
end
json_schema_type() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 74
def json_schema_type
  :string
end
load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 173
def self.load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
  return filters if filters.is_a?(native_type)
  return new if filters.nil? || filters.blank?

  parsed = Parser.new.parse(filters)

  tree = ConditionGroup.load(parsed)

  rr = tree.flattened_conditions
  accum = []
  rr.each do |spec|
    attr_name = spec[:name]
    # TODO: Do we need to CGI.unescape things? here or even before??...
    coerced = \
      if media_type
        filter_components = attr_name.to_s.split('.').map(&:to_sym)
        attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
        if spec[:values].is_a?(Array)
          attr_coll = Attributor::Collection.of(attr.type)
          attr_coll.load(spec[:values])
        else
          attr.load(spec[:values])
        end
      else
        spec[:values]
      end
    accum.push(name: attr_name, op: spec[:op], value: coerced, fuzzy: spec[:fuzzies], node_object: spec[:node_object])
  end
  new(accum)
end
name() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 104
def self.name
  'Praxis::Types::FilteringParams'
end
native_type() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 100
def self.native_type
  self
end
new(parsed = []) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 220
def initialize(parsed = [])
  @parsed_array = parsed
end
validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 168
def self.validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
  instance = load(value, context)
  instance.validate(context)
end

Private Instance Methods

allowed_filters() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 297
def allowed_filters
  # Class method defined by the subclassing Class (using .for)
  self.class.allowed_filters
end
allowed_leaves() click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 302
def allowed_leaves
  # Class method defined by the subclassing Class (using .for)
  self.class.allowed_leaves
end
dump() click to toggle source

Dump back string parseable form

# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 264
def dump
  parsed_array.each_with_object([]) do |item, arr|
    field = item[:name]
    value = \
      if item[:value].is_a?(Array)
        item[:value].map.with_index do |i, idx|
          case item[:fuzzy][idx]
          when nil
            i
          when :start
            "*#{i}"
          when :end
            "#{i}*"
          end
        end.join(',')
      else
        case item[:fuzzy]
        when nil
          item[:value]
        when :start
          "*#{item[:value]}"
        when :end
          "#{item[:value]}*"
        end
      end
    arr << "#{field}#{item[:op]}#{value}"
  end.join('&')
end
each(&block) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 293
def each(&block)
  parsed_array&.each(&block)
end
matching_leaf_filter(filter_string) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 224
def matching_leaf_filter(filter_string)
  return nil unless allowed_leaves.keys.present?

  last_component = filter_string.to_s.split('.').last.to_sym
  allowed_leaves[last_component]
end
validate(_context = Attributor::DEFAULT_ROOT_CONTEXT) click to toggle source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 231
def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
  # Treat a blank block definition for the filters, as a way to allow any valid filter, on any operator and fuz
  # Obviously, the filter names need to be valid, but that's checked below.
  # Also, in some circumstances, you'd need to make sure there is a filters_map entry for the ones that aren't directly translatable to query associations/columns
  return [] if allowed_filters.blank? && allowed_leaves.blank?

  parsed_array.each_with_object([]) do |item, errors|
    attr_name = item[:name]
    attr_filters = allowed_filters[attr_name]
    unless attr_filters
      # does not match a complete filter, let's check if it matches an 'any' filter on the last component
      attr_filters = matching_leaf_filter(attr_name)
      unless attr_filters
        msg = "Filtering by #{attr_name} is not allowed. You can filter by #{allowed_filters.keys.map(&:to_s).join(', ')}"
        msg += " or leaf attributes matching #{allowed_leaves.keys.map(&:to_s).join(', ')}" if allowed_leaves.keys.presence
        errors << msg
        next
      end
    end
    allowed_operators = attr_filters[:operators]
    errors << "Operator #{item[:op]} not allowed for filter #{attr_name}" unless allowed_operators.include?(item[:op])
    value_type = attr_filters[:value_type]
    next unless value_type == Attributor::String

    next unless item[:value].presence

    fuzzy_match = attr_filters[:fuzzy_match]
    # If fuzzy matches aren't allowed, but there is one passed in (or in the case of a multimatch, any of the ones in it), we disallow it
    errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)" if item[:fuzzy] && !fuzzy_match && !(item[:fuzzy].is_a?(Array) && item[:fuzzy].compact.empty?)
  end
end