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.
- 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
|