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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +1 -0
- data/README.md +37 -16
- data/gemfiles/rails_4.gemfile +1 -0
- data/gemfiles/rails_5.gemfile +1 -0
- data/lib/rails_stuff/engine.rb +1 -1
- data/lib/rails_stuff/resources_controller/actions.rb +3 -3
- data/lib/rails_stuff/resources_controller/basic_helpers.rb +1 -1
- data/lib/rails_stuff/resources_controller/resource_helper.rb +32 -14
- data/lib/rails_stuff/resources_controller.rb +0 -2
- data/lib/rails_stuff/responders/turbolinks.rb +13 -0
- data/lib/rails_stuff/responders.rb +19 -0
- data/lib/rails_stuff/rspec_helpers/concurrency.rb +45 -0
- data/lib/rails_stuff/rspec_helpers/groups/feature.rb +25 -0
- data/lib/rails_stuff/rspec_helpers/groups/request.rb +79 -0
- data/lib/rails_stuff/rspec_helpers/matchers/be_valid_js.rb +12 -0
- data/lib/rails_stuff/rspec_helpers/matchers/redirect_with_turbolinks.rb +53 -0
- data/lib/rails_stuff/rspec_helpers/signinable.rb +21 -0
- data/lib/rails_stuff/rspec_helpers.rb +123 -0
- data/lib/rails_stuff/sort_scope.rb +19 -0
- data/lib/rails_stuff/statusable/builder.rb +114 -0
- data/lib/rails_stuff/statusable/helper.rb +64 -0
- data/lib/rails_stuff/statusable/mapped_builder.rb +48 -0
- data/lib/rails_stuff/statusable/mapped_helper.rb +46 -0
- data/lib/rails_stuff/statusable.rb +53 -199
- data/lib/rails_stuff/test_helpers/concurrency.rb +1 -32
- data/lib/rails_stuff/test_helpers/integration_session.rb +18 -0
- data/lib/rails_stuff/test_helpers/response.rb +10 -0
- data/lib/rails_stuff/test_helpers.rb +50 -0
- data/lib/rails_stuff/types_tracker.rb +1 -1
- data/lib/rails_stuff/version.rb +13 -3
- data/lib/rails_stuff.rb +3 -0
- metadata +20 -8
- data/lib/rails_stuff/resources_controller/responder.rb +0 -21
- data/lib/rails_stuff/test_helpers/configurator.rb +0 -60
- 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
|