right 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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