module HalApi::Controller::Filtering extend ActiveSupport::Concern included do class_eval do class_attribute :allowed_filter_names class_attribute :allowed_filter_types end end class FilterParams < OpenStruct def initialize(filters = {}) @filters = filters.with_indifferent_access end def method_missing(m, *args, &_block) if @filters.key?(m) && args.empty? @filters[m] elsif m.to_s[-1] == '?' && args.empty? && @filters.key?(m.to_s.chop) !!@filters[m.to_s.chop] else msg = "Unknown filter param '#{m}'" hint = "Valid filters are: #{@filters.keys.join(' ')}" raise HalApi::Errors::UnknownFilterError.new(msg, hint) end end end module ClassMethods def filter_params(*args) self.allowed_filter_names = [] self.allowed_filter_types = {} (args || []).map do |arg| if arg.is_a? Hash arg.to_a.each { |key, val| add_filter_param(key.to_s, val.to_s) } else add_filter_param(arg.to_s) end end end private def add_filter_param(name, type = nil) unless allowed_filter_names.include? name allowed_filter_names << name allowed_filter_types[name] = type unless type.nil? end end end def filters @filters ||= parse_filters_param end def filter_facets end def index_collection collection = defined?(super) ? super : HalApi::PagedCollection.new([]) # add facets if defined, removing filters/facets with counts of 0 non_zero_facets = (filter_facets || {}).with_indifferent_access.tap do |hash| hash.each do |filter_key, facets| hash[filter_key] = facets.try(:select) { |f| f.try(:[], :count) > 0 } hash.delete(filter_key) if hash[filter_key].blank? end end collection.facets = non_zero_facets unless non_zero_facets.blank? collection end private def parse_filters_param filters_map = {} filters = self.class.allowed_filter_names force_types = self.class.allowed_filter_types # set nils filters.each do |name| filters_map[name] = nil end # parse query param (params[:filters] || '').split(',').each do |str| name, value = str.split('=', 2) next unless filters_map.key?(name) # convert/guess type of known params filters_map[name] = if force_types[name] == 'date' parse_date(value) elsif force_types[name] == 'time' parse_time(value) elsif value.nil? true elsif value.blank? '' elsif [false, 'false'].include? value false elsif [true, 'true'].include? value true elsif value =~ /\A[-+]?\d+\z/ value.to_i else value end end FilterParams.new(filters_map) end def parse_date(str) Date.parse(str) rescue ArgumentError raise HalApi::Errors::BadFilterValueError.new "Invalid filter date: '#{str}'" end def parse_time(str) Time.find_zone('UTC').parse(str) || (raise ArgumentError.new 'Nil result!') rescue ArgumentError raise HalApi::Errors::BadFilterValueError.new "Invalid filter time: '#{str}'" end end