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
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/lib/right.rb
ADDED
@@ -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,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
|