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,7 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'right'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ en:
2
+ activemodel:
3
+ errors:
4
+ messages:
5
+ undefined_filter: is not allowed filter name
6
+ undefined_sort_order: is not allowed sort order
7
+ invalid_page_type: is expected to be a HashMap with :limit and :offset keys
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "activerecord", "4.2.7"
7
+ gem "activesupport", "4.2.7"
8
+ gem "ransack", "1.6.6"
9
+
10
+ gemspec :path => "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "activerecord", "4.2.7"
7
+ gem "activesupport", "4.2.7"
8
+ gem "ransack", "1.8.2"
9
+
10
+ gemspec :path => "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "activerecord", "5.0.0"
7
+ gem "activesupport", "5.0.0"
8
+ gem "ransack", "1.6.6"
9
+
10
+ gemspec :path => "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
6
+ gem "activerecord", "5.0.0"
7
+ gem "activesupport", "5.0.0"
8
+ gem "ransack", "1.8.2"
9
+
10
+ gemspec :path => "../"
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+ require 'forwardable'
4
+ require 'active_model'
5
+ require 'active_support'
6
+ require 'active_support/dependencies/autoload'
7
+ require 'active_support/core_ext/module/delegation'
8
+ require 'active_support/core_ext/object/blank'
9
+ require 'active_support/concern'
10
+ require 'active_support/callbacks'
11
+ require 'middleware'
12
+ require 'uber/inheritable_attr'
13
+
14
+ # Top level namespace for mighty fetchers
15
+ module Right
16
+ require 'right/version'
17
+ extend ActiveSupport::Autoload
18
+
19
+ autoload :Fetcher
20
+ autoload :FetcherError
21
+ autoload :FilterMiddleware
22
+ autoload :FilterParameter
23
+ autoload :FilterParameterDefinition
24
+ autoload :FilterParameters
25
+ autoload :FilterParametersExtractor
26
+ autoload :FilterParametersValidator
27
+ autoload :FilterPredicates
28
+ autoload :FilterUndefinedParameter
29
+ autoload :FilterValueValidator
30
+ autoload :PaginationMiddleware
31
+ autoload :PaginationParametersValidator
32
+ autoload :Paginator
33
+ autoload :RansackableFilter
34
+ autoload :RansackableFilterParametersAdapter
35
+ autoload :RansackableSort
36
+ autoload :RansackableSortParametersAdapter
37
+ autoload :Result
38
+ autoload :Failure, 'right/result'
39
+ autoload :Success, 'right/result'
40
+ autoload :SortMiddleware
41
+ autoload :SortParameter
42
+ autoload :FilterUndefinedParameter
43
+ autoload :SortParameterDefinition
44
+ autoload :SortParametersExtractor
45
+ autoload :SortParametersValidator
46
+ autoload :SortUndefinedParameter
47
+ autoload :SortValueValidator
48
+ autoload :VERSION
49
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+ module Right
3
+ # Configure your own fetcher
4
+ #
5
+ # PagesFetcher < Right::Fetcher
6
+ # self.resource_class = Page
7
+ # end
8
+ #
9
+ # You can configure filterable attributes for model
10
+ #
11
+ # filter :id, validates: { presence: true }
12
+ # filter :name
13
+ # filter :start_at, validates: { presence: true }
14
+ #
15
+ # If your property name doesn't match the name in the query string, use the :as option:
16
+ #
17
+ # filter :kind, as: :type
18
+ #
19
+ # So the Movie#kind property would be exposed to API as :type
20
+ #
21
+ # You may specify allowed sorting order:
22
+ #
23
+ # sort :id
24
+ # sort :name
25
+ #
26
+ # If your property name doesn't match the name in the query string, use the :as option:
27
+ #
28
+ # sort :position, as: :relevance
29
+ #
30
+ # So client should pass +?sort=relevance+ in order to sort by position
31
+ #
32
+ # It's also possible to reverse meaning of the order direction. For example it's not
33
+ # make sense to order by position from lower value to higher.
34
+ # The meaning default for that sorting is reversed order by default, so more relevant elenents
35
+ # would be the first.
36
+ #
37
+ # sort :position, as: :relevance, reverse_direction: true
38
+ #
39
+ class Fetcher # rubocop:disable Metrics/ClassLength
40
+ extend Uber::InheritableAttr
41
+
42
+ inheritable_attr :resource_class
43
+ inheritable_attr :filter_parameters_definition
44
+ inheritable_attr :sort_parameters_definition
45
+ inheritable_attr :middleware_changes
46
+
47
+ self.filter_parameters_definition = FilterParameters.new
48
+ self.sort_parameters_definition = Set.new
49
+ self.middleware_changes = []
50
+
51
+ # @return [Hash]
52
+ attr_reader :params
53
+
54
+ # @param params [Hash]
55
+ def initialize(params)
56
+ @params = params
57
+ end
58
+
59
+ # @return [Right::Result] filtered and sorted collection
60
+ # @yieldparam collection [Result] if a block given
61
+ #
62
+ # @example
63
+ # PagesFetcher.new(params).call #=> Result
64
+ #
65
+ # @example block syntax
66
+ # PagesFetcher.new(params) do |result|
67
+ # if result.success?
68
+ # result.get
69
+ # else
70
+ # result.errors
71
+ # end
72
+ # end
73
+ #
74
+ def call
75
+ processed_params, errors = process_params(params)
76
+ result = if errors.any?
77
+ Failure.new(errors)
78
+ else
79
+ Success.new(fetch(processed_params))
80
+ end
81
+
82
+ if block_given?
83
+ yield result
84
+ else
85
+ result
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # @param parsed_params [Hash]
92
+ # @return [ActiveRecord::Relation]
93
+ # @api public
94
+ # This method may be overridden to implement integration with library other than Ransack
95
+ #
96
+ def fetch(parsed_params)
97
+ collection, = middleware.call([self.class.resource_class, parsed_params])
98
+ collection
99
+ end
100
+
101
+ # @return [Hash, Array] tuple of parameters and processing errors
102
+ # this errors may be shown to front-end developer
103
+ # @api public
104
+ def process_params(params)
105
+ process_params_middleware.call([params, []])
106
+ end
107
+
108
+ # Library params processing middleware stack
109
+ # @return [Middleware::Builder]
110
+ def process_params_middleware
111
+ Middleware::Builder.new do |b|
112
+ b.use FilterParametersExtractor, self.class.filter_parameters_definition
113
+ b.use FilterParametersValidator
114
+ b.use SortParametersExtractor, self.class.sort_parameters_definition
115
+ b.use SortParametersValidator
116
+ b.use PaginationParametersValidator
117
+ end
118
+ end
119
+
120
+ # Library middleware stack
121
+ # @return [Middleware::Builder]
122
+ def fetch_middleware
123
+ Middleware::Builder.new do |b|
124
+ b.use FilterMiddleware
125
+ b.use SortMiddleware
126
+ b.use PaginationMiddleware
127
+ end
128
+ end
129
+
130
+ # User modified middleware stack
131
+ # @return [Middleware::Builder]
132
+ def middleware
133
+ fetch_middleware.tap do |builder|
134
+ self.class.middleware_changes.each do |change|
135
+ builder.instance_eval(&change)
136
+ end
137
+ end
138
+ end
139
+
140
+ class << self
141
+ # Alter middleware chain with the given block
142
+ # @param [Proc] change
143
+ # @return [Right]
144
+ #
145
+ # @example
146
+ # class ChannelsFetcher
147
+ # middleware do
148
+ # use CustomMiddleware
149
+ # end
150
+ # end
151
+ #
152
+ def middleware(&change)
153
+ fail ArgumentError unless block_given?
154
+ middleware_changes << change
155
+ self
156
+ end
157
+
158
+ # Register collection sorting by its name
159
+ # @param name [Symbol] of the field
160
+ # @return [void]
161
+ # @see +RansackableSort+ for details
162
+ #
163
+ def sort(name, options = {})
164
+ definition = SortParameterDefinition.new(name, options)
165
+ sort_parameters_definition.add(definition)
166
+ end
167
+
168
+ # Register collection filter by its name
169
+ # @see +Right::Filter+ for details
170
+ #
171
+ # @overload filter(filter_name, options)
172
+ # @param [Symbol] filter_name
173
+ # @param [Hash] options
174
+ # @return [void]
175
+ # @example
176
+ # filter :genre_name, on: :resource
177
+ #
178
+ # @overload filter(filter_name: predicates, **options)
179
+ # @param [Symbol] filter_name
180
+ # @param [<Symbol>] predicates
181
+ # @param [Hash] other options options
182
+ # @return [void]
183
+ # @example
184
+ # filter genre_name: [:eq, :in], on: :resource
185
+ #
186
+ def filter(*args)
187
+ options = args.extract_options!
188
+ if args.empty?
189
+ filter_name = options.keys.first
190
+ predicates = options.values.first
191
+ options = options.except(filter_name).merge(predicates: predicates)
192
+ else
193
+ filter_name = args.first
194
+ end
195
+
196
+ definition = FilterParameterDefinition.new(filter_name, options)
197
+ filter_parameters_definition.add(definition)
198
+ end
199
+
200
+ # @param params [Hash] user provided input
201
+ # @yieldparam collection [ActiveRecord::Relation]
202
+ def run(params, &block)
203
+ new(params).call(&block)
204
+ end
205
+
206
+ # Add middleware to the end of middleware chane
207
+ #
208
+ # class MovieFetcher
209
+ # after do |scope, params|
210
+ # # do something with scope and params
211
+ # [scope.map(&:resource), params]
212
+ # end
213
+ # end
214
+ #
215
+ def after(&block)
216
+ alter_middleware(:use, &block)
217
+ end
218
+
219
+ # Add middleware to the beginning of middleware chane
220
+ #
221
+ # class MovieFetcher
222
+ # before do |scope, params|
223
+ # # do something with scope and params
224
+ # [scope.map(&:resource), params]
225
+ # end
226
+ # end
227
+ #
228
+ def before(middleware_or_index = 0, &block)
229
+ alter_middleware(:insert_before, middleware_or_index, &block)
230
+ end
231
+
232
+ private
233
+
234
+ def alter_middleware(method_name, *args)
235
+ fail ArgumentError unless block_given?
236
+ middleware_changes.push lambda { |builder|
237
+ builder.send method_name, *args, lambda { |env|
238
+ yield(*env).tap do |r|
239
+ if !r.is_a?(Array) || r.size != 2
240
+ fail 'After block must return tuple of scope and params'
241
+ end
242
+ end
243
+ }
244
+ }
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module Right
3
+ class FetcherError < StandardError
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module Right
3
+ # Filter scope using ransack gem
4
+ #
5
+ class FilterMiddleware
6
+ # @param app [#call, Proc]
7
+ #
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ scope, = ::Middleware::Builder.new do |b|
14
+ b.use RansackableFilterParametersAdapter
15
+ b.use RansackableFilter
16
+ end.call(env)
17
+
18
+ app.call([scope, env[1]])
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :app
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module Right
3
+ # User provided filtering on particular parameter
4
+ #
5
+ class FilterParameter
6
+ # @return [String, nil]
7
+ #
8
+ attr_reader :predicate
9
+
10
+ # @return [ParameterDefinition, nil]
11
+ #
12
+ attr_reader :definition
13
+
14
+ def initialize(value, predicate, definition)
15
+ @value = value
16
+ @predicate = predicate.to_s
17
+ @definition = definition
18
+ end
19
+
20
+ delegate :name, to: :definition
21
+ delegate :on, to: :definition
22
+
23
+ # Check if this parameter provided by user.
24
+ # If the parameter defined by not given by user this returns false.
25
+ # @return [Boolean]
26
+ def provided?
27
+ value.present? && predicate.present?
28
+ end
29
+
30
+ def ==(other)
31
+ is_a?(other.class) &&
32
+ value == other.value &&
33
+ predicate == other.predicate &&
34
+ definition == other.definition
35
+ end
36
+
37
+ # @return [String, Array<String>]
38
+ # TODO: memoize
39
+ def value
40
+ if @value.is_a?(Array)
41
+ @value.map { |v| definition.coerce(v) }
42
+ else
43
+ definition.coerce(@value)
44
+ end
45
+ end
46
+
47
+ def valid?
48
+ validators.all?(&:valid?)
49
+ end
50
+
51
+ def invalid?
52
+ validators.any?(&:invalid?)
53
+ end
54
+
55
+ def errors
56
+ validators.map(&:errors).flat_map(&:full_messages).compact.uniq
57
+ end
58
+
59
+ private
60
+
61
+ def validators
62
+ values_for_validation = @value.is_a?(Array) ? @value : [@value]
63
+ @validators ||= values_for_validation.map { |v| definition.validator.new(v) }
64
+ end
65
+ end
66
+ end