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,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