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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.hound.yml +2 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +19 -0
  6. data/.travis.yml +14 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE +202 -0
  9. data/README.md +301 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +7 -0
  13. data/config/locales/en.yml +7 -0
  14. data/lib/might.rb +4 -0
  15. data/lib/might/fetcher.rb +252 -0
  16. data/lib/might/fetcher_error.rb +4 -0
  17. data/lib/might/filter_middleware.rb +28 -0
  18. data/lib/might/filter_parameter.rb +68 -0
  19. data/lib/might/filter_parameter_definition.rb +82 -0
  20. data/lib/might/filter_parameters_extractor.rb +78 -0
  21. data/lib/might/filter_parameters_validator.rb +26 -0
  22. data/lib/might/filter_predicates.rb +26 -0
  23. data/lib/might/filter_undefined_parameter.rb +15 -0
  24. data/lib/might/filter_value_validator.rb +43 -0
  25. data/lib/might/pagination_middleware.rb +60 -0
  26. data/lib/might/pagination_parameters_validator.rb +55 -0
  27. data/lib/might/paginator.rb +58 -0
  28. data/lib/might/railtie.rb +8 -0
  29. data/lib/might/ransackable_filter.rb +24 -0
  30. data/lib/might/ransackable_filter_parameters_adapter.rb +61 -0
  31. data/lib/might/ransackable_sort.rb +27 -0
  32. data/lib/might/ransackable_sort_parameters_adapter.rb +23 -0
  33. data/lib/might/result.rb +68 -0
  34. data/lib/might/sort_middleware.rb +31 -0
  35. data/lib/might/sort_parameter.rb +54 -0
  36. data/lib/might/sort_parameter_definition.rb +54 -0
  37. data/lib/might/sort_parameters_extractor.rb +62 -0
  38. data/lib/might/sort_parameters_validator.rb +26 -0
  39. data/lib/might/sort_undefined_parameter.rb +15 -0
  40. data/lib/might/sort_value_validator.rb +43 -0
  41. data/lib/might/version.rb +4 -0
  42. data/might.gemspec +33 -0
  43. metadata +240 -0
@@ -0,0 +1,27 @@
1
+ module Might
2
+ # Sort scope
3
+ class RansackableSort
4
+ # @param app [#call]
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ # @param env [<ActiveRecord::Relation, <String>]
10
+ # * first element is a scope to be sorted
11
+ # * second is a array with user provided sortings
12
+ # @return [<ActiveRecord::Relation, <String>]
13
+ #
14
+ def call(env)
15
+ scope, params = env
16
+
17
+ ransackable_query = scope.ransack
18
+ ransackable_query.sorts = params[:sort]
19
+
20
+ app.call([ransackable_query.result, params])
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :app
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Might
2
+ # Converts array of parameters to hash familiar to ransack gem
3
+ #
4
+ class RansackableSortParametersAdapter
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ scope, params = env
11
+
12
+ ransackable_parameters = Array(params[:sort]).map do |parameter|
13
+ "#{parameter.name} #{parameter.direction}"
14
+ end
15
+
16
+ app.call([scope, params.merge(sort: ransackable_parameters)])
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :app
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ module Might
2
+ # Marker module
3
+ module Result
4
+ end
5
+
6
+ # Represents fetching failure
7
+ class Failure
8
+ include Result
9
+
10
+ # @param errors [<String>]
11
+ def initialize(errors)
12
+ @errors = errors
13
+ end
14
+
15
+ # @param errors [<String>]
16
+ attr_reader :errors
17
+
18
+ # @return [true]
19
+ def failure?
20
+ !success?
21
+ end
22
+
23
+ # @return [false]
24
+ def success?
25
+ false
26
+ end
27
+
28
+ # @raise [NotImplementedError]
29
+ def get
30
+ fail NotImplementedError
31
+ end
32
+
33
+ # @yield given block
34
+ def get_or_else
35
+ yield
36
+ end
37
+ end
38
+
39
+ # Represents fetching success
40
+ class Success
41
+ include Result
42
+
43
+ # @param value [ActiveRecord::Relation]
44
+ def initialize(value)
45
+ @value = value
46
+ end
47
+
48
+ # @return [false]
49
+ def failure?
50
+ !success?
51
+ end
52
+
53
+ # @return [true]
54
+ def success?
55
+ true
56
+ end
57
+
58
+ # @return [ActiveRecord::Relation]
59
+ def get
60
+ @value
61
+ end
62
+
63
+ # @return [ActiveRecord::Relation]
64
+ def get_or_else
65
+ @value
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,31 @@
1
+ require 'might/ransackable_sort_parameters_adapter'
2
+ require 'might/ransackable_sort'
3
+ require 'might/sort_parameters_extractor'
4
+ require 'middleware'
5
+
6
+ module Might
7
+ # Sort scope using ransack gem
8
+ #
9
+ class SortMiddleware
10
+ # @param app [#call]
11
+ #
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ attr_reader :app
17
+
18
+ # @param [Array(ActiveRecord::Relation, Hash)] env
19
+ # First argument is a ActiveRecord relation which must be sorted
20
+ # Second argument is a request parameters provided by user
21
+ #
22
+ def call(env)
23
+ scope, = ::Middleware::Builder.new do |b|
24
+ b.use RansackableSortParametersAdapter
25
+ b.use RansackableSort
26
+ end.call(env)
27
+
28
+ app.call([scope, env[1]])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Might
4
+ # User provided filtering on particular parameter
5
+ #
6
+ class SortParameter
7
+ DIRECTIONS = %w(asc desc).freeze
8
+ REVERSED_DIRECTIONS = {
9
+ 'asc' => 'desc',
10
+ 'desc' => 'asc'
11
+ }.freeze
12
+
13
+ attr_reader :direction
14
+
15
+ # @return [ParameterDefinition]
16
+ #
17
+ attr_reader :definition
18
+
19
+ # @param ['asc', desc'] direction
20
+ # @param [SortParameterDefinition]
21
+ #
22
+ def initialize(direction, definition)
23
+ @direction = direction.to_s
24
+ fail ArgumentError unless DIRECTIONS.include?(@direction)
25
+ @definition = definition
26
+ @validator = definition.validator
27
+ end
28
+
29
+ attr_reader :validator
30
+ delegate :name, to: :definition
31
+ delegate :valid?, to: :validator
32
+ delegate :invalid?, to: :validator
33
+
34
+ def errors
35
+ validator.errors.full_messages
36
+ end
37
+
38
+ def ==(other)
39
+ is_a?(other.class) &&
40
+ direction == other.direction &&
41
+ definition == other.definition
42
+ end
43
+
44
+ # @return ['asc', desc']
45
+ #
46
+ def direction
47
+ if definition.reverse_direction?
48
+ REVERSED_DIRECTIONS[@direction]
49
+ else
50
+ @direction
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'sort_value_validator'
2
+
3
+ module Might
4
+ # Sorting parameter definition
5
+ #
6
+ class SortParameterDefinition
7
+ # @return [String]
8
+ attr_reader :name
9
+
10
+ # If the property name doesn't match the name in the query string, use the :as option
11
+ # @return [String]
12
+ attr_reader :as
13
+
14
+ # @return [Boolean]
15
+ attr_reader :reverse_direction
16
+
17
+ # @param [String] name of the field
18
+ # @param [String] as (#name) alias for the property
19
+ # @param [Boolean] reverse_direction (false) default sorting direction
20
+ #
21
+ def initialize(name, as: name, reverse_direction: false)
22
+ @name = name.to_s
23
+ @as = as.to_s
24
+ @reverse_direction = reverse_direction
25
+ end
26
+
27
+ # If two parameters have the same name, they are equal.
28
+ delegate :hash, to: :name
29
+
30
+ def eql?(other)
31
+ other.is_a?(self.class) && other.name == name
32
+ end
33
+
34
+ def ==(other)
35
+ other.is_a?(self.class) &&
36
+ other.name == name &&
37
+ other.as == as
38
+ end
39
+
40
+ alias_method :reverse_direction?, :reverse_direction
41
+
42
+ def validator
43
+ SortValueValidator.build(self).new
44
+ end
45
+
46
+ def defined?
47
+ true
48
+ end
49
+
50
+ def undefined?
51
+ !self.defined?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,62 @@
1
+ require 'might/sort_undefined_parameter'
2
+ require 'might/sort_parameter'
3
+
4
+ module Might
5
+ # User provided sorting syntax:
6
+ # * `name` - sort by name
7
+ # * `-name` - sort by name in reversed order
8
+ # * `-name,created_at` - sort by name in reversed order, and then sort by created_at
9
+ #
10
+ # This middleware parses sorting string and builds Parameter array
11
+ # @see Might::Sort::Parameter
12
+ #
13
+ # If user passes not defined sort order, it yields to `UndefinedParameter`, so you may
14
+ # validate it.
15
+ #
16
+ class SortParametersExtractor
17
+ # @param app [#call]
18
+ # @param parameters_definition [Set<Might::SortParameterDefinition>]
19
+ def initialize(app, parameters_definition)
20
+ @app = app
21
+ @parameters_definition = parameters_definition
22
+ end
23
+
24
+ # @param env [<String, []>]
25
+ # * first element is a scope to be sorted
26
+ # * second is a String with user provided sortings
27
+ # @return [<<Might::RansackableSort::SortParameter, []>]
28
+ #
29
+ def call(env)
30
+ params, errors = env
31
+
32
+ sort_params = sort_order(params[:sort]).map do |(attribute, direction)|
33
+ extract_parameter(attribute, direction)
34
+ end
35
+
36
+ app.call([params.merge(sort: sort_params), errors])
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :parameters_definition, :app
42
+
43
+ def extract_parameter(name, direction)
44
+ definition = parameters_definition.detect { |d| d.as == name } || SortUndefinedParameter.new(name)
45
+ SortParameter.new(direction, definition)
46
+ end
47
+
48
+ def sort_order(params)
49
+ String(params).split(',').map do |attribute|
50
+ sorting_for(attribute)
51
+ end
52
+ end
53
+
54
+ def sorting_for(field)
55
+ if field.start_with?('-')
56
+ [field.delete('-'), 'desc']
57
+ else
58
+ [field, 'asc']
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,26 @@
1
+ module Might
2
+ # Validates sortings and raises error if one of them is invalid
3
+ #
4
+ class SortParametersValidator
5
+ # @param app [#call]
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ # @param env [<{:sort => Might::FilterParameter}, Array>]
11
+ # @return [<{:sort => Might::FilterParameter}, Array>]
12
+ #
13
+ def call(env)
14
+ params, errors = env
15
+
16
+ not_allowed_parameters = Array(params[:sort]).select(&:invalid?)
17
+ messages = not_allowed_parameters.flat_map(&:errors)
18
+
19
+ app.call([params, errors.concat(messages)])
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :app
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ require 'might/sort_parameter_definition'
2
+
3
+ module Might
4
+ # Null object for ParameterDefinition
5
+ #
6
+ class SortUndefinedParameter < SortParameterDefinition
7
+ def required?
8
+ false
9
+ end
10
+
11
+ def defined?
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'active_model/validator'
4
+ require 'active_model/validations'
5
+ require 'active_model/callbacks'
6
+ require 'active_model/naming'
7
+ require 'active_model/translation'
8
+ require 'active_model/errors'
9
+
10
+ module Might
11
+ # Build singleton validation class for specified attribute name
12
+ # @example you need a nice validator for a first_name
13
+ # validator_klass = ValueValidator.build('first_name', presence: true, length: { minimum: 3 })
14
+ # validator = validator_klass.new('Bo')
15
+ # validator.valid? #=> false
16
+ # validator.errors.full_messages #=> ['First name is too short (minimum is 3 characters)']
17
+ #
18
+ module SortValueValidator
19
+ module_function
20
+
21
+ # Validates if Parameter is undefined or not
22
+ class DefinedValidator < ActiveModel::EachValidator
23
+ def validate_each(record, attribute, _value)
24
+ record.errors.add(attribute, :undefined_sort_order) if record.undefined?
25
+ end
26
+ end
27
+
28
+ def build(definition)
29
+ Class.new do
30
+ include ActiveModel::Validations
31
+
32
+ validates(definition.name, 'might/sort_value_validator/defined': true)
33
+
34
+ define_method(:undefined?) { definition.undefined? }
35
+ define_method(definition.name) {}
36
+
37
+ def self.model_name
38
+ ActiveModel::Name.new(Might, nil, 'Might')
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ #
2
+ module Might
3
+ VERSION = '0.3.0'
4
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'might/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'might'
8
+ spec.version = Might::VERSION
9
+ spec.authors = ['Tema Bolshakov']
10
+ spec.email = ['abolshakov@spbtv.com']
11
+
12
+ spec.summary = 'Mighty resource fetchers'
13
+ spec.description = 'Mighty resource fetchers build on top of Ransack gem'
14
+ spec.homepage = 'https://github.com/SPBTV/might'
15
+ spec.license = 'Apache 2.0'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_runtime_dependency 'activesupport', '>= 3.2'
23
+ spec.add_runtime_dependency 'activemodel', '>= 3.2'
24
+ spec.add_runtime_dependency 'uber', '~> 0.0.15'
25
+ spec.add_runtime_dependency 'ibsciss-middleware', '~> 0.3'
26
+ spec.add_runtime_dependency 'ransack', '~> 1.6.6'
27
+ spec.add_development_dependency 'activerecord', '>= 3.2'
28
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
29
+ spec.add_development_dependency 'bundler', '~> 1.10'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.3'
32
+ spec.add_development_dependency 'rubocop', '~> 0.34.2'
33
+ end