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,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,8 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/railtie'
3
+
4
+ module Right
5
+ # Load railtie to get translations
6
+ class Railtie < Rails::Railtie
7
+ end
8
+ 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
@@ -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