right 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|