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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +19 -0
  3. data/.gitignore +11 -0
  4. data/.hound.yml +2 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +13 -0
  7. data/.travis.yml +13 -0
  8. data/Appraisals +27 -0
  9. data/CHANGELOG.md +25 -0
  10. data/Gemfile +7 -0
  11. data/LICENSE +202 -0
  12. data/README.md +323 -0
  13. data/Rakefile +7 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +7 -0
  16. data/config/locales/en.yml +7 -0
  17. data/gemfiles/rails_4.2.7_ransack_1.6.6.gemfile +10 -0
  18. data/gemfiles/rails_4.2.7_ransack_1.8.2.gemfile +10 -0
  19. data/gemfiles/rails_5.0.0_ransack_1.6.6.gemfile +10 -0
  20. data/gemfiles/rails_5.0.0_ransack_1.8.2.gemfile +10 -0
  21. data/lib/right.rb +49 -0
  22. data/lib/right/fetcher.rb +248 -0
  23. data/lib/right/fetcher_error.rb +5 -0
  24. data/lib/right/filter_middleware.rb +25 -0
  25. data/lib/right/filter_parameter.rb +66 -0
  26. data/lib/right/filter_parameter_definition.rb +79 -0
  27. data/lib/right/filter_parameters.rb +93 -0
  28. data/lib/right/filter_parameters_extractor.rb +75 -0
  29. data/lib/right/filter_parameters_validator.rb +27 -0
  30. data/lib/right/filter_predicates.rb +97 -0
  31. data/lib/right/filter_undefined_parameter.rb +14 -0
  32. data/lib/right/filter_value_validator.rb +35 -0
  33. data/lib/right/pagination_middleware.rb +54 -0
  34. data/lib/right/pagination_parameters_validator.rb +46 -0
  35. data/lib/right/paginator.rb +38 -0
  36. data/lib/right/railtie.rb +8 -0
  37. data/lib/right/ransackable_filter.rb +25 -0
  38. data/lib/right/ransackable_filter_parameters_adapter.rb +62 -0
  39. data/lib/right/ransackable_sort.rb +28 -0
  40. data/lib/right/ransackable_sort_parameters_adapter.rb +24 -0
  41. data/lib/right/result.rb +72 -0
  42. data/lib/right/sort_middleware.rb +27 -0
  43. data/lib/right/sort_parameter.rb +53 -0
  44. data/lib/right/sort_parameter_definition.rb +53 -0
  45. data/lib/right/sort_parameters_extractor.rb +60 -0
  46. data/lib/right/sort_parameters_validator.rb +27 -0
  47. data/lib/right/sort_undefined_parameter.rb +14 -0
  48. data/lib/right/sort_value_validator.rb +35 -0
  49. data/lib/right/version.rb +4 -0
  50. data/right.gemspec +36 -0
  51. 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,14 @@
1
+ # frozen_string_literal: true
2
+ module Right
3
+ # Null object for ParameterDefinition
4
+ #
5
+ class FilterUndefinedParameter < FilterParameterDefinition
6
+ def required?
7
+ false
8
+ end
9
+
10
+ def defined?
11
+ false
12
+ end
13
+ end
14
+ 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