rails_stuff 0.5.1 → 0.6.0.rc1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Gemfile +1 -0
  5. data/README.md +37 -16
  6. data/gemfiles/rails_4.gemfile +1 -0
  7. data/gemfiles/rails_5.gemfile +1 -0
  8. data/lib/rails_stuff/engine.rb +1 -1
  9. data/lib/rails_stuff/resources_controller/actions.rb +3 -3
  10. data/lib/rails_stuff/resources_controller/basic_helpers.rb +1 -1
  11. data/lib/rails_stuff/resources_controller/resource_helper.rb +32 -14
  12. data/lib/rails_stuff/resources_controller.rb +0 -2
  13. data/lib/rails_stuff/responders/turbolinks.rb +13 -0
  14. data/lib/rails_stuff/responders.rb +19 -0
  15. data/lib/rails_stuff/rspec_helpers/concurrency.rb +45 -0
  16. data/lib/rails_stuff/rspec_helpers/groups/feature.rb +25 -0
  17. data/lib/rails_stuff/rspec_helpers/groups/request.rb +79 -0
  18. data/lib/rails_stuff/rspec_helpers/matchers/be_valid_js.rb +12 -0
  19. data/lib/rails_stuff/rspec_helpers/matchers/redirect_with_turbolinks.rb +53 -0
  20. data/lib/rails_stuff/rspec_helpers/signinable.rb +21 -0
  21. data/lib/rails_stuff/rspec_helpers.rb +123 -0
  22. data/lib/rails_stuff/sort_scope.rb +19 -0
  23. data/lib/rails_stuff/statusable/builder.rb +114 -0
  24. data/lib/rails_stuff/statusable/helper.rb +64 -0
  25. data/lib/rails_stuff/statusable/mapped_builder.rb +48 -0
  26. data/lib/rails_stuff/statusable/mapped_helper.rb +46 -0
  27. data/lib/rails_stuff/statusable.rb +53 -199
  28. data/lib/rails_stuff/test_helpers/concurrency.rb +1 -32
  29. data/lib/rails_stuff/test_helpers/integration_session.rb +18 -0
  30. data/lib/rails_stuff/test_helpers/response.rb +10 -0
  31. data/lib/rails_stuff/test_helpers.rb +50 -0
  32. data/lib/rails_stuff/types_tracker.rb +1 -1
  33. data/lib/rails_stuff/version.rb +13 -3
  34. data/lib/rails_stuff.rb +3 -0
  35. metadata +20 -8
  36. data/lib/rails_stuff/resources_controller/responder.rb +0 -21
  37. data/lib/rails_stuff/test_helpers/configurator.rb +0 -60
  38. data/lib/rails_stuff/test_helpers/rails.rb +0 -9
@@ -0,0 +1,123 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+ require 'active_support/dependencies/autoload'
3
+
4
+ module RailsStuff
5
+ # Collection of RSpec configurations and helpers for better experience.
6
+ module RSpecHelpers
7
+ autoload :Signinable, 'rails_stuff/rspec_helpers/signinable'
8
+
9
+ extend self
10
+
11
+ # Single endpoint for multiple seups. Use `:only` and `:except` options
12
+ # to filter actions.
13
+ def setup(only: nil, except: nil)
14
+ items = instance_methods.map(&:to_s) - %w(setup)
15
+ items -= Array.wrap(except).map(&:to_s) if except
16
+ if only
17
+ only = Array.wrap(only).map(&:to_s)
18
+ items &= only
19
+ items += only
20
+ end
21
+ items.uniq.each { |item| public_send(item) }
22
+ end
23
+
24
+ %w(
25
+ concurrency
26
+ groups/request
27
+ groups/feature
28
+ matchers/be_valid_js
29
+ matchers/redirect_with_turbolinks
30
+ ).each do |file|
31
+ define_method(file) { require "rails_stuff/rspec_helpers/#{file}" }
32
+ end
33
+
34
+ # Setup all TestHelpers.
35
+ def test_helpers
36
+ TestHelpers.setup
37
+ end
38
+
39
+ # Setups database cleaner to use strategy depending on metadata.
40
+ # By default it uses `:transaction` for all examples and `:truncation`
41
+ # for features and examples with `concurrent: true`.
42
+ #
43
+ # Other types can be tuned with `config.cleaner_strategy` hash &
44
+ # `config.cleaner_strategy.default`.
45
+ def database_cleaner # rubocop:disable AbcSize
46
+ return unless defined?(DatabaseCleaner)
47
+ ::RSpec.configure do |config|
48
+ if config.respond_to?(:use_transactional_fixtures=)
49
+ config.use_transactional_fixtures = false
50
+ end
51
+ config.add_setting :database_cleaner_strategy
52
+ config.database_cleaner_strategy = {feature: :truncation}
53
+ config.database_cleaner_strategy.default = :transaction
54
+ config.add_setting :database_cleaner_options
55
+ config.database_cleaner_options = {truncation: {except: %w(spatial_ref_sys)}}
56
+ config.add_setting :database_cleaner_args
57
+ config.database_cleaner_args = ->(ex) do
58
+ strategy = ex.metadata[:concurrent] && :truncation
59
+ strategy ||= config.database_cleaner_strategy[ex.metadata[:type]]
60
+ options = config.database_cleaner_options[strategy] || {}
61
+ [strategy, options]
62
+ end
63
+ config.around do |ex|
64
+ DatabaseCleaner.strategy = config.database_cleaner_args.call(ex)
65
+ DatabaseCleaner.cleaning { ex.run }
66
+ end
67
+ end
68
+ end
69
+
70
+ # Setups redis to flush db after suite and before each example with
71
+ # `flush_redis: :true`. `Rails.redis` client is used by default.
72
+ # Can be tuned with `config.redis`.
73
+ def redis
74
+ ::RSpec.configure do |config|
75
+ config.add_setting :redis
76
+ config.redis = Rails.redis if defined?(Rails.redis)
77
+ config.add_setting :flush_redis_proc
78
+ config.flush_redis_proc = ->(*) { Array.wrap(config.redis).each(&:flushdb) }
79
+ config.before(flush_redis: true) { instance_exec(&config.flush_redis_proc) }
80
+ config.after(:suite) { instance_exec(&config.flush_redis_proc) }
81
+ end
82
+ end
83
+
84
+ # Runs debugger after each failed example with `:debug` tag.
85
+ # Uses `pry` by default, this can be configured `config.debugger=`.
86
+ def debug
87
+ ::RSpec.configure do |config|
88
+ config.add_setting :debugger_proc
89
+ config.debugger_proc = ->(ex) do
90
+ exception = ex.exception
91
+ defined?(Pry) ? binding.pry : debugger # rubocop:disable Debugger
92
+ end
93
+ config.after(debug: true) do |ex|
94
+ instance_exec(ex, &config.debugger_proc) if ex.exception
95
+ end
96
+ end
97
+ end
98
+
99
+ # Clear logs `tail -f`-safely.
100
+ def clear_logs
101
+ ::RSpec.configure do |config|
102
+ config.add_setting :clear_log_file
103
+ config.clear_log_file = Rails.root.join('log', 'test.log') if defined?(Rails.root)
104
+ config.add_setting :clear_log_file_proc
105
+ config.clear_log_file_proc = ->(file) do
106
+ next unless file && File.exist?(file)
107
+ FileUtils.cp(file, "#{file}.last")
108
+ File.open(file, 'w').close
109
+ end
110
+ config.after(:suite) do
111
+ instance_exec(config.clear_log_file, &config.clear_log_file_proc) unless ENV['KEEP_LOG']
112
+ end
113
+ end
114
+ end
115
+
116
+ # Freeze time for specs with `:frozen_time` metadata.
117
+ def frozen_time
118
+ ::RSpec.configure do |config|
119
+ config.around(frozen_time: true) { |ex| Timecop.freeze { ex.run } }
120
+ end
121
+ end
122
+ end
123
+ end
@@ -30,6 +30,7 @@ module RailsStuff
30
30
  def has_sort_scope(config = {})
31
31
  @@_sort_scope_id ||= 0
32
32
  default = config[:default] || :id
33
+ default = default.is_a?(Hash) ? default.stringify_keys : default.to_s
33
34
  allowed = Array.wrap(config[:by]).map(&:to_s)
34
35
  only_actions = config.fetch(:only, :index)
35
36
  order_method = config.fetch(:order_method, :order)
@@ -42,12 +43,30 @@ module RailsStuff
42
43
  type: :any,
43
44
  ) do |c, scope, val|
44
45
  sort_args = SortScope.filter_param(val, c.params, allowed, default)
46
+ c.instance_variable_set(:@current_sort_scope, sort_args)
45
47
  scope.public_send(order_method, sort_args)
46
48
  end
47
49
  end
48
50
  # rubocop:enable ClassVars
49
51
 
52
+ # It does not use `ClassMethods` similar to `ActiveSupport::Concern`
53
+ # due to backward compatibility (SortScope extends controller class).
54
+ module InstanceMethods
55
+ protected
56
+
57
+ def current_sort_scope
58
+ @current_sort_scope ||= {}
59
+ end
60
+ end
61
+
50
62
  class << self
63
+ def extended(base)
64
+ base.class_eval do
65
+ include InstanceMethods
66
+ helper_method :current_sort_scope if respond_to?(:helper_method)
67
+ end
68
+ end
69
+
51
70
  # Filters value with whitelist of allowed fields to sort by.
52
71
  #
53
72
  # rubocop:disable CyclomaticComplexity, PerceivedComplexity, BlockNesting
@@ -0,0 +1,114 @@
1
+ module RailsStuff
2
+ module Statusable
3
+ # Basic builder for statuses list. Generates methods and scopes.
4
+ class Builder
5
+ attr_reader :helper, :options, :prefix, :suffix
6
+ delegate :model, :field, :list, to: :helper
7
+ delegate :define_scope, :define_method, :define_class_method, to: :helper
8
+
9
+ def initialize(helper, **options)
10
+ @helper = helper
11
+ @options = options
12
+ @prefix = options[:prefix]
13
+ @suffix = options[:suffix]
14
+ end
15
+
16
+ def generate
17
+ validations if options.fetch(:validate, true)
18
+ field_accessor
19
+ field_scope
20
+ value_scopes
21
+ value_accessors
22
+ translation_helpers
23
+ end
24
+
25
+ def validations
26
+ model.validates_inclusion_of field,
27
+ {in: valid_list}.merge!(options.fetch(:validate, {}))
28
+ end
29
+
30
+ # Field reader returns string, so we stringify list for validation.
31
+ def valid_list
32
+ list.map(&:to_s)
33
+ end
34
+
35
+ # Yields every status with it's database value into block.
36
+ def each_status
37
+ list.each { |x| yield x, x.to_s }
38
+ end
39
+
40
+ # Wraps status name with prefix and suffix.
41
+ def status_method_name(status)
42
+ "#{prefix}#{status}#{suffix}"
43
+ end
44
+
45
+ # Scope with given status. Useful for has_scope.
46
+ def field_scope
47
+ field = self.field
48
+ define_scope "with_#{field}", ->(status) { where(field => status) }
49
+ end
50
+
51
+ # Status accessors for every status.
52
+ def value_accessors
53
+ each_status do |status, value|
54
+ value_accessor status, value
55
+ end
56
+ end
57
+
58
+ # Scopes for every status.
59
+ def value_scopes
60
+ field = self.field
61
+ each_status do |status, value|
62
+ define_scope status_method_name(status), -> { where(field => value) }
63
+ define_scope "not_#{status_method_name(status)}", -> { where.not(field => value) }
64
+ end
65
+ end
66
+
67
+ # Generates methods for specific value.
68
+ def value_accessor(status, value)
69
+ field = self.field
70
+
71
+ # Shortcut to check status.
72
+ define_method "#{status_method_name(status)}?" do
73
+ # Access raw value, 'cause reader can be overriden.
74
+ self[field] == value
75
+ end
76
+
77
+ # Shortcut to update status.
78
+ define_method "#{status_method_name(status)}!" do
79
+ update!(field => value)
80
+ end
81
+ end
82
+
83
+ def field_accessor
84
+ field_reader
85
+ field_writer
86
+ end
87
+
88
+ # Make field accept sympbols.
89
+ def field_writer
90
+ define_method "#{field}=" do |val|
91
+ val = val.to_s if val.is_a?(Symbol)
92
+ super(val)
93
+ end
94
+ end
95
+
96
+ # Status as symbol.
97
+ def field_reader
98
+ field = self.field
99
+ define_method "#{field}_sym" do
100
+ val = self[field]
101
+ val && val.to_sym
102
+ end
103
+ end
104
+
105
+ def translation_helpers
106
+ field = self.field
107
+ define_method "#{field}_name" do
108
+ val = send(field)
109
+ self.class.t(".#{field}_name.#{val}") if val
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,64 @@
1
+ module RailsStuff
2
+ module Statusable
3
+ # Class to hold helper methods for statusable field.
4
+ #
5
+ # Order.has_status_field :status, %i(pending complete)
6
+ # Order.statuses.list # => %(pending complete)
7
+ # # ...
8
+ class Helper
9
+ attr_reader :model, :field, :list
10
+
11
+ def initialize(model, field, statuses)
12
+ @model = model
13
+ @field = field.freeze
14
+ @list = statuses.freeze
15
+ end
16
+
17
+ def translate(status)
18
+ model.t(".#{field}_name.#{status}") if status
19
+ end
20
+
21
+ alias_method :t, :translate
22
+
23
+ # Returns array compatible with select_options helper.
24
+ def select_options(only: nil, except: nil)
25
+ only ||= list
26
+ only -= except if except
27
+ only.map { |x| [translate(x), x] }
28
+ end
29
+
30
+ # Generate class method in model to access helper.
31
+ def attach(method_name = field.to_s.pluralize)
32
+ helper = self
33
+ define_class_method(method_name) { helper }
34
+ end
35
+
36
+ # Rails 4 doesn't use `instance_exec` for scopes, so we do it manually.
37
+ # For Rails 5 it's just use `.scope`.
38
+ def define_scope(name, body)
39
+ if RailsStuff.rails4?
40
+ model.singleton_class.send(:define_method, name) do |*args|
41
+ all.scoping { instance_exec(*args, &body) } || all
42
+ end
43
+ else
44
+ model.scope(name, body)
45
+ end
46
+ end
47
+
48
+ def define_method(method, &block)
49
+ methods_module.send(:define_method, method, &block)
50
+ end
51
+
52
+ def define_class_method(method, &block)
53
+ methods_module::ClassMethods.send(:define_method, method, &block)
54
+ end
55
+
56
+ protected
57
+
58
+ # Module to hold generated methods.
59
+ def methods_module
60
+ model.statusable_methods
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ module RailsStuff
2
+ module Statusable
3
+ # Generates methods and scopes when status names are mapped to internal values.
4
+ class MappedBuilder < Statusable::Builder
5
+ delegate :mapping, :inverse_mapping, to: :helper
6
+
7
+ # Field reader returns mapped value, so we don't need to stringify list.
8
+ alias_method :valid_list, :list
9
+
10
+ def each_status(&block)
11
+ mapping.each(&block)
12
+ end
13
+
14
+ # Scope with given status. Useful for has_scope.
15
+ def field_scope
16
+ field = self.field
17
+ helper = self.helper
18
+ define_scope "with_#{field}", ->(status) { where(field => helper.map(status)) }
19
+ end
20
+
21
+ def field_reader
22
+ field = self.field
23
+ helper = self.helper
24
+
25
+ # Returns status name.
26
+ define_method field do |original = false|
27
+ val = super()
28
+ original || !val ? val : helper.unmap(val)
29
+ end
30
+
31
+ # Status as symbol.
32
+ define_method "#{field}_sym" do
33
+ val = public_send(field)
34
+ val && val.to_sym
35
+ end
36
+ end
37
+
38
+ def field_writer
39
+ helper = self.helper
40
+ # Make field accept sympbols.
41
+ define_method "#{field}=" do |val|
42
+ val = val.to_s if val.is_a?(Symbol)
43
+ super(helper.map(val))
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ module RailsStuff
2
+ module Statusable
3
+ # Helper to hold
4
+ class MappedHelper < Helper
5
+ attr_reader :mapping, :inverse_mapping, :indifferent_mapping
6
+
7
+ def initialize(*)
8
+ super
9
+ @mapping = @list
10
+ @indifferent_mapping = mapping.with_indifferent_access
11
+ @list = mapping.keys.freeze
12
+ @inverse_mapping = mapping.invert.freeze
13
+ end
14
+
15
+ def select_options(original: false, only: nil, except: nil)
16
+ return super(only: only, except: except) unless original
17
+ only ||= mapping_values
18
+ only -= except if except
19
+ only.map { |x| [translate(inverse_mapping.fetch(x)), x] }
20
+ end
21
+
22
+ def mapping_values
23
+ @mapping_values ||= mapping.values
24
+ end
25
+
26
+ def map(val)
27
+ map_with(indifferent_mapping, val)
28
+ end
29
+
30
+ def unmap(val)
31
+ map_with(inverse_mapping, val)
32
+ end
33
+
34
+ protected
35
+
36
+ # Maps single value or array with given map.
37
+ def map_with(map, val)
38
+ if val.is_a?(Array)
39
+ val.map { |x| map.fetch(x, x) }
40
+ else
41
+ map.fetch(val, val)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end