-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from PRX/feat/sorting_filtering
Add sorting and filtering
- Loading branch information
Showing
10 changed files
with
397 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
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 | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
module HalApi::Controller::Sorting | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
class_eval do | ||
class_attribute :allowed_sort_names | ||
class_attribute :default_sort | ||
end | ||
end | ||
|
||
module ClassMethods | ||
def sort_params(args) | ||
self.allowed_sort_names = args[:allowed].map(&:to_s).uniq | ||
self.default_sort = args[:default] | ||
if default_sort && !default_sort.is_a?(Array) | ||
self.default_sort = Array[default_sort] | ||
end | ||
end | ||
end | ||
|
||
def sorts | ||
@sorts ||= parse_sorts_param | ||
end | ||
|
||
def sorted(arel) | ||
apply_sorts = !sorts.blank? ? sorts : default_sort | ||
if apply_sorts.blank? | ||
super | ||
else | ||
arel.order(*apply_sorts) | ||
end | ||
end | ||
|
||
private | ||
|
||
# support ?sorts=attribute,attribute:direction params | ||
# e.g. ?sorts=published_at,updated_at:desc | ||
# desc is default if a direction is not specified | ||
def parse_sorts_param | ||
sorts_array = [] | ||
allowed_sorts = self.class.allowed_sort_names | ||
|
||
# parse sort param for name of the column and direction | ||
# default is descending, because I say so, and we have a bias towards the new | ||
(params[:sorts] || '').split(',').each do |str| | ||
name, direction = str.split(':', 2).map { |s| s.to_s.strip } | ||
name = name.underscore | ||
direction = direction.blank? ? 'desc' : direction.downcase | ||
unless allowed_sorts.include?(name) | ||
hint = "Valid sorts are: #{allowed_sorts.join(' ')}" | ||
raise HalApi::Errors::BadSortError.new("Invalid sort: #{name}", hint) | ||
end | ||
unless ['asc', 'desc'].include?(direction) | ||
hint = "Valid directions are: asc desc" | ||
raise HalApi::Errors::BadSortError.new("Invalid sort direction: #{direction}", hint) | ||
end | ||
sorts_array << { name => direction } | ||
end | ||
sorts_array | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
module HalApi | ||
module Rails | ||
VERSION = "0.5.0" | ||
VERSION = "0.6.0" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
require 'test_helper' | ||
|
||
describe HalApi::Controller::Filtering do | ||
|
||
class FilteringTestController < ActionController::Base | ||
include HalApi::Controller::Filtering | ||
|
||
filter_params :one, :two, :three, :four, :five, six: :date, seven: :time | ||
|
||
attr_accessor :filter_string | ||
|
||
def params | ||
{ filters: filter_string } | ||
end | ||
end | ||
|
||
let(:controller) { FilteringTestController.new } | ||
|
||
it 'parses query params' do | ||
controller.filter_string = 'one,two=2,three=something,four=' | ||
controller.filters.one.must_equal true | ||
controller.filters.two.must_equal 2 | ||
controller.filters.three.must_equal 'something' | ||
controller.filters.four.must_equal '' | ||
end | ||
|
||
it 'restricts to known params' do | ||
controller.filter_string = 'one,foo,two,bar' | ||
controller.filters.one.must_equal true | ||
controller.filters.two.must_equal true | ||
err = assert_raises { controller.filters.foo } | ||
err.must_be_instance_of(HalApi::Errors::UnknownFilterError) | ||
err.hint.must_match /valid filters are: one two/i | ||
err = assert_raises { controller.filters.whatever } | ||
err.must_be_instance_of(HalApi::Errors::UnknownFilterError) | ||
err.hint.must_match /valid filters are: one two/i | ||
end | ||
|
||
it 'provides boolean testers' do | ||
controller.filter_string = 'one,two=1,three=false,four=,five=0' | ||
controller.filters.one?.must_equal true | ||
controller.filters.two?.must_equal true | ||
controller.filters.three?.must_equal false | ||
controller.filters.four?.must_equal true | ||
controller.filters.five?.must_equal true | ||
controller.filters.six?.must_equal false | ||
controller.filters.seven?.must_equal false | ||
assert_raises { controller.filters.whatever? } | ||
end | ||
|
||
it 'defaults to nil/false for unset filters' do | ||
controller.filter_string = nil | ||
controller.filters.one.must_be_nil | ||
controller.filters.one?.must_equal false | ||
end | ||
|
||
it 'parses dates' do | ||
controller.filter_string = 'six=20190203' | ||
controller.filters.six?.must_equal true | ||
controller.filters.six.must_equal Date.parse('2019-02-03') | ||
end | ||
|
||
it 'raises parse errors for dates' do | ||
controller.filter_string = 'six=bad-string' | ||
err = assert_raises { puts controller.filters.six } | ||
err.must_be_instance_of(HalApi::Errors::BadFilterValueError) | ||
end | ||
|
||
it 'parses datetimes' do | ||
controller.filter_string = 'seven=2019-02-03T01:02:03 -0700' | ||
controller.filters.seven?.must_equal true | ||
controller.filters.seven.must_equal Time.parse('2019-02-03T08:02:03Z') | ||
end | ||
|
||
it 'defaults datetimes to utc' do | ||
controller.filter_string = 'seven=20190203' | ||
controller.filters.seven?.must_equal true | ||
controller.filters.seven.must_equal Time.parse('2019-02-03T00:00:00Z') | ||
end | ||
|
||
it 'raises parse errors for times' do | ||
controller.filter_string = 'seven=bad-string' | ||
err = assert_raises { puts controller.filters.seven } | ||
err.must_be_instance_of(HalApi::Errors::BadFilterValueError) | ||
end | ||
|
||
end |
Oops, something went wrong.