might 0.3.0
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/.gitignore +10 -0
- data/.hound.yml +2 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +14 -0
- data/Gemfile +6 -0
- data/LICENSE +202 -0
- data/README.md +301 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/config/locales/en.yml +7 -0
- data/lib/might.rb +4 -0
- data/lib/might/fetcher.rb +252 -0
- data/lib/might/fetcher_error.rb +4 -0
- data/lib/might/filter_middleware.rb +28 -0
- data/lib/might/filter_parameter.rb +68 -0
- data/lib/might/filter_parameter_definition.rb +82 -0
- data/lib/might/filter_parameters_extractor.rb +78 -0
- data/lib/might/filter_parameters_validator.rb +26 -0
- data/lib/might/filter_predicates.rb +26 -0
- data/lib/might/filter_undefined_parameter.rb +15 -0
- data/lib/might/filter_value_validator.rb +43 -0
- data/lib/might/pagination_middleware.rb +60 -0
- data/lib/might/pagination_parameters_validator.rb +55 -0
- data/lib/might/paginator.rb +58 -0
- data/lib/might/railtie.rb +8 -0
- data/lib/might/ransackable_filter.rb +24 -0
- data/lib/might/ransackable_filter_parameters_adapter.rb +61 -0
- data/lib/might/ransackable_sort.rb +27 -0
- data/lib/might/ransackable_sort_parameters_adapter.rb +23 -0
- data/lib/might/result.rb +68 -0
- data/lib/might/sort_middleware.rb +31 -0
- data/lib/might/sort_parameter.rb +54 -0
- data/lib/might/sort_parameter_definition.rb +54 -0
- data/lib/might/sort_parameters_extractor.rb +62 -0
- data/lib/might/sort_parameters_validator.rb +26 -0
- data/lib/might/sort_undefined_parameter.rb +15 -0
- data/lib/might/sort_value_validator.rb +43 -0
- data/lib/might/version.rb +4 -0
- data/might.gemspec +33 -0
- metadata +240 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'might'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/might.rb
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'might/sort_parameters_extractor'
|
2
|
+
require 'might/sort_parameters_validator'
|
3
|
+
require 'might/filter_parameters_extractor'
|
4
|
+
require 'might/filter_parameters_validator'
|
5
|
+
require 'might/pagination_parameters_validator'
|
6
|
+
|
7
|
+
require 'might/filter_middleware'
|
8
|
+
require 'might/sort_middleware'
|
9
|
+
require 'might/pagination_middleware'
|
10
|
+
|
11
|
+
require 'might/result'
|
12
|
+
require 'uber/inheritable_attr'
|
13
|
+
require 'middleware'
|
14
|
+
|
15
|
+
#
|
16
|
+
module Might
|
17
|
+
# Configure your own fetcher
|
18
|
+
#
|
19
|
+
# PagesFetcher < Might::Fetcher
|
20
|
+
# self.resource_class = Page
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# You can configure filterable attributes for model
|
24
|
+
#
|
25
|
+
# filter :id, validates: { presence: true }
|
26
|
+
# filter :name
|
27
|
+
# filter :start_at, validates: { presence: true }
|
28
|
+
#
|
29
|
+
# If your property name doesn't match the name in the query string, use the :as option:
|
30
|
+
#
|
31
|
+
# filter :kind, as: :type
|
32
|
+
#
|
33
|
+
# So the Movie#kind property would be exposed to API as :type
|
34
|
+
#
|
35
|
+
# You may specify allowed sorting order:
|
36
|
+
#
|
37
|
+
# sort :id
|
38
|
+
# sort :name
|
39
|
+
#
|
40
|
+
# If your property name doesn't match the name in the query string, use the :as option:
|
41
|
+
#
|
42
|
+
# sort :position, as: :relevance
|
43
|
+
#
|
44
|
+
# So client should pass +?sort=relevance+ in order to sort by position
|
45
|
+
#
|
46
|
+
# It's also possible to reverse meaning of the order direction. For example it's not
|
47
|
+
# make sense to order by position from lower value to higher.
|
48
|
+
# The meaning default for that sorting is reversed order by default, so more relevant elenents
|
49
|
+
# would be the first.
|
50
|
+
#
|
51
|
+
# sort :position, as: :relevance, reverse_direction: true
|
52
|
+
#
|
53
|
+
class Fetcher
|
54
|
+
extend Uber::InheritableAttr
|
55
|
+
|
56
|
+
inheritable_attr :resource_class
|
57
|
+
inheritable_attr :filter_parameters_definition
|
58
|
+
inheritable_attr :sort_parameters_definition
|
59
|
+
inheritable_attr :middleware_changes
|
60
|
+
|
61
|
+
self.filter_parameters_definition = Set.new
|
62
|
+
self.sort_parameters_definition = Set.new
|
63
|
+
self.middleware_changes = []
|
64
|
+
|
65
|
+
# @return [Hash]
|
66
|
+
attr_reader :params
|
67
|
+
|
68
|
+
# @param params [Hash]
|
69
|
+
def initialize(params)
|
70
|
+
@params = params
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [ActiveRecord::Result] filtered and sorted collection
|
74
|
+
# @yieldparam collection [Result] if a block given
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# PagesFetcher.new(params).call #=> Result
|
78
|
+
#
|
79
|
+
# @example block syntax
|
80
|
+
# PagesFetcher.new(params) do |result|
|
81
|
+
# if result.success?
|
82
|
+
# result.get
|
83
|
+
# else
|
84
|
+
# result.errors
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
def call
|
89
|
+
processed_params, errors = process_params(params)
|
90
|
+
result = if errors.any?
|
91
|
+
Failure.new(errors)
|
92
|
+
else
|
93
|
+
processed_collection, = middleware.call([collection, processed_params])
|
94
|
+
Success.new(processed_collection)
|
95
|
+
end
|
96
|
+
|
97
|
+
if block_given?
|
98
|
+
yield result
|
99
|
+
else
|
100
|
+
result
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# @return [ActiveRecord::Relation]
|
107
|
+
def collection
|
108
|
+
self.class.resource_class.all
|
109
|
+
end
|
110
|
+
|
111
|
+
# Library middleware stack
|
112
|
+
# @return [Middleware::Builder]
|
113
|
+
def default_middleware
|
114
|
+
Middleware::Builder.new do |b|
|
115
|
+
b.use FilterMiddleware
|
116
|
+
b.use SortMiddleware
|
117
|
+
b.use PaginationMiddleware
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# User modified middleware stack
|
122
|
+
# @return [Middleware::Builder]
|
123
|
+
def middleware
|
124
|
+
default_middleware.tap do |builder|
|
125
|
+
self.class.middleware_changes.each do |change|
|
126
|
+
builder.instance_eval(&change)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [Hash, Array] tuple of parameters and processing errors
|
132
|
+
# this errors may be shown to front-end developer
|
133
|
+
def process_params(params)
|
134
|
+
Middleware::Builder.new do |b|
|
135
|
+
b.use FilterParametersExtractor, self.class.filter_parameters_definition
|
136
|
+
b.use FilterParametersValidator
|
137
|
+
b.use SortParametersExtractor, self.class.sort_parameters_definition
|
138
|
+
b.use SortParametersValidator
|
139
|
+
b.use PaginationParametersValidator
|
140
|
+
end.call([params, []])
|
141
|
+
end
|
142
|
+
|
143
|
+
class << self
|
144
|
+
# Alter middleware chain with the given block
|
145
|
+
# @param [Proc] change
|
146
|
+
# @return [Might]
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# class ChannelsFetcher
|
150
|
+
# middleware do
|
151
|
+
# use CustomMiddleware
|
152
|
+
# end
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
def middleware(&change)
|
156
|
+
fail ArgumentError unless block_given?
|
157
|
+
middleware_changes << change
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
# Register collection sorting by its name
|
162
|
+
# @param name [Symbol] of the field
|
163
|
+
# @return [void]
|
164
|
+
# @see +RansackableSort+ for details
|
165
|
+
#
|
166
|
+
def sort(name, options = {})
|
167
|
+
definition = SortParameterDefinition.new(name, options)
|
168
|
+
sort_parameters_definition.add(definition)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Register collection filter by its name
|
172
|
+
# @see +Might::Filter+ for details
|
173
|
+
#
|
174
|
+
# @overload filter(filter_name, options)
|
175
|
+
# @param [Symbol] filter_name
|
176
|
+
# @param [Hash] options
|
177
|
+
# @return [void]
|
178
|
+
# @example
|
179
|
+
# filter :genre_name, on: :resource
|
180
|
+
#
|
181
|
+
# @overload filter(filter_name: predicates, **options)
|
182
|
+
# @param [Symbol] filter_name
|
183
|
+
# @param [<Symbol>] predicates
|
184
|
+
# @param [Hash] other options options
|
185
|
+
# @return [void]
|
186
|
+
# @example
|
187
|
+
# filter genre_name: [:eq, :in], on: :resource
|
188
|
+
#
|
189
|
+
def filter(*args)
|
190
|
+
options = args.extract_options!
|
191
|
+
if args.empty?
|
192
|
+
filter_name = options.keys.first
|
193
|
+
predicates = options.values.first
|
194
|
+
options = options.except(filter_name).merge(predicates: predicates)
|
195
|
+
else
|
196
|
+
filter_name = args.first
|
197
|
+
end
|
198
|
+
|
199
|
+
definition = FilterParameterDefinition.new(filter_name, options)
|
200
|
+
filter_parameters_definition.add(definition)
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param params [Hash] user provided input
|
204
|
+
# @yieldparam collection [ActiveRecord::Relation]
|
205
|
+
def run(params, &block)
|
206
|
+
new(params).call(&block)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Add middleware to the end of middleware chane
|
210
|
+
#
|
211
|
+
# class MovieFetcher
|
212
|
+
# after do |scope, params|
|
213
|
+
# # do something with scope and params
|
214
|
+
# [scope.map(&:resource), params]
|
215
|
+
# end
|
216
|
+
# end
|
217
|
+
#
|
218
|
+
def after(&block)
|
219
|
+
alter_middleware(:use, &block)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Add middleware to the beginning of middleware chane
|
223
|
+
#
|
224
|
+
# class MovieFetcher
|
225
|
+
# before do |scope, params|
|
226
|
+
# # do something with scope and params
|
227
|
+
# [scope.map(&:resource), params]
|
228
|
+
# end
|
229
|
+
# end
|
230
|
+
#
|
231
|
+
def before(middleware_or_index = 0, &block)
|
232
|
+
alter_middleware(:insert_before, middleware_or_index, &block)
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def alter_middleware(method_name, *args, &block)
|
238
|
+
fail ArgumentError unless block_given?
|
239
|
+
middleware_changes.push lambda { |builder|
|
240
|
+
builder.send method_name, *args, lambda { |env|
|
241
|
+
scope, params = env
|
242
|
+
block.call(scope, params).tap do |r|
|
243
|
+
if !r.is_a?(Array) || r.size != 2
|
244
|
+
fail 'After block must return tuple of scope and params'
|
245
|
+
end
|
246
|
+
end
|
247
|
+
}
|
248
|
+
}
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'ransackable_filter'
|
2
|
+
require_relative 'ransackable_filter_parameters_adapter'
|
3
|
+
require 'middleware'
|
4
|
+
#
|
5
|
+
module Might
|
6
|
+
# Filter scope using ransack gem
|
7
|
+
#
|
8
|
+
class FilterMiddleware
|
9
|
+
# @param app [#call, Proc]
|
10
|
+
#
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
scope, = ::Middleware::Builder.new do |b|
|
17
|
+
b.use RansackableFilterParametersAdapter
|
18
|
+
b.use RansackableFilter
|
19
|
+
end.call(env)
|
20
|
+
|
21
|
+
app.call([scope, env[1]])
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :app
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
module Might
|
5
|
+
# User provided filtering on particular parameter
|
6
|
+
#
|
7
|
+
class FilterParameter
|
8
|
+
# @return [String, nil]
|
9
|
+
#
|
10
|
+
attr_reader :predicate
|
11
|
+
|
12
|
+
# @return [ParameterDefinition, nil]
|
13
|
+
#
|
14
|
+
attr_reader :definition
|
15
|
+
|
16
|
+
def initialize(value, predicate, definition)
|
17
|
+
@value = value
|
18
|
+
@predicate = predicate.to_s
|
19
|
+
@definition = definition
|
20
|
+
end
|
21
|
+
|
22
|
+
delegate :name, to: :definition
|
23
|
+
delegate :on, to: :definition
|
24
|
+
|
25
|
+
# Check if this parameter provided by user.
|
26
|
+
# If the parameter defined by not given by user this returns false.
|
27
|
+
# @return [Boolean]
|
28
|
+
def provided?
|
29
|
+
value.present? && predicate.present?
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
is_a?(other.class) &&
|
34
|
+
value == other.value &&
|
35
|
+
predicate == other.predicate &&
|
36
|
+
definition == other.definition
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String, Array<String>]
|
40
|
+
# TODO: memoize
|
41
|
+
def value
|
42
|
+
if @value.is_a?(Array)
|
43
|
+
@value.map { |v| definition.coerce(v) }
|
44
|
+
else
|
45
|
+
definition.coerce(@value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?
|
50
|
+
validators.all?(&:valid?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def invalid?
|
54
|
+
validators.any?(&:invalid?)
|
55
|
+
end
|
56
|
+
|
57
|
+
def errors
|
58
|
+
validators.map(&:errors).flat_map(&:full_messages).compact.uniq
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def validators
|
64
|
+
values_for_validation = @value.is_a?(Array) ? @value : [@value]
|
65
|
+
@validators ||= values_for_validation.map { |v| definition.validator.new(v) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require_relative 'filter_predicates'
|
3
|
+
require_relative 'filter_value_validator'
|
4
|
+
|
5
|
+
module Might
|
6
|
+
# Filtering parameter definition
|
7
|
+
#
|
8
|
+
class FilterParameterDefinition
|
9
|
+
# If the property name doesn't match the name in the query string, use the :as option
|
10
|
+
# @return [String]
|
11
|
+
attr_reader :as
|
12
|
+
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# Association on which this parameter is defined.
|
17
|
+
# @return [String, nil]
|
18
|
+
attr_reader :on
|
19
|
+
|
20
|
+
# White-listed predicates
|
21
|
+
# @return [<String>]
|
22
|
+
attr_reader :predicates
|
23
|
+
|
24
|
+
# @param [String] name of the field
|
25
|
+
# @param [Hash] options
|
26
|
+
# @option options [Symbol, {Symbol => String}, {Symbol => <String>}] :on (nil) filter on given relation
|
27
|
+
# @option options [<String>] :predicates (ALL_PREDICATES) white-listed predicates
|
28
|
+
# @option options [Proc] :coerce coercion for the value
|
29
|
+
# @option options [Hash{Symbol => any}] :validates for the value
|
30
|
+
# @example
|
31
|
+
# validates: presence: true, length: { is: 6 }
|
32
|
+
#
|
33
|
+
def initialize(name, options = {})
|
34
|
+
options.assert_valid_keys(:as, :on, :predicates, :coerce, :validates)
|
35
|
+
@as = options.fetch(:as, name).to_s
|
36
|
+
@name = name.to_s
|
37
|
+
@on = options[:on]
|
38
|
+
@predicates = Array(options.fetch(:predicates, FilterPredicates::ALL)).map(&:to_s)
|
39
|
+
@coerce = options.fetch(:coerce, ->(v) { v })
|
40
|
+
@validations = options.fetch(:validates, {})
|
41
|
+
end
|
42
|
+
|
43
|
+
# If two parameters have the same name, they are equal.
|
44
|
+
delegate :hash, to: :name
|
45
|
+
|
46
|
+
def eql?(other)
|
47
|
+
other.is_a?(self.class) && other.name == name
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
other.is_a?(self.class) &&
|
52
|
+
other.name == name &&
|
53
|
+
other.as == as &&
|
54
|
+
other.on == on &&
|
55
|
+
other.predicates == predicates
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [any] value
|
59
|
+
# @return [any] coerced value according the definition
|
60
|
+
#
|
61
|
+
def coerce(value)
|
62
|
+
@coerce.call(value)
|
63
|
+
end
|
64
|
+
|
65
|
+
def validator
|
66
|
+
FilterValueValidator.build(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
def defined?
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def undefined?
|
74
|
+
!self.defined?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Proc with defined validations
|
78
|
+
# @return [Proc]
|
79
|
+
# @api private
|
80
|
+
attr_reader :validations
|
81
|
+
end
|
82
|
+
end
|