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.
@@ -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
- # Ensure validations run before execution
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InteractorSupport
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.3'
5
5
  end
@@ -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.2
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-03-28 00:00:00.000000000 Z
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