interactor_support 1.0.2 → 1.0.3
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/.prettierignore +1 -0
- data/.yardopts +19 -0
- data/CHANGELOG.md +6 -0
- data/README.md +177 -319
- data/lib/interactor_support/actions.rb +31 -0
- data/lib/interactor_support/concerns/findable.rb +64 -20
- data/lib/interactor_support/concerns/skippable.rb +42 -0
- data/lib/interactor_support/concerns/transactionable.rb +37 -0
- data/lib/interactor_support/concerns/transformable.rb +64 -10
- data/lib/interactor_support/concerns/updatable.rb +43 -1
- data/lib/interactor_support/configuration.rb +32 -9
- data/lib/interactor_support/core.rb +19 -1
- data/lib/interactor_support/request_object.rb +131 -19
- data/lib/interactor_support/validations.rb +89 -9
- data/lib/interactor_support/version.rb +1 -1
- data/lib/interactor_support.rb +44 -0
- metadata +4 -2
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
require 'active_model'
|
|
2
2
|
|
|
3
3
|
module InteractorSupport
|
|
4
|
+
##
|
|
5
|
+
# Provides context-aware validation DSL for interactors.
|
|
6
|
+
#
|
|
7
|
+
# This module adds `ActiveModel::Validations` and wraps it with methods like
|
|
8
|
+
# `required`, `optional`, `validates_before`, and `validates_after`, allowing
|
|
9
|
+
# declarative validation of interactor context values.
|
|
10
|
+
#
|
|
11
|
+
# Validations are executed automatically before (or after) the interactor runs.
|
|
12
|
+
#
|
|
13
|
+
# @example Required attributes with ActiveModel rules
|
|
14
|
+
# required :email, :name
|
|
15
|
+
# required age: { numericality: { greater_than: 18 } }
|
|
16
|
+
#
|
|
17
|
+
# @example Optional attributes with presence/format validations
|
|
18
|
+
# optional bio: { length: { maximum: 500 } }
|
|
19
|
+
#
|
|
20
|
+
# @example Type and inclusion validation before execution
|
|
21
|
+
# validates_before :role, type: String, inclusion: { in: %w[admin user guest] }
|
|
22
|
+
#
|
|
23
|
+
# @example Persistence validation after execution
|
|
24
|
+
# validates_after :user, persisted: true
|
|
25
|
+
#
|
|
26
|
+
# @see ActiveModel::Validations
|
|
4
27
|
module Validations
|
|
5
28
|
extend ActiveSupport::Concern
|
|
6
29
|
include InteractorSupport::Core
|
|
@@ -11,14 +34,35 @@ module InteractorSupport
|
|
|
11
34
|
end
|
|
12
35
|
|
|
13
36
|
class_methods do
|
|
37
|
+
##
|
|
38
|
+
# Declares one or more attributes as required.
|
|
39
|
+
#
|
|
40
|
+
# Values must be present in the context. You can also pass validation options
|
|
41
|
+
# as a hash, which will be forwarded to ActiveModel's `validates`.
|
|
42
|
+
#
|
|
43
|
+
# @param keys [Array<Symbol, Hash>] attribute names or hash of attributes with validation options
|
|
14
44
|
def required(*keys)
|
|
15
45
|
apply_validations(keys, required: true)
|
|
16
46
|
end
|
|
17
47
|
|
|
48
|
+
##
|
|
49
|
+
# Declares one or more attributes as optional.
|
|
50
|
+
#
|
|
51
|
+
# Optional values can be nil, but still support validation rules.
|
|
52
|
+
#
|
|
53
|
+
# @param keys [Array<Symbol, Hash>] attribute names or hash of attributes with validation options
|
|
18
54
|
def optional(*keys)
|
|
19
55
|
apply_validations(keys, required: false)
|
|
20
56
|
end
|
|
21
57
|
|
|
58
|
+
##
|
|
59
|
+
# Runs additional validations *after* the interactor executes.
|
|
60
|
+
#
|
|
61
|
+
# Useful for checking persisted records, custom conditions, or results
|
|
62
|
+
# that depend on post-processing logic.
|
|
63
|
+
#
|
|
64
|
+
# @param keys [Array<Symbol>] context keys to validate
|
|
65
|
+
# @param validations [Hash] validation options (e.g., presence:, type:, inclusion:, persisted:)
|
|
22
66
|
def validates_after(*keys, **validations)
|
|
23
67
|
after do
|
|
24
68
|
keys.each do |key|
|
|
@@ -27,6 +71,15 @@ module InteractorSupport
|
|
|
27
71
|
end
|
|
28
72
|
end
|
|
29
73
|
|
|
74
|
+
##
|
|
75
|
+
# Runs validations *before* the interactor executes.
|
|
76
|
+
#
|
|
77
|
+
# Prevents invalid data from reaching business logic.
|
|
78
|
+
#
|
|
79
|
+
# NOTE: `persisted:` validation is only available in `validates_after`.
|
|
80
|
+
#
|
|
81
|
+
# @param keys [Array<Symbol>] context keys to validate
|
|
82
|
+
# @param validations [Hash] validation options (e.g., presence:, type:, inclusion:)
|
|
30
83
|
def validates_before(*keys, **validations)
|
|
31
84
|
before do
|
|
32
85
|
if validations[:persisted]
|
|
@@ -41,6 +94,11 @@ module InteractorSupport
|
|
|
41
94
|
|
|
42
95
|
private
|
|
43
96
|
|
|
97
|
+
##
|
|
98
|
+
# Applies ActiveModel-based validations and wires up accessors to context.
|
|
99
|
+
#
|
|
100
|
+
# @param keys [Array<Symbol, Hash>] attributes to validate
|
|
101
|
+
# @param required [Boolean] whether presence is enforced
|
|
44
102
|
def apply_validations(keys, required:)
|
|
45
103
|
keys.each do |key|
|
|
46
104
|
if key.is_a?(Hash)
|
|
@@ -61,18 +119,27 @@ module InteractorSupport
|
|
|
61
119
|
validates(key, presence: true) if required
|
|
62
120
|
end
|
|
63
121
|
end
|
|
64
|
-
|
|
122
|
+
|
|
65
123
|
before do
|
|
66
124
|
context.fail!(errors: errors.full_messages) unless valid?
|
|
67
125
|
end
|
|
68
126
|
end
|
|
69
127
|
|
|
128
|
+
##
|
|
129
|
+
# Defines methods to read/write from the interactor context.
|
|
130
|
+
#
|
|
131
|
+
# @param key [Symbol] the context key
|
|
70
132
|
def define_context_methods(key)
|
|
71
133
|
define_method(key) { context[key] }
|
|
72
134
|
define_method("#{key}=") { |value| context[key] = value }
|
|
73
135
|
end
|
|
74
136
|
end
|
|
75
137
|
|
|
138
|
+
##
|
|
139
|
+
# Applies custom inline validations to a context key.
|
|
140
|
+
#
|
|
141
|
+
# @param key [Symbol] the context key
|
|
142
|
+
# @param validations [Hash] options like presence:, type:, inclusion:, persisted:
|
|
76
143
|
def apply_custom_validations(key, validations)
|
|
77
144
|
validation_for_presence(key) if validations[:presence]
|
|
78
145
|
validation_for_inclusion(key, validations[:inclusion]) if validations[:inclusion]
|
|
@@ -80,10 +147,20 @@ module InteractorSupport
|
|
|
80
147
|
validation_for_type(key, validations[:type]) if validations[:type]
|
|
81
148
|
end
|
|
82
149
|
|
|
150
|
+
##
|
|
151
|
+
# Fails if context value is not of expected type.
|
|
152
|
+
#
|
|
153
|
+
# @param key [Symbol]
|
|
154
|
+
# @param type [Class]
|
|
83
155
|
def validation_for_type(key, type)
|
|
84
156
|
context.fail!(errors: ["#{key} was not of type #{type}"]) unless context[key].is_a?(type)
|
|
85
157
|
end
|
|
86
158
|
|
|
159
|
+
##
|
|
160
|
+
# Fails if value is not included in allowed values.
|
|
161
|
+
#
|
|
162
|
+
# @param key [Symbol]
|
|
163
|
+
# @param inclusion [Hash] with `:in` key
|
|
87
164
|
def validation_for_inclusion(key, inclusion)
|
|
88
165
|
unless inclusion.is_a?(Hash) && inclusion[:in].is_a?(Enumerable)
|
|
89
166
|
raise ArgumentError, 'inclusion validation requires an :in key with an array or range'
|
|
@@ -94,23 +171,26 @@ module InteractorSupport
|
|
|
94
171
|
context.fail!(errors: [e.message])
|
|
95
172
|
end
|
|
96
173
|
|
|
174
|
+
##
|
|
175
|
+
# Fails if value is nil or blank.
|
|
176
|
+
#
|
|
177
|
+
# @param key [Symbol]
|
|
97
178
|
def validation_for_presence(key)
|
|
98
179
|
context.fail!(errors: ["#{key} does not exist"]) unless context[key].present?
|
|
99
180
|
end
|
|
100
181
|
|
|
182
|
+
##
|
|
183
|
+
# Fails if value is not a persisted `ApplicationRecord`.
|
|
184
|
+
#
|
|
185
|
+
# @param key [Symbol]
|
|
101
186
|
def validation_for_persistence(key)
|
|
102
187
|
validation_for_presence(key)
|
|
188
|
+
|
|
103
189
|
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
|
-
)
|
|
190
|
+
context.fail!(errors: ["#{key} is not an ApplicationRecord, which is required for persisted validation"])
|
|
109
191
|
end
|
|
110
192
|
|
|
111
|
-
context.fail!(
|
|
112
|
-
errors: ["#{key} was not persisted"] + context[key].errors.full_messages,
|
|
113
|
-
) unless context[key].persisted?
|
|
193
|
+
context.fail!(errors: ["#{key} was not persisted"] + context[key].errors.full_messages) unless context[key].persisted?
|
|
114
194
|
end
|
|
115
195
|
end
|
|
116
196
|
end
|
data/lib/interactor_support.rb
CHANGED
|
@@ -12,15 +12,59 @@ require_relative 'interactor_support/configuration'
|
|
|
12
12
|
|
|
13
13
|
Dir[File.join(__dir__, 'interactor_support/concerns/*.rb')].sort.each { |file| require file }
|
|
14
14
|
|
|
15
|
+
##
|
|
16
|
+
# InteractorSupport is a modular DSL for building expressive, validated, and
|
|
17
|
+
# transactional service objects using the [Interactor](https://github.com/collectiveidea/interactor) gem.
|
|
18
|
+
#
|
|
19
|
+
# It enhances interactors with powerful helpers like:
|
|
20
|
+
# - `Actions` for data loading, transformation, and persistence
|
|
21
|
+
# - `Validations` for context-aware presence/type/inclusion checks
|
|
22
|
+
# - `RequestObject` for clean, validated, form-like parameter objects
|
|
23
|
+
#
|
|
24
|
+
# It also provides configuration options to control request object behavior.
|
|
25
|
+
#
|
|
26
|
+
# @example Basic usage
|
|
27
|
+
# class CreateUser
|
|
28
|
+
# include Interactor
|
|
29
|
+
# include InteractorSupport
|
|
30
|
+
#
|
|
31
|
+
# required :email, :name
|
|
32
|
+
#
|
|
33
|
+
# transform :email, with: [:strip, :downcase]
|
|
34
|
+
#
|
|
35
|
+
# find_by :account
|
|
36
|
+
#
|
|
37
|
+
# update :user, attributes: { email: :email, name: :name }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# @example Configuration
|
|
41
|
+
# InteractorSupport.configure do |config|
|
|
42
|
+
# config.request_object_behavior = :returns_self
|
|
43
|
+
# config.request_object_key_type = :symbol
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# @see InteractorSupport::Actions
|
|
47
|
+
# @see InteractorSupport::Validations
|
|
48
|
+
# @see InteractorSupport::RequestObject
|
|
49
|
+
# @see InteractorSupport::Configuration
|
|
15
50
|
module InteractorSupport
|
|
16
51
|
extend ActiveSupport::Concern
|
|
17
52
|
|
|
18
53
|
class << self
|
|
54
|
+
##
|
|
55
|
+
# Allows external configuration of InteractorSupport.
|
|
56
|
+
#
|
|
57
|
+
# @yieldparam config [InteractorSupport::Configuration] the global configuration object
|
|
58
|
+
# @return [void]
|
|
19
59
|
def configure
|
|
20
60
|
self.configuration ||= Configuration.new
|
|
21
61
|
yield(configuration) if block_given?
|
|
22
62
|
end
|
|
23
63
|
|
|
64
|
+
##
|
|
65
|
+
# Returns the global InteractorSupport configuration object.
|
|
66
|
+
#
|
|
67
|
+
# @return [InteractorSupport::Configuration]
|
|
24
68
|
def configuration
|
|
25
69
|
@configuration ||= Configuration.new
|
|
26
70
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: interactor_support
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Charlie Mitchell
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-04-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description:
|
|
14
14
|
email:
|
|
@@ -17,8 +17,10 @@ executables: []
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
+
- ".prettierignore"
|
|
20
21
|
- ".rspec"
|
|
21
22
|
- ".rubocop.yml"
|
|
23
|
+
- ".yardopts"
|
|
22
24
|
- CHANGELOG.md
|
|
23
25
|
- CODE_OF_CONDUCT.md
|
|
24
26
|
- LICENSE.txt
|