right 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +19 -0
- data/.gitignore +11 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +13 -0
- data/Appraisals +27 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +7 -0
- data/LICENSE +202 -0
- data/README.md +323 -0
- data/Rakefile +7 -0
- data/bin/console +15 -0
- data/bin/setup +7 -0
- data/config/locales/en.yml +7 -0
- data/gemfiles/rails_4.2.7_ransack_1.6.6.gemfile +10 -0
- data/gemfiles/rails_4.2.7_ransack_1.8.2.gemfile +10 -0
- data/gemfiles/rails_5.0.0_ransack_1.6.6.gemfile +10 -0
- data/gemfiles/rails_5.0.0_ransack_1.8.2.gemfile +10 -0
- data/lib/right.rb +49 -0
- data/lib/right/fetcher.rb +248 -0
- data/lib/right/fetcher_error.rb +5 -0
- data/lib/right/filter_middleware.rb +25 -0
- data/lib/right/filter_parameter.rb +66 -0
- data/lib/right/filter_parameter_definition.rb +79 -0
- data/lib/right/filter_parameters.rb +93 -0
- data/lib/right/filter_parameters_extractor.rb +75 -0
- data/lib/right/filter_parameters_validator.rb +27 -0
- data/lib/right/filter_predicates.rb +97 -0
- data/lib/right/filter_undefined_parameter.rb +14 -0
- data/lib/right/filter_value_validator.rb +35 -0
- data/lib/right/pagination_middleware.rb +54 -0
- data/lib/right/pagination_parameters_validator.rb +46 -0
- data/lib/right/paginator.rb +38 -0
- data/lib/right/railtie.rb +8 -0
- data/lib/right/ransackable_filter.rb +25 -0
- data/lib/right/ransackable_filter_parameters_adapter.rb +62 -0
- data/lib/right/ransackable_sort.rb +28 -0
- data/lib/right/ransackable_sort_parameters_adapter.rb +24 -0
- data/lib/right/result.rb +72 -0
- data/lib/right/sort_middleware.rb +27 -0
- data/lib/right/sort_parameter.rb +53 -0
- data/lib/right/sort_parameter_definition.rb +53 -0
- data/lib/right/sort_parameters_extractor.rb +60 -0
- data/lib/right/sort_parameters_validator.rb +27 -0
- data/lib/right/sort_undefined_parameter.rb +14 -0
- data/lib/right/sort_value_validator.rb +35 -0
- data/lib/right/version.rb +4 -0
- data/right.gemspec +36 -0
- metadata +281 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Filtering parameter definition
|
4
|
+
#
|
5
|
+
class FilterParameterDefinition
|
6
|
+
# If the property name doesn't match the name in the query string, use the :as option
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :as
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Association on which this parameter is defined.
|
14
|
+
# @return [String, nil]
|
15
|
+
attr_reader :on
|
16
|
+
|
17
|
+
# White-listed predicates
|
18
|
+
# @return [<String>]
|
19
|
+
attr_reader :predicates
|
20
|
+
|
21
|
+
# @param [String] name of the field
|
22
|
+
# @param [Hash] options
|
23
|
+
# @option options [Symbol, {Symbol => String}, {Symbol => <String>}] :on (nil) filter on given relation
|
24
|
+
# @option options [<String>] :predicates (ALL_PREDICATES) white-listed predicates
|
25
|
+
# @option options [Proc] :coerce coercion for the value
|
26
|
+
# @option options [Hash{Symbol => any}] :validates for the value
|
27
|
+
# @example
|
28
|
+
# validates: presence: true, length: { is: 6 }
|
29
|
+
#
|
30
|
+
def initialize(name, options = {})
|
31
|
+
options.assert_valid_keys(:as, :on, :predicates, :coerce, :validates)
|
32
|
+
@as = options.fetch(:as, name).to_s
|
33
|
+
@name = name.to_s
|
34
|
+
@on = options[:on]
|
35
|
+
@predicates = Array(options.fetch(:predicates, FilterPredicates.all)).map(&:to_s)
|
36
|
+
@coerce = options.fetch(:coerce, ->(v) { v })
|
37
|
+
@validations = options.fetch(:validates, {})
|
38
|
+
end
|
39
|
+
|
40
|
+
# If two parameters have the same name, they are equal.
|
41
|
+
delegate :hash, to: :name
|
42
|
+
|
43
|
+
def eql?(other)
|
44
|
+
other.is_a?(self.class) && other.name == name
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
other.is_a?(self.class) &&
|
49
|
+
other.name == name &&
|
50
|
+
other.as == as &&
|
51
|
+
other.on == on &&
|
52
|
+
other.predicates == predicates
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [any] value
|
56
|
+
# @return [any] coerced value according the definition
|
57
|
+
#
|
58
|
+
def coerce(value)
|
59
|
+
@coerce.call(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def validator
|
63
|
+
FilterValueValidator.build(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def defined?
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def undefined?
|
71
|
+
!self.defined?
|
72
|
+
end
|
73
|
+
|
74
|
+
# Proc with defined validations
|
75
|
+
# @return [Proc]
|
76
|
+
# @api private
|
77
|
+
attr_reader :validations
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
#
|
4
|
+
class FilterParameters
|
5
|
+
include Comparable
|
6
|
+
include Enumerable
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
FilterError = Class.new(KeyError)
|
10
|
+
|
11
|
+
def_delegators :parameters, :detect, :each
|
12
|
+
|
13
|
+
# @param [<Right::FilterParameter>]
|
14
|
+
def initialize(parameters = nil)
|
15
|
+
@parameters = Set.new(parameters)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Dup internal set.
|
19
|
+
def initialize_dup(orig)
|
20
|
+
super
|
21
|
+
@parameters = orig.parameters.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
# Clone internal set.
|
25
|
+
def initialize_clone(orig)
|
26
|
+
super
|
27
|
+
@parameters = orig.parameters.clone
|
28
|
+
end
|
29
|
+
|
30
|
+
# Find filter by name
|
31
|
+
# @param name [String]
|
32
|
+
# @return [Right::FilterParameter, nil]
|
33
|
+
#
|
34
|
+
def [](name)
|
35
|
+
parameters.detect { |filter| filter.name == name }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Find filter by name or raise error
|
39
|
+
# @param name [String]
|
40
|
+
# @yieldparam name [String]
|
41
|
+
# block value will be returned if no `FilterParameter` found with specified name
|
42
|
+
# @return [Right::FilterParameter]
|
43
|
+
# @raise FilterError
|
44
|
+
#
|
45
|
+
def fetch(name)
|
46
|
+
if (filter = self[name])
|
47
|
+
filter
|
48
|
+
elsif block_given?
|
49
|
+
yield(name)
|
50
|
+
else
|
51
|
+
fail FilterError, "filter not found: #{name.inspect}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# param value [Right::FilterParameter]
|
56
|
+
def add(value)
|
57
|
+
self.class.new(parameters.add(value))
|
58
|
+
end
|
59
|
+
|
60
|
+
alias << add
|
61
|
+
|
62
|
+
def map(&block)
|
63
|
+
self.class.new(parameters.map(&block))
|
64
|
+
end
|
65
|
+
|
66
|
+
# param other [Right::FilterParameters]
|
67
|
+
def -(other)
|
68
|
+
self.class.new(parameters - other.parameters)
|
69
|
+
end
|
70
|
+
|
71
|
+
# param other [Right::FilterParameters]
|
72
|
+
def +(other)
|
73
|
+
self.class.new(parameters.merge(other.parameters))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Delete filter by name
|
77
|
+
# @param name [String]
|
78
|
+
# @return [Right::FilterParameter, nil]
|
79
|
+
def delete(name)
|
80
|
+
filter_parameter = self[name]
|
81
|
+
parameters.delete(filter_parameter) if filter_parameter
|
82
|
+
filter_parameter
|
83
|
+
end
|
84
|
+
|
85
|
+
def <=>(other)
|
86
|
+
parameters <=> other.parameters
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
attr_reader :parameters
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# User provided filters syntax:
|
4
|
+
# * `{ 'name_eq' => 'Martin' }` - name is equal to Martin
|
5
|
+
# * `{ 'name_in' => 'Martin,Bob' }` - name is either Martin or Bob
|
6
|
+
#
|
7
|
+
# This middleware parses filters hash and builds Parameter array
|
8
|
+
# @see Right::FilterParameter
|
9
|
+
#
|
10
|
+
# If user passes not defined filter, it yields to `UndefinedParameter`, so you may
|
11
|
+
# validate it.
|
12
|
+
#
|
13
|
+
class FilterParametersExtractor
|
14
|
+
# @param app [#call]
|
15
|
+
# @param parameters_definition [Set<Right::FilterParameterDefinition>]
|
16
|
+
def initialize(app, parameters_definition)
|
17
|
+
@app = app
|
18
|
+
@parameters_definition = parameters_definition
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param env [<{:filter => Hash}, []>]
|
22
|
+
# * first element is a scope to be filtered
|
23
|
+
# * second is a hash with user provided filters
|
24
|
+
# @return [<{:filter => <Right::FilterParameter>, []}]
|
25
|
+
#
|
26
|
+
def call(env)
|
27
|
+
params, errors = env
|
28
|
+
|
29
|
+
provided_parameters = Hash(params[:filter]).each_with_object(FilterParameters.new) do |(name, value), parameters|
|
30
|
+
type_casted_value = type_cast_value(name, value)
|
31
|
+
parameters << extract_parameter(name, type_casted_value)
|
32
|
+
end
|
33
|
+
|
34
|
+
not_provided_parameters = parameters_definition - provided_parameters.map(&:definition)
|
35
|
+
|
36
|
+
undefined_parameters = not_provided_parameters.map do |definition|
|
37
|
+
FilterParameter.new(nil, nil, definition)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Keep even undefined parameters to validate required ones
|
41
|
+
filters = provided_parameters + undefined_parameters
|
42
|
+
|
43
|
+
app.call([params.merge(filter: filters), errors])
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :parameters_definition, :app
|
49
|
+
|
50
|
+
def name_and_predicate(name_with_predicate)
|
51
|
+
predicate = FilterPredicates.all.detect { |p| name_with_predicate.end_with?("_#{p}") }
|
52
|
+
[
|
53
|
+
name_with_predicate.chomp("_#{predicate}"),
|
54
|
+
predicate,
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [String] name
|
59
|
+
# @param [String, Array<String>] value
|
60
|
+
#
|
61
|
+
def extract_parameter(name, value)
|
62
|
+
name, predicate = name_and_predicate(name)
|
63
|
+
definition = parameters_definition.detect { |d| d.as == name } || FilterUndefinedParameter.new(name)
|
64
|
+
FilterParameter.new(value, predicate, definition)
|
65
|
+
end
|
66
|
+
|
67
|
+
def type_cast_value(name, value)
|
68
|
+
if name.end_with?(*FilterPredicates.array)
|
69
|
+
value.split(',')
|
70
|
+
else
|
71
|
+
value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Validates filters and raises error if one of them is invalid
|
4
|
+
#
|
5
|
+
class FilterParametersValidator
|
6
|
+
# @param app [#call]
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param env [<{:filter => <Right::FilterParameter>, Array>}]
|
12
|
+
# @return [<{:filter => <Right::FilterParameter>, Array>}]
|
13
|
+
#
|
14
|
+
def call(env)
|
15
|
+
params, errors = env
|
16
|
+
|
17
|
+
invalid_filters = Array(params[:filter]).select(&:invalid?)
|
18
|
+
messages = invalid_filters.flat_map(&:errors)
|
19
|
+
|
20
|
+
app.call([params, errors.concat(messages)])
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :app
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Contains contains with all supported predicates
|
4
|
+
# You can register your own predicates. Predicates should perform on array or on singular value:
|
5
|
+
#
|
6
|
+
# Right::FilterPredicates.register('includes', on: :array)
|
7
|
+
# Right::FilterPredicates.register('is_upper_case', on: :value)
|
8
|
+
#
|
9
|
+
module FilterPredicates
|
10
|
+
NOT_EQ = 'not_eq'
|
11
|
+
EQ = 'eq'
|
12
|
+
DOES_NOT_MATCH = 'does_not_match'
|
13
|
+
MATCHES = 'matches'
|
14
|
+
GT = 'gt'
|
15
|
+
LT = 'lt'
|
16
|
+
GTEQ = 'gteq'
|
17
|
+
LTEQ = 'lteq'
|
18
|
+
NOT_CONT = 'not_cont'
|
19
|
+
CONT = 'cont'
|
20
|
+
NOT_START = 'not_start'
|
21
|
+
START = 'start'
|
22
|
+
DOES_NOT_END = 'not_end'
|
23
|
+
ENDS = 'end'
|
24
|
+
NOT_TRUE = 'not_true'
|
25
|
+
TRUE = 'true'
|
26
|
+
NOT_FALSE = 'not_false'
|
27
|
+
FALSE = 'false'
|
28
|
+
BLANK = 'blank'
|
29
|
+
PRESENT = 'present'
|
30
|
+
NOT_NULL = 'not_null'
|
31
|
+
NULL = 'null'
|
32
|
+
NOT_IN = 'not_in'
|
33
|
+
IN = 'in'
|
34
|
+
NOT_CONT_ANY = 'not_cont_any'
|
35
|
+
CONT_ANY = 'cont_any'
|
36
|
+
|
37
|
+
@predicates_on_value = Set.new([
|
38
|
+
NOT_EQ, EQ,
|
39
|
+
DOES_NOT_MATCH, MATCHES,
|
40
|
+
GT, LT,
|
41
|
+
GTEQ, LTEQ,
|
42
|
+
NOT_CONT, CONT,
|
43
|
+
NOT_START, START,
|
44
|
+
DOES_NOT_END, ENDS,
|
45
|
+
NOT_TRUE, TRUE,
|
46
|
+
NOT_FALSE, FALSE,
|
47
|
+
BLANK, PRESENT,
|
48
|
+
NOT_NULL, NULL
|
49
|
+
])
|
50
|
+
@predicates_on_array = Set.new([
|
51
|
+
NOT_IN, IN,
|
52
|
+
NOT_CONT_ANY, CONT_ANY
|
53
|
+
])
|
54
|
+
|
55
|
+
# Registers predicate on singular value or on array
|
56
|
+
# @param predicate [String, Symbol]
|
57
|
+
# @param on [:array, :value]
|
58
|
+
# @return [Right::FilterPredicates]
|
59
|
+
#
|
60
|
+
def register(predicate, on:)
|
61
|
+
case on
|
62
|
+
when :value
|
63
|
+
@predicates_on_value.add(predicate.to_s)
|
64
|
+
when :array
|
65
|
+
@predicates_on_array.add(predicate.to_s)
|
66
|
+
else
|
67
|
+
fail ArgumentError, 'on must be :array, or :value'
|
68
|
+
end
|
69
|
+
self
|
70
|
+
end
|
71
|
+
module_function :register
|
72
|
+
|
73
|
+
# Returns predicates for array
|
74
|
+
# @return [Set<String>]
|
75
|
+
#
|
76
|
+
def array
|
77
|
+
@predicates_on_array.dup
|
78
|
+
end
|
79
|
+
module_function :array
|
80
|
+
|
81
|
+
# Returns predicates for values
|
82
|
+
# @return [Set<String>]
|
83
|
+
#
|
84
|
+
def value
|
85
|
+
@predicates_on_value.dup
|
86
|
+
end
|
87
|
+
module_function :value
|
88
|
+
|
89
|
+
# Returns all predicates
|
90
|
+
# @return [Set<String>]
|
91
|
+
#
|
92
|
+
def all
|
93
|
+
@predicates_on_value + @predicates_on_array
|
94
|
+
end
|
95
|
+
module_function :all
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Build singleton validation class for specified attribute name
|
4
|
+
# @example you need a nice validator for a first_name
|
5
|
+
# validator_klass = ValueValidator.build('first_name', presence: true, length: { minimum: 3 })
|
6
|
+
# validator = validator_klass.new('Bo')
|
7
|
+
# validator.valid? #=> false
|
8
|
+
# validator.errors.full_messages #=> ['First name is too short (minimum is 3 characters)']
|
9
|
+
#
|
10
|
+
module FilterValueValidator
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# Validates if Parameter is undefined or not
|
14
|
+
class DefinedValidator < ActiveModel::EachValidator
|
15
|
+
def validate_each(record, attribute, _value)
|
16
|
+
record.errors.add(attribute, :undefined_filter) if record.undefined?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def build(definition)
|
21
|
+
Struct.new(definition.as.to_sym) do
|
22
|
+
include ActiveModel::Validations
|
23
|
+
|
24
|
+
validates(definition.as, definition.validations) if definition.validations.present?
|
25
|
+
validates(definition.as, 'right/filter_value_validator/defined': true)
|
26
|
+
|
27
|
+
define_method(:undefined?) { definition.undefined? }
|
28
|
+
|
29
|
+
define_singleton_method :model_name do
|
30
|
+
ActiveModel::Name.new(Right, nil, 'Right')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Pagination middleware
|
4
|
+
#
|
5
|
+
class PaginationMiddleware
|
6
|
+
# @param app [#call, Proc]
|
7
|
+
# @param max_per_page [Integer]
|
8
|
+
# @param per_page [Integer]
|
9
|
+
#
|
10
|
+
def initialize(app, max_per_page: false, per_page: 50, paginator_class: Paginator)
|
11
|
+
@app = app
|
12
|
+
@max_per_page = max_per_page
|
13
|
+
@per_page = per_page
|
14
|
+
@paginator_class = paginator_class
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Array(ActiveRecord::Relation, Hash)] env
|
18
|
+
# First argument is a ActiveRecord relation which must be paginated
|
19
|
+
# Second argument is a request parameters provided by user
|
20
|
+
#
|
21
|
+
def call(env)
|
22
|
+
scope, params = env
|
23
|
+
|
24
|
+
paginated_scope = @paginator_class.new(pagination_options(params)).paginate(scope)
|
25
|
+
app.call([paginated_scope, params])
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :app
|
31
|
+
|
32
|
+
# @param [Hash] params
|
33
|
+
# @option params [Hash] (nil) :limit
|
34
|
+
# @option params [Hash] (nil) :offset
|
35
|
+
#
|
36
|
+
def pagination_options(params)
|
37
|
+
options = default_pagination_options.merge(Hash(params[:page]))
|
38
|
+
max_per_page = @max_per_page
|
39
|
+
|
40
|
+
if max_per_page && options[:limit] > max_per_page
|
41
|
+
options.merge(limit: max_per_page)
|
42
|
+
else
|
43
|
+
options
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_pagination_options
|
48
|
+
{
|
49
|
+
limit: @per_page,
|
50
|
+
offset: 0,
|
51
|
+
}.with_indifferent_access
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|