interactor_support 1.0.1
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +285 -0
- data/Rakefile +51 -0
- data/interactor_support.gemspec +35 -0
- data/lib/interactor_support/actions.rb +12 -0
- data/lib/interactor_support/concerns/findable.rb +83 -0
- data/lib/interactor_support/concerns/skippable.rb +46 -0
- data/lib/interactor_support/concerns/transactionable.rb +21 -0
- data/lib/interactor_support/concerns/transformable.rb +65 -0
- data/lib/interactor_support/concerns/updatable.rb +57 -0
- data/lib/interactor_support/configuration.rb +18 -0
- data/lib/interactor_support/core.rb +10 -0
- data/lib/interactor_support/request_object.rb +86 -0
- data/lib/interactor_support/rubocop/cop/base_interactor_cop.rb +94 -0
- data/lib/interactor_support/rubocop/cop/require_required_for_interactor_support.rb +29 -0
- data/lib/interactor_support/rubocop/cop/unused_included_modules.rb +67 -0
- data/lib/interactor_support/rubocop/cop/used_unincluded_modules.rb +46 -0
- data/lib/interactor_support/rubocop.rb +2 -0
- data/lib/interactor_support/validations.rb +116 -0
- data/lib/interactor_support/version.rb +5 -0
- data/lib/interactor_support.rb +33 -0
- data/sig/interactor_support.rbs +4 -0
- metadata +73 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module InteractorSupport
|
|
2
|
+
module Concerns
|
|
3
|
+
module Transformable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include InteractorSupport::Core
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
class << self
|
|
9
|
+
# context_variable first_post: Post.first
|
|
10
|
+
# context_variable user: -> { User.find(user_id) }
|
|
11
|
+
# context_variable items: Item.all
|
|
12
|
+
# context_variable numbers: [1, 2, 3]
|
|
13
|
+
def context_variable(key_values)
|
|
14
|
+
before do
|
|
15
|
+
key_values.each do |key, value|
|
|
16
|
+
context[key] = if value.is_a?(Proc)
|
|
17
|
+
context.instance_exec(&value)
|
|
18
|
+
else
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# transform :email, :name, with: [:downcase, :strip]
|
|
26
|
+
# transform :url, with: :downcase
|
|
27
|
+
# transform :items, with: :compact
|
|
28
|
+
# transform :items, with: ->(value) { value.compact }
|
|
29
|
+
# transform :email, :url, with: ->(value) { value.downcase.strip }
|
|
30
|
+
# transform :items, with: :compact
|
|
31
|
+
def transform(*keys, with: [])
|
|
32
|
+
before do
|
|
33
|
+
if keys.empty?
|
|
34
|
+
raise ArgumentError, 'transform action requires at least one key.'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
keys.each do |key|
|
|
38
|
+
if with.is_a?(Proc)
|
|
39
|
+
begin
|
|
40
|
+
context[key] = context.instance_exec(&with)
|
|
41
|
+
rescue => e
|
|
42
|
+
context.fail!(errors: ["#{key} failed to transform: #{e.message}"])
|
|
43
|
+
end
|
|
44
|
+
elsif with.is_a?(Array)
|
|
45
|
+
context.fail!(errors: ["#{key} does not respond to all transforms"]) unless with.all? do |t|
|
|
46
|
+
t.is_a?(Symbol) && context[key].respond_to?(t)
|
|
47
|
+
end
|
|
48
|
+
context[key] = with.inject(context[key]) do |value, method|
|
|
49
|
+
value.send(method)
|
|
50
|
+
end
|
|
51
|
+
elsif with.is_a?(Symbol) && context[key].respond_to?(with)
|
|
52
|
+
context[key] = context[key].send(with)
|
|
53
|
+
elsif with.is_a?(Symbol)
|
|
54
|
+
context.fail!(errors: ["#{key} does not respond to #{with}"])
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, 'transform requires `with` to be a symbol or array of symbols.'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module InteractorSupport
|
|
2
|
+
module Concerns
|
|
3
|
+
module Updatable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include InteractorSupport::Core
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
class << self
|
|
9
|
+
def update(model, attributes: {}, context_key: nil)
|
|
10
|
+
context_key ||= model
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
record = context[model]
|
|
14
|
+
context.fail!(errors: ["#{model} not found"]) unless record
|
|
15
|
+
|
|
16
|
+
update_data =
|
|
17
|
+
case attributes
|
|
18
|
+
when Hash
|
|
19
|
+
attributes.each_with_object({}) do |(key, value), result|
|
|
20
|
+
case value
|
|
21
|
+
when Hash
|
|
22
|
+
parent = context[key]
|
|
23
|
+
context.fail!(errors: ["#{key} not found"]) unless parent
|
|
24
|
+
result.merge!(value.transform_values { |v| parent[v] })
|
|
25
|
+
when Array
|
|
26
|
+
parent = context[key]
|
|
27
|
+
context.fail!(errors: ["#{key} not found"]) unless parent
|
|
28
|
+
result.merge!(value.index_with { |v| parent[v] })
|
|
29
|
+
when Proc
|
|
30
|
+
begin
|
|
31
|
+
result[key] = context.instance_exec(&value)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
context.fail!(errors: [e.message])
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
result[key] = context.send(value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
when Symbol
|
|
40
|
+
data = context[attributes]
|
|
41
|
+
context.fail!(errors: ["#{attributes} not found"]) unless data
|
|
42
|
+
data
|
|
43
|
+
else
|
|
44
|
+
raise ArgumentError, "Invalid attributes: #{attributes}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
record.update!(update_data)
|
|
48
|
+
|
|
49
|
+
# Assign the updated record to context
|
|
50
|
+
context[context_key] = record
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module InteractorSupport
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :request_object_behavior, :request_object_key_type
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
# Default configuration values.
|
|
7
|
+
# :returns_context - request objects return a context object.
|
|
8
|
+
# :returns_self - request objects return self.
|
|
9
|
+
@request_object_behavior = :returns_context
|
|
10
|
+
|
|
11
|
+
# Default configuration values, only applies when request_object_behavior is :returns_context.
|
|
12
|
+
# :string - request object keys are strings.
|
|
13
|
+
# :symbol - request object keys are symbols.
|
|
14
|
+
# :struct - request object keys are struct objects.
|
|
15
|
+
@request_object_key_type = :symbol
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# app/concerns/interactor_support/request_object.rb
|
|
2
|
+
module InteractorSupport
|
|
3
|
+
module RequestObject
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
include ActiveModel::Model
|
|
8
|
+
include ActiveModel::Attributes
|
|
9
|
+
include ActiveModel::Validations::Callbacks
|
|
10
|
+
|
|
11
|
+
def initialize(attributes = {})
|
|
12
|
+
super(attributes)
|
|
13
|
+
raise ActiveModel::ValidationError, self unless valid?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_context
|
|
17
|
+
key_type = InteractorSupport.configuration.request_object_key_type
|
|
18
|
+
attrs = attributes.each_with_object({}) do |(name, value), hash|
|
|
19
|
+
name = key_type == :string ? name.to_s : name.to_sym
|
|
20
|
+
hash[name] = if value.respond_to?(:to_context)
|
|
21
|
+
value.to_context
|
|
22
|
+
elsif value.is_a?(Array) && value.first.respond_to?(:to_context)
|
|
23
|
+
value.map(&:to_context)
|
|
24
|
+
else
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
return Struct.new(*attrs.keys).new(*attrs.values) if key_type == :struct
|
|
29
|
+
|
|
30
|
+
attrs
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def new(*args, **kwargs)
|
|
35
|
+
return super(*args, **kwargs) if InteractorSupport.configuration.request_object_behavior == :returns_self
|
|
36
|
+
|
|
37
|
+
super(*args, **kwargs).to_context
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Accepts one or more attribute names along with options.
|
|
41
|
+
#
|
|
42
|
+
# Options:
|
|
43
|
+
# - type: a class to cast the value (often another RequestObject)
|
|
44
|
+
# - array: when true, expects an array; each element is cast.
|
|
45
|
+
# - default: default value for the attribute.
|
|
46
|
+
# - transform: a symbol or an array of symbols that will be applied (if the value responds to them).
|
|
47
|
+
def attribute(*names, type: nil, array: false, default: nil, transform: nil)
|
|
48
|
+
names.each do |name|
|
|
49
|
+
transform_options[name.to_sym] = transform if transform.present?
|
|
50
|
+
super(name, default: default)
|
|
51
|
+
original_writer = instance_method("#{name}=")
|
|
52
|
+
define_method("#{name}=") do |value|
|
|
53
|
+
# Apply transforms immediately if provided.
|
|
54
|
+
if transform
|
|
55
|
+
Array(transform).each do |method|
|
|
56
|
+
if value.respond_to?(method)
|
|
57
|
+
value = value.send(method)
|
|
58
|
+
elsif respond_to?(method)
|
|
59
|
+
value = send(method, value)
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "transform method #{method} not found"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
# Type: only wrap if not already an instance.
|
|
66
|
+
if type
|
|
67
|
+
value = if array
|
|
68
|
+
Array(value).map { |v| v.is_a?(type) ? v : type.new(v) }
|
|
69
|
+
else
|
|
70
|
+
value.is_a?(type) ? value : type.new(value)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
original_writer.bind(self).call(value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def transform_options
|
|
81
|
+
@_transform_options ||= {}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
class BaseInteractorCop < RuboCop::Cop::Base
|
|
8
|
+
def on_class(node)
|
|
9
|
+
check_interactor_usage(node)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def on_module(node)
|
|
13
|
+
check_interactor_usage(node)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def extract_included_modules(node)
|
|
19
|
+
node.each_descendant(:send)
|
|
20
|
+
.select { |send_node| send_node.method?(:include) }
|
|
21
|
+
.map { |send_node| [send_node, send_node.first_argument&.const_name] }
|
|
22
|
+
.reject { |_, name| name.nil? }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extract_used_methods(node)
|
|
26
|
+
node.each_descendant(:send).map(&:method_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def known_module_methods
|
|
30
|
+
{
|
|
31
|
+
'InteractorSupport::Concerns::Findable' => [:find_by, :find_where],
|
|
32
|
+
'InteractorSupport::Concerns::Skippable' => [:skip],
|
|
33
|
+
'InteractorSupport::Concerns::Transactionable' => [:transaction],
|
|
34
|
+
'InteractorSupport::Concerns::Transformable' => [:context_variable, :transform],
|
|
35
|
+
'InteractorSupport::Concerns::Updatable' => [:update],
|
|
36
|
+
'InteractorSupport::Validations' => validations_methods,
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def module_for_method_name(method_name)
|
|
41
|
+
known_module_methods.each do |mod, methods|
|
|
42
|
+
return mod if methods.include?(method_name)
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validations_methods
|
|
48
|
+
[
|
|
49
|
+
:required,
|
|
50
|
+
:optional,
|
|
51
|
+
:context_accessor,
|
|
52
|
+
:validates_after,
|
|
53
|
+
:validates_before,
|
|
54
|
+
:after_validation,
|
|
55
|
+
:before_validation,
|
|
56
|
+
:validates_acceptance_of,
|
|
57
|
+
:validates_numericality_of,
|
|
58
|
+
:validates_presence_of,
|
|
59
|
+
:validates_length_of,
|
|
60
|
+
:validates_size_of,
|
|
61
|
+
:validates_comparison_of,
|
|
62
|
+
:validates_confirmation_of,
|
|
63
|
+
:validates_absence_of,
|
|
64
|
+
:validates_exclusion_of,
|
|
65
|
+
:validates_format_of,
|
|
66
|
+
:validates_inclusion_of,
|
|
67
|
+
:human_attribute_name,
|
|
68
|
+
:lookup_ancestors,
|
|
69
|
+
:i18n_scope,
|
|
70
|
+
:descendants,
|
|
71
|
+
:reset_callbacks,
|
|
72
|
+
:define_callbacks,
|
|
73
|
+
:set_callback,
|
|
74
|
+
:normalize_callback_params,
|
|
75
|
+
:get_callbacks,
|
|
76
|
+
:set_callbacks,
|
|
77
|
+
:skip_callback,
|
|
78
|
+
:define_model_callbacks,
|
|
79
|
+
:model_name,
|
|
80
|
+
:clear_validators!,
|
|
81
|
+
:validators_on,
|
|
82
|
+
:attribute_method?,
|
|
83
|
+
:validates,
|
|
84
|
+
:validates!,
|
|
85
|
+
:validates_each,
|
|
86
|
+
:validates_with,
|
|
87
|
+
:validate,
|
|
88
|
+
:validators,
|
|
89
|
+
:yaml_tag,
|
|
90
|
+
]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
class RequireRequiredForInteractorSupport < Cop
|
|
6
|
+
MSG = 'Classes including `InteractorSupport` or `InteractorSupport::Validations` must invoke `required`.'
|
|
7
|
+
|
|
8
|
+
def_node_matcher :calls_required?, <<~PATTERN
|
|
9
|
+
(send nil? :required ...)
|
|
10
|
+
PATTERN
|
|
11
|
+
|
|
12
|
+
def includes_support?(node)
|
|
13
|
+
node.to_s =~ /InteractorSupport/ && (
|
|
14
|
+
node.to_s =~ /\(const nil :InteractorSupport\) :Validations\)/ ||
|
|
15
|
+
(node.to_s =~ /\(const nil :InteractorSupport\)/ && node.to_s !~ /:RequestObject/)
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_class(node)
|
|
20
|
+
return unless includes_support?(node)
|
|
21
|
+
|
|
22
|
+
required_called = calls_required?(node.body) || node.body&.children&.any? do |child|
|
|
23
|
+
calls_required?(child)
|
|
24
|
+
end
|
|
25
|
+
add_offense(node, message: MSG) unless required_called
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
require_relative 'base_interactor_cop'
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
class UnusedIncludedModules < BaseInteractorCop
|
|
9
|
+
MSG_SINGLE = 'Module `%<module>s` is included but its methods are not used in this class.'
|
|
10
|
+
MSG_GROUP = 'Use `%<correct_modules>s` instead.'
|
|
11
|
+
|
|
12
|
+
GROUP_MODULES = {
|
|
13
|
+
'InteractorSupport' => [
|
|
14
|
+
'InteractorSupport::Concerns::Findable',
|
|
15
|
+
'InteractorSupport::Concerns::Skippable',
|
|
16
|
+
'InteractorSupport::Concerns::Transactionable',
|
|
17
|
+
'InteractorSupport::Concerns::Transformable',
|
|
18
|
+
'InteractorSupport::Concerns::Updatable',
|
|
19
|
+
'InteractorSupport::Validations',
|
|
20
|
+
],
|
|
21
|
+
'InteractorSupport::Actions' => [
|
|
22
|
+
'InteractorSupport::Concerns::Skippable',
|
|
23
|
+
'InteractorSupport::Concerns::Transactionable',
|
|
24
|
+
'InteractorSupport::Concerns::Updatable',
|
|
25
|
+
'InteractorSupport::Concerns::Findable',
|
|
26
|
+
'InteractorSupport::Concerns::Transformable',
|
|
27
|
+
],
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def check_interactor_usage(node)
|
|
33
|
+
included_modules = extract_included_modules(node)
|
|
34
|
+
used_methods = extract_used_methods(node)
|
|
35
|
+
|
|
36
|
+
included_modules.each do |include_node, mod|
|
|
37
|
+
next unless mod.start_with?('InteractorSupport')
|
|
38
|
+
next if mod == 'InteractorSupport::RequestObject'
|
|
39
|
+
|
|
40
|
+
expanded_modules = GROUP_MODULES.fetch(mod, [mod])
|
|
41
|
+
unused_modules = expanded_modules.reject do |m|
|
|
42
|
+
known_module_methods[m]&.any? { |meth| used_methods.include?(meth) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
next if unused_modules.empty?
|
|
46
|
+
|
|
47
|
+
missing_modules = []
|
|
48
|
+
used_methods.each do |method_name|
|
|
49
|
+
missing_module = module_for_method_name(method_name)
|
|
50
|
+
next if missing_module.nil? || included_modules.any? { |_, mod| mod == missing_module }
|
|
51
|
+
|
|
52
|
+
missing_modules << missing_module unless missing_modules.include?(missing_module)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
modules = missing_modules.map { |missing_module| "include #{missing_module}" }
|
|
56
|
+
message = if unused_modules.size == 1
|
|
57
|
+
format(MSG_SINGLE, module: unused_modules.first)
|
|
58
|
+
else
|
|
59
|
+
format(MSG_GROUP, correct_modules: modules.join(', '))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
add_offense(include_node, message: message)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
require_relative 'base_interactor_cop'
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
class UsedUnincludedModules < BaseInteractorCop
|
|
9
|
+
MSG_MISSING_INTERACTOR = '`include Interactor` is required when including `%<module>s`.'
|
|
10
|
+
MSG_MISSING_MODULE = 'Method `%<method>s` is used but `%<module>s` is not included.'
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def check_interactor_usage(node)
|
|
15
|
+
included_modules = extract_included_modules(node)
|
|
16
|
+
used_methods = extract_used_methods_with_nodes(node)
|
|
17
|
+
|
|
18
|
+
interactor_included = included_modules.any? { |_, mod| mod == 'Interactor' }
|
|
19
|
+
|
|
20
|
+
included_modules.each do |include_node, mod|
|
|
21
|
+
if mod.start_with?('InteractorSupport') && !interactor_included
|
|
22
|
+
add_offense(include_node, message: format(MSG_MISSING_INTERACTOR, module: mod))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return unless interactor_included
|
|
27
|
+
|
|
28
|
+
used_methods.each do |method_name, method_node|
|
|
29
|
+
missing_module = module_for_method_name(method_name)
|
|
30
|
+
next if missing_module.nil? || included_modules.any? { |_, mod| mod == missing_module }
|
|
31
|
+
|
|
32
|
+
# Register offense on the exact method call
|
|
33
|
+
add_offense(
|
|
34
|
+
method_node,
|
|
35
|
+
message: format(MSG_MISSING_MODULE, method: method_name, module: missing_module),
|
|
36
|
+
severity: :info,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_used_methods_with_nodes(node)
|
|
42
|
+
node.each_descendant(:send).map { |send_node| [send_node.method_name, send_node] }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
|
|
3
|
+
module InteractorSupport
|
|
4
|
+
module Validations
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include InteractorSupport::Core
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
include ActiveModel::Validations
|
|
10
|
+
include ActiveModel::Validations::Callbacks
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def required(*keys)
|
|
15
|
+
apply_validations(keys, required: true)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def optional(*keys)
|
|
19
|
+
apply_validations(keys, required: false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validates_after(*keys, **validations)
|
|
23
|
+
after do
|
|
24
|
+
keys.each do |key|
|
|
25
|
+
apply_custom_validations(key, validations)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validates_before(*keys, **validations)
|
|
31
|
+
before do
|
|
32
|
+
if validations[:persisted]
|
|
33
|
+
context.fail!(errors: ['persisted validation is only available for after validations'])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
keys.each do |key|
|
|
37
|
+
apply_custom_validations(key, validations)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def apply_validations(keys, required:)
|
|
45
|
+
keys.each do |key|
|
|
46
|
+
if key.is_a?(Hash)
|
|
47
|
+
key.each do |attribute, validation_options|
|
|
48
|
+
attr_accessor(attribute)
|
|
49
|
+
|
|
50
|
+
define_context_methods(attribute)
|
|
51
|
+
if required
|
|
52
|
+
validates(attribute, validation_options)
|
|
53
|
+
else
|
|
54
|
+
validates(attribute, validation_options.merge(allow_nil: true))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
attr_accessor(key)
|
|
59
|
+
|
|
60
|
+
define_context_methods(key)
|
|
61
|
+
validates(key, presence: true) if required
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
# Ensure validations run before execution
|
|
65
|
+
before do
|
|
66
|
+
context.fail!(errors: errors.full_messages) unless valid?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def define_context_methods(key)
|
|
71
|
+
define_method(key) { context[key] }
|
|
72
|
+
define_method("#{key}=") { |value| context[key] = value }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def apply_custom_validations(key, validations)
|
|
77
|
+
validation_for_presence(key) if validations[:presence]
|
|
78
|
+
validation_for_inclusion(key, validations[:inclusion]) if validations[:inclusion]
|
|
79
|
+
validation_for_persistence(key) if validations[:persisted]
|
|
80
|
+
validation_for_type(key, validations[:type]) if validations[:type]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validation_for_type(key, type)
|
|
84
|
+
context.fail!(errors: ["#{key} was not of type #{type}"]) unless context[key].is_a?(type)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validation_for_inclusion(key, inclusion)
|
|
88
|
+
unless inclusion.is_a?(Hash) && inclusion[:in].is_a?(Enumerable)
|
|
89
|
+
raise ArgumentError, 'inclusion validation requires an :in key with an array or range'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context.fail!(errors: ["#{key} was not in the specified inclusion"]) unless inclusion[:in].include?(context[key])
|
|
93
|
+
rescue ArgumentError => e
|
|
94
|
+
context.fail!(errors: [e.message])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validation_for_presence(key)
|
|
98
|
+
context.fail!(errors: ["#{key} does not exist"]) unless context[key].present?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validation_for_persistence(key)
|
|
102
|
+
validation_for_presence(key)
|
|
103
|
+
unless context[key].is_a?(ApplicationRecord)
|
|
104
|
+
context.fail!(
|
|
105
|
+
errors: [
|
|
106
|
+
"#{key} is not an ApplicationRecord, which is required for persisted validation",
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context.fail!(
|
|
112
|
+
errors: ["#{key} was not persisted"] + context[key].errors.full_messages,
|
|
113
|
+
) unless context[key].persisted?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'interactor'
|
|
4
|
+
require 'logger'
|
|
5
|
+
require 'active_support/concern'
|
|
6
|
+
require_relative 'interactor_support/core'
|
|
7
|
+
require_relative 'interactor_support/version'
|
|
8
|
+
require_relative 'interactor_support/actions'
|
|
9
|
+
require_relative 'interactor_support/validations'
|
|
10
|
+
require_relative 'interactor_support/request_object'
|
|
11
|
+
require_relative 'interactor_support/configuration'
|
|
12
|
+
|
|
13
|
+
Dir[File.join(__dir__, 'interactor_support/concerns/*.rb')].sort.each { |file| require file }
|
|
14
|
+
|
|
15
|
+
module InteractorSupport
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def configure
|
|
20
|
+
self.configuration ||= Configuration.new
|
|
21
|
+
yield(configuration) if block_given?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
included do
|
|
30
|
+
include InteractorSupport::Actions
|
|
31
|
+
include InteractorSupport::Validations
|
|
32
|
+
end
|
|
33
|
+
end
|