class Praxis::Extensions::AttributeFiltering::FilteringParams
Constants
- AVAILABLE_OPERATORS
- NOVALUE_OPERATORS
- VALUE_OPERATORS
Attributes
Private Class Methods
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
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
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
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 116 def self.constructable? true end
Source
# 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
Calls superclass method
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 108 def self.display_name 'Filtering' end
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 204 def self.dump(value, **_opts) load(value).dump end
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
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 112 def self.family 'string' end
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
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
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 74 def json_schema_type :string end
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
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 104 def self.name 'Praxis::Types::FilteringParams' end
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 100 def self.native_type self end
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 220 def initialize(parsed = []) @parsed_array = parsed end
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
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
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
Source
# 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
Dump back string parseable form
Source
# File lib/praxis/extensions/attribute_filtering/filtering_params.rb, line 293 def each(&block) parsed_array&.each(&block) end
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
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