rails_stuff 0.5.1 → 0.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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