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
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
#
|
4
|
+
class PaginationParametersValidator
|
5
|
+
# Validates pagination parameters
|
6
|
+
# @param [Hash, nil] page
|
7
|
+
# @option page [Number, nil] :limit
|
8
|
+
# @option page [Number, nil] :offset
|
9
|
+
#
|
10
|
+
Validator = Struct.new(:page) do
|
11
|
+
include ActiveModel::Validations
|
12
|
+
|
13
|
+
validate :page_is_a_hash
|
14
|
+
validates :limit, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
15
|
+
validates :offset, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
16
|
+
|
17
|
+
def page_is_a_hash
|
18
|
+
return if page.is_a?(Hash) || page.nil?
|
19
|
+
errors.add(:page, :invalid_page_type)
|
20
|
+
end
|
21
|
+
|
22
|
+
def limit
|
23
|
+
page[:limit] if page.is_a?(Hash)
|
24
|
+
end
|
25
|
+
|
26
|
+
def offset
|
27
|
+
page[:offset] if page.is_a?(Hash)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(app)
|
32
|
+
@app = app
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(env)
|
36
|
+
params, errors = env
|
37
|
+
|
38
|
+
validator = Validator.new(params[:page]).tap(&:validate)
|
39
|
+
messages = validator.errors.full_messages
|
40
|
+
|
41
|
+
app.call([params, errors.concat(messages)])
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :app
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Paginates ActiveRecord scopes
|
4
|
+
# @example
|
5
|
+
#
|
6
|
+
# Paginator.new(limit: 10, offset: 100).paginate(Movie.all)
|
7
|
+
#
|
8
|
+
# As a side effect of pagination it defines the following methods on collection:
|
9
|
+
# collection#limit
|
10
|
+
# collection#offset
|
11
|
+
# collection#count
|
12
|
+
# collection#total_count
|
13
|
+
#
|
14
|
+
class Paginator
|
15
|
+
InvalidLimitOrOffset = Class.new(StandardError)
|
16
|
+
|
17
|
+
attr_reader :limit, :offset
|
18
|
+
|
19
|
+
# @param [{Symbol => Integer}] params
|
20
|
+
# @option params [Integer] :limit
|
21
|
+
# @option params [Integer] :offset
|
22
|
+
#
|
23
|
+
def initialize(options = {})
|
24
|
+
@limit = Integer(options.fetch(:limit))
|
25
|
+
@offset = Integer(options.fetch(:offset))
|
26
|
+
|
27
|
+
fail InvalidLimitOrOffset if @limit < 0 || @offset < 0
|
28
|
+
end
|
29
|
+
|
30
|
+
# Paginate given collection
|
31
|
+
# @param [ActiveRecord::CollectionProxy, ActiveRecord::Base] collection
|
32
|
+
# @return [ActiveRecord::CollectionProxy]
|
33
|
+
#
|
34
|
+
def paginate(collection)
|
35
|
+
collection.offset(offset).limit(limit)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
#
|
4
|
+
class RansackableFilter
|
5
|
+
# @param app [#call]
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param env [<ActiveRecord::Relation, Hash]
|
11
|
+
# * first element is a scope to be filtered
|
12
|
+
# * second is a hash with user provided filters
|
13
|
+
# @return [<ActiveRecord::Relation, Hash]
|
14
|
+
#
|
15
|
+
def call(env)
|
16
|
+
scope, params = env
|
17
|
+
filtered_scope = scope.ransack(params[:filter]).result
|
18
|
+
app.call([filtered_scope, params])
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :app
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Converts array of parameters to hash familiar to ransack gem
|
4
|
+
#
|
5
|
+
class RansackableFilterParametersAdapter
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
scope, params = env
|
12
|
+
|
13
|
+
ransackable_parameters = Array(params[:filter]).reject { |f| f.predicate.nil? }
|
14
|
+
.each_with_object({}) do |filter, ransackable_filters|
|
15
|
+
ransackable_filters[canonical_name_for(filter)] = filter.value
|
16
|
+
end
|
17
|
+
|
18
|
+
app.call([scope, params.merge(filter: ransackable_parameters)])
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :app
|
24
|
+
|
25
|
+
# @return [String, nil] filter with predicate. E.g. `first_name_eq`
|
26
|
+
# nil value means that
|
27
|
+
#
|
28
|
+
def canonical_name_for(filter)
|
29
|
+
if filter.on.is_a?(Hash)
|
30
|
+
name_for_polymorphic(filter)
|
31
|
+
else
|
32
|
+
[filter.on, filter.name, filter.predicate].compact.join('_')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Build query method for polymorphic association
|
37
|
+
# @see https://github.com/activerecord-hackery/ransack/wiki/Polymorphic-searches
|
38
|
+
# @see https://github.com/activerecord-hackery/ransack/commit/c156fa4a7ac6e1a8d730791c49bf4403aa0f3af7#diff-a26803e1ff6e56eb67b80c91d79a063eR34
|
39
|
+
# @param [SortParameter] filter
|
40
|
+
# @return [String]
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
#
|
44
|
+
# definition = ParameterDefinition.new('genre_name', on: { resource: ['Movie', 'Channel'] })
|
45
|
+
# parameter = Parameter.new('Horror', 'eq', definition)
|
46
|
+
# name_for_polymorphic(parameter)
|
47
|
+
# #=> 'resource_of_Movie_type_genre_name_or_resource_of_Channel_type_genre_name_eq'
|
48
|
+
#
|
49
|
+
def name_for_polymorphic(filter)
|
50
|
+
unless filter.on.size == 1
|
51
|
+
fail ArgumentError, 'Polymorphic association must be defined as Hash with single value'
|
52
|
+
end
|
53
|
+
polymorphic_name = filter.on.keys.first
|
54
|
+
|
55
|
+
name = Array(filter.on.values.first)
|
56
|
+
.map { |polymorphic_type| "#{polymorphic_name}_of_#{polymorphic_type}_type_#{filter.name}" }
|
57
|
+
.join('_or_')
|
58
|
+
|
59
|
+
"#{name}_#{filter.predicate}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Sort scope
|
4
|
+
class RansackableSort
|
5
|
+
# @param app [#call]
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param env [<ActiveRecord::Relation, <String>]
|
11
|
+
# * first element is a scope to be sorted
|
12
|
+
# * second is a array with user provided sortings
|
13
|
+
# @return [<ActiveRecord::Relation, <String>]
|
14
|
+
#
|
15
|
+
def call(env)
|
16
|
+
scope, params = env
|
17
|
+
|
18
|
+
ransackable_query = scope.ransack
|
19
|
+
ransackable_query.sorts = params[:sort]
|
20
|
+
|
21
|
+
app.call([ransackable_query.result, params])
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :app
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Converts array of parameters to hash familiar to ransack gem
|
4
|
+
#
|
5
|
+
class RansackableSortParametersAdapter
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
scope, params = env
|
12
|
+
|
13
|
+
ransackable_parameters = Array(params[:sort]).map do |parameter|
|
14
|
+
"#{parameter.name} #{parameter.direction}"
|
15
|
+
end
|
16
|
+
|
17
|
+
app.call([scope, params.merge(sort: ransackable_parameters)])
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :app
|
23
|
+
end
|
24
|
+
end
|
data/lib/right/result.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Marker module
|
4
|
+
module Result
|
5
|
+
end
|
6
|
+
|
7
|
+
# Represents fetching failure
|
8
|
+
class Failure
|
9
|
+
include Result
|
10
|
+
|
11
|
+
# @param errors [<String>]
|
12
|
+
def initialize(errors)
|
13
|
+
@errors = errors
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param errors [<String>]
|
17
|
+
attr_reader :errors
|
18
|
+
|
19
|
+
# @return [true]
|
20
|
+
def failure?
|
21
|
+
!success?
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [false]
|
25
|
+
def success?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
# @raise [NotImplementedError]
|
30
|
+
def get
|
31
|
+
fail NotImplementedError, <<-MESSAGE.strip_heredoc
|
32
|
+
#{self.class} does not respond to #get. You should explicitly check for
|
33
|
+
#success? before calling #get.
|
34
|
+
MESSAGE
|
35
|
+
end
|
36
|
+
|
37
|
+
# @yield given block
|
38
|
+
def get_or_else
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Represents fetching success
|
44
|
+
class Success
|
45
|
+
include Result
|
46
|
+
|
47
|
+
# @param value [ActiveRecord::Relation]
|
48
|
+
def initialize(value)
|
49
|
+
@value = value
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [false]
|
53
|
+
def failure?
|
54
|
+
!success?
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [true]
|
58
|
+
def success?
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [ActiveRecord::Relation]
|
63
|
+
def get
|
64
|
+
@value
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [ActiveRecord::Relation]
|
68
|
+
def get_or_else
|
69
|
+
@value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Sort scope using ransack gem
|
4
|
+
#
|
5
|
+
class SortMiddleware
|
6
|
+
# @param app [#call]
|
7
|
+
#
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :app
|
13
|
+
|
14
|
+
# @param [Array(ActiveRecord::Relation, Hash)] env
|
15
|
+
# First argument is a ActiveRecord relation which must be sorted
|
16
|
+
# Second argument is a request parameters provided by user
|
17
|
+
#
|
18
|
+
def call(env)
|
19
|
+
scope, = ::Middleware::Builder.new do |b|
|
20
|
+
b.use RansackableSortParametersAdapter
|
21
|
+
b.use RansackableSort
|
22
|
+
end.call(env)
|
23
|
+
|
24
|
+
app.call([scope, env[1]])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# User provided filtering on particular parameter
|
4
|
+
#
|
5
|
+
class SortParameter
|
6
|
+
DIRECTIONS = %w(asc desc).freeze
|
7
|
+
REVERSED_DIRECTIONS = {
|
8
|
+
'asc' => 'desc',
|
9
|
+
'desc' => 'asc',
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
attr_reader :direction
|
13
|
+
|
14
|
+
# @return [ParameterDefinition]
|
15
|
+
#
|
16
|
+
attr_reader :definition
|
17
|
+
|
18
|
+
# @param ['asc', desc'] direction
|
19
|
+
# @param [SortParameterDefinition]
|
20
|
+
#
|
21
|
+
def initialize(direction, definition)
|
22
|
+
@direction = direction.to_s
|
23
|
+
fail ArgumentError unless DIRECTIONS.include?(@direction)
|
24
|
+
@definition = definition
|
25
|
+
@validator = definition.validator
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :validator
|
29
|
+
delegate :name, to: :definition
|
30
|
+
delegate :valid?, to: :validator
|
31
|
+
delegate :invalid?, to: :validator
|
32
|
+
|
33
|
+
def errors
|
34
|
+
validator.errors.full_messages
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
is_a?(other.class) &&
|
39
|
+
direction == other.direction &&
|
40
|
+
definition == other.definition
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return ['asc', desc']
|
44
|
+
#
|
45
|
+
def direction
|
46
|
+
if definition.reverse_direction?
|
47
|
+
REVERSED_DIRECTIONS[@direction]
|
48
|
+
else
|
49
|
+
@direction
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Right
|
3
|
+
# Sorting parameter definition
|
4
|
+
#
|
5
|
+
class SortParameterDefinition
|
6
|
+
# @return [String]
|
7
|
+
attr_reader :name
|
8
|
+
|
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 [Boolean]
|
14
|
+
attr_reader :reverse_direction
|
15
|
+
|
16
|
+
# @param [String] name of the field
|
17
|
+
# @param [String] as (#name) alias for the property
|
18
|
+
# @param [Boolean] reverse_direction (false) default sorting direction
|
19
|
+
#
|
20
|
+
def initialize(name, as: name, reverse_direction: false)
|
21
|
+
@name = name.to_s
|
22
|
+
@as = as.to_s
|
23
|
+
@reverse_direction = reverse_direction
|
24
|
+
end
|
25
|
+
|
26
|
+
# If two parameters have the same name, they are equal.
|
27
|
+
delegate :hash, to: :name
|
28
|
+
|
29
|
+
def eql?(other)
|
30
|
+
other.is_a?(self.class) && other.name == name
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
other.is_a?(self.class) &&
|
35
|
+
other.name == name &&
|
36
|
+
other.as == as
|
37
|
+
end
|
38
|
+
|
39
|
+
alias reverse_direction? reverse_direction
|
40
|
+
|
41
|
+
def validator
|
42
|
+
SortValueValidator.build(self).new
|
43
|
+
end
|
44
|
+
|
45
|
+
def defined?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def undefined?
|
50
|
+
!self.defined?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|