active_interaction 0.10.2 → 1.0.0

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/README.md +32 -35
  4. data/lib/active_interaction.rb +14 -6
  5. data/lib/active_interaction/base.rb +155 -135
  6. data/lib/active_interaction/concerns/active_modelable.rb +46 -0
  7. data/lib/active_interaction/{modules/overload_hash.rb → concerns/hashable.rb} +5 -1
  8. data/lib/active_interaction/concerns/missable.rb +47 -0
  9. data/lib/active_interaction/concerns/runnable.rb +156 -0
  10. data/lib/active_interaction/errors.rb +50 -14
  11. data/lib/active_interaction/filter.rb +33 -45
  12. data/lib/active_interaction/filters/abstract_date_time_filter.rb +24 -15
  13. data/lib/active_interaction/filters/abstract_filter.rb +18 -0
  14. data/lib/active_interaction/filters/abstract_numeric_filter.rb +13 -8
  15. data/lib/active_interaction/filters/array_filter.rb +42 -35
  16. data/lib/active_interaction/filters/boolean_filter.rb +8 -11
  17. data/lib/active_interaction/filters/date_filter.rb +11 -15
  18. data/lib/active_interaction/filters/date_time_filter.rb +11 -15
  19. data/lib/active_interaction/filters/file_filter.rb +11 -11
  20. data/lib/active_interaction/filters/float_filter.rb +7 -15
  21. data/lib/active_interaction/filters/hash_filter.rb +18 -24
  22. data/lib/active_interaction/filters/integer_filter.rb +7 -14
  23. data/lib/active_interaction/filters/model_filter.rb +13 -14
  24. data/lib/active_interaction/filters/string_filter.rb +11 -14
  25. data/lib/active_interaction/filters/symbol_filter.rb +6 -9
  26. data/lib/active_interaction/filters/time_filter.rb +13 -16
  27. data/lib/active_interaction/modules/validation.rb +9 -4
  28. data/lib/active_interaction/version.rb +5 -2
  29. data/spec/active_interaction/base_spec.rb +109 -4
  30. data/spec/active_interaction/{modules/active_model_spec.rb → concerns/active_modelable_spec.rb} +15 -3
  31. data/spec/active_interaction/{modules/overload_hash_spec.rb → concerns/hashable_spec.rb} +2 -3
  32. data/spec/active_interaction/{modules/method_missing_spec.rb → concerns/missable_spec.rb} +38 -3
  33. data/spec/active_interaction/concerns/runnable_spec.rb +192 -0
  34. data/spec/active_interaction/filters/abstract_filter_spec.rb +8 -0
  35. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -1
  36. data/spec/active_interaction/modules/validation_spec.rb +2 -2
  37. data/spec/support/concerns.rb +15 -0
  38. data/spec/support/filters.rb +5 -5
  39. data/spec/support/interactions.rb +6 -5
  40. metadata +47 -73
  41. data/lib/active_interaction/filters.rb +0 -28
  42. data/lib/active_interaction/modules/active_model.rb +0 -32
  43. data/lib/active_interaction/modules/core.rb +0 -70
  44. data/lib/active_interaction/modules/method_missing.rb +0 -20
  45. data/spec/active_interaction/filters_spec.rb +0 -23
  46. data/spec/active_interaction/modules/core_spec.rb +0 -114
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 88026615847767352e357dadf32abd0a6a114889
4
- data.tar.gz: 5dd283e9aee1bab91a73ea20d52a7c8c24d5bed9
3
+ metadata.gz: 02f4ec454ba99aa3327ab6d539a8dd2f6e9b8622
4
+ data.tar.gz: e20fe1fb15d09040a7c00379962f53ac41d26cf1
5
5
  SHA512:
6
- metadata.gz: 0d6082582172127078839165df075f8de2f222a3ed6822f52ad69270c6240e9d3f3eacfbfc95e3bd4989e21e43d371182b3ca1a09a9639ac14546d7c76b604d6
7
- data.tar.gz: 6e81489a89de567be04f3f8713f88a0af5959de1fd2954b3731b1ce6ff7335615d423a59a428bd2081aa64a6a6dfede40e422005fbbb60cf3737f2a9e791ec31
6
+ metadata.gz: ab410e55639bb75899387091d50213f0b53b4d58de8b4cc0c3cc36d9c0f2acdc247bfe88662d276ed635a3fc695631a291c6c1f6234a7a46c38b4615127b3c7f
7
+ data.tar.gz: f48693acfcc32d503fdc03af08cad9b2ed1a4c7fb32b7dac467f30e325ab97f3647c7b79d865686d26a1cd93fb04ca9b449e52a8d247086a3bd1cb27ab85aad2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # [Master][]
2
2
 
3
+ # [1.0.0][]
4
+
5
+ - **Replace `Filters` with a hash.** To iterate over `Filter` objects, use
6
+ `Interaction.filters.values`.
7
+ - Rename `Filter#has_default?` to `Filter#default?`.
8
+ - Add `respond_to_missing?` to complement `method_missing` calls.
9
+ - Add predicate methods for checking if an input was passed.
10
+ - When adding a filter that shares a name with an existing filter, it will now
11
+ replace the existing one instead of adding a duplicate.
12
+ - Allow fetching filters by name.
13
+ - Allow import filters from another interaction with `import_filters`.
14
+
3
15
  # [0.10.2][] (2014-01-02)
4
16
 
5
17
  - Fix a bug that marked Time instances as invalid if Time.zone was set.
@@ -106,7 +118,8 @@
106
118
 
107
119
  - Initial release.
108
120
 
109
- [master]: https://github.com/orgsync/active_interaction/compare/v0.10.2...master
121
+ [master]: https://github.com/orgsync/active_interaction/compare/v1.0.0...master
122
+ [1.0.0]: https://github.com/orgsync/active_interaction/compare/v0.10.2...v1.0.0
110
123
  [0.10.2]: https://github.com/orgsync/active_interaction/compare/v0.10.1...v0.10.2
111
124
  [0.10.1]: https://github.com/orgsync/active_interaction/compare/v0.10.0...v0.10.1
112
125
  [0.10.0]: https://github.com/orgsync/active_interaction/compare/v0.9.1...v0.10.0
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # ActiveInteraction
1
+ # [ActiveInteraction][0]
2
2
 
3
- [![Gem Version][]][1]
4
- [![Build Status][]][2]
5
- [![Coverage Status][]][3]
6
- [![Code Climate][]][4]
7
- [![Dependency Status][]][5]
3
+ [![Gem Version][1]][2]
4
+ [![Build Status][3]][4]
5
+ [![Coverage Status][5]][6]
6
+ [![Code Climate][7]][8]
7
+ [![Dependency Status][9]][10]
8
8
 
9
9
  At first it seemed alright. A little business logic in a controller
10
10
  or model wasn't going to hurt anything. Then one day you wake up
@@ -15,17 +15,17 @@ to this.
15
15
  Take back control. Slim down models and wrangle monstrous controller
16
16
  methods with ActiveInteraction.
17
17
 
18
- Read more on the [project page][] or check out the full [documentation][]
18
+ Read more on the [project page][11] or check out the full [documentation][12]
19
19
  on RubyDoc.info.
20
20
 
21
21
  ## Installation
22
22
 
23
- This project uses [semantic versioning][].
23
+ This project uses [semantic versioning][13].
24
24
 
25
25
  Add it to your Gemfile:
26
26
 
27
27
  ```ruby
28
- gem 'active_interaction', '~> 0.10.2'
28
+ gem 'active_interaction', '~> 1.0'
29
29
  ```
30
30
 
31
31
  And then execute:
@@ -198,12 +198,10 @@ end
198
198
  end
199
199
  ```
200
200
 
201
- Check out the [documentation][] for a full list of methods.
201
+ Check out the [documentation][12] for a full list of methods.
202
202
 
203
203
  ## How do I compose interactions?
204
204
 
205
- (Note: this feature is experimental. See [#41][] & [#79][].)
206
-
207
205
  You can run interactions from within other interactions by calling `compose`.
208
206
  If the interaction is successful, it'll return the result (just like if you had
209
207
  called it with `run!`). If something went wrong, execution will halt
@@ -278,26 +276,25 @@ p Interaction.run.errors.messages
278
276
 
279
277
  ## Credits
280
278
 
281
- ActiveInteraction is brought to you by [@AaronLasseigne][] and
282
- [@tfausak][] from [@orgsync][]. We were inspired by the fantastic
283
- work done in [Mutations][].
284
-
285
- [#41]: https://github.com/orgsync/active_interaction/issues/41
286
- [#79]: https://github.com/orgsync/active_interaction/issues/79
287
- [1]: https://badge.fury.io/rb/active_interaction "Gem Version"
288
- [2]: https://travis-ci.org/orgsync/active_interaction "Build Status"
289
- [3]: https://coveralls.io/r/orgsync/active_interaction "Coverage Status"
290
- [4]: https://codeclimate.com/github/orgsync/active_interaction "Code Climate"
291
- [5]: https://gemnasium.com/orgsync/active_interaction "Dependency Status"
292
- [@AaronLasseigne]: https://github.com/AaronLasseigne
293
- [@orgsync]: https://github.com/orgsync
294
- [@tfausak]: https://github.com/tfausak
295
- [build status]: https://travis-ci.org/orgsync/active_interaction.png
296
- [code climate]: https://codeclimate.com/github/orgsync/active_interaction.png
297
- [coverage status]: https://coveralls.io/repos/orgsync/active_interaction/badge.png
298
- [dependency status]: https://gemnasium.com/orgsync/active_interaction.png
299
- [documentation]: http://rubydoc.info/github/orgsync/active_interaction
300
- [gem version]: https://badge.fury.io/rb/active_interaction.png
301
- [mutations]: https://github.com/cypriss/mutations
302
- [project page]: http://orgsync.github.io/active_interaction/
303
- [semantic versioning]: http://semver.org/spec/v2.0.0.html
279
+ ActiveInteraction is brought to you by [@AaronLasseigne][14] and
280
+ [@tfausak][15] from [@orgsync][16]. We were inspired by the fantastic
281
+ work done in [Mutations][17].
282
+
283
+ [0]: https://github.com/orgsync/active_interaction
284
+ [1]: https://badge.fury.io/rb/active_interaction.png
285
+ [2]: https://badge.fury.io/rb/active_interaction "Gem Version"
286
+ [3]: https://travis-ci.org/orgsync/active_interaction.png
287
+ [4]: https://travis-ci.org/orgsync/active_interaction "Build Status"
288
+ [5]: https://coveralls.io/repos/orgsync/active_interaction/badge.png
289
+ [6]: https://coveralls.io/r/orgsync/active_interaction "Coverage Status"
290
+ [7]: https://codeclimate.com/github/orgsync/active_interaction.png
291
+ [8]: https://codeclimate.com/github/orgsync/active_interaction "Code Climate"
292
+ [9]: https://gemnasium.com/orgsync/active_interaction.png
293
+ [10]: https://gemnasium.com/orgsync/active_interaction "Dependency Status"
294
+ [11]: http://orgsync.github.io/active_interaction/
295
+ [12]: http://rubydoc.info/github/orgsync/active_interaction
296
+ [13]: http://semver.org/spec/v2.0.0.html
297
+ [14]: https://github.com/AaronLasseigne
298
+ [15]: https://github.com/tfausak
299
+ [16]: https://github.com/orgsync
300
+ [17]: https://github.com/cypriss/mutations
@@ -5,14 +5,15 @@ require 'active_model'
5
5
  require 'active_interaction/version'
6
6
  require 'active_interaction/errors'
7
7
 
8
- require 'active_interaction/modules/active_model'
9
- require 'active_interaction/modules/core'
10
- require 'active_interaction/modules/method_missing'
11
- require 'active_interaction/modules/overload_hash'
8
+ require 'active_interaction/concerns/active_modelable'
9
+ require 'active_interaction/concerns/hashable'
10
+ require 'active_interaction/concerns/missable'
11
+ require 'active_interaction/concerns/runnable'
12
+
12
13
  require 'active_interaction/modules/validation'
13
14
 
14
15
  require 'active_interaction/filter'
15
- require 'active_interaction/filters'
16
+ require 'active_interaction/filters/abstract_filter'
16
17
  require 'active_interaction/filters/abstract_date_time_filter'
17
18
  require 'active_interaction/filters/abstract_numeric_filter'
18
19
  require 'active_interaction/filters/array_filter'
@@ -34,5 +35,12 @@ I18n.backend.load_translations(
34
35
  Dir[File.join(%w(lib active_interaction locale *.yml))]
35
36
  )
36
37
 
37
- # @since 0.1.0
38
+ # Manage application specific business logic.
39
+ #
40
+ # @author Aaron Lasseigne <aaron.lasseigne@gmail.com>
41
+ # @author Taylor Fausak <taylor@fausak.me>
42
+ #
43
+ # @since 1.0.0
44
+ #
45
+ # @version 1.0.0
38
46
  module ActiveInteraction end
@@ -4,199 +4,219 @@ require 'active_support/core_ext/hash/indifferent_access'
4
4
 
5
5
  module ActiveInteraction
6
6
  # @abstract Subclass and override {#execute} to implement a custom
7
- # ActiveInteraction class.
7
+ # ActiveInteraction::Base class.
8
+ #
9
+ # Provides interaction functionality. Subclass this to create an interaction.
8
10
  #
9
11
  # @example
10
12
  # class ExampleInteraction < ActiveInteraction::Base
11
13
  # # Required
12
- # integer :a, :b
14
+ # boolean :a
13
15
  #
14
16
  # # Optional
15
- # integer :c, default: nil
17
+ # boolean :b, default: false
16
18
  #
17
19
  # def execute
18
- # sum = a + b
19
- # c.nil? ? sum : sum + c
20
+ # a && b
20
21
  # end
21
22
  # end
22
23
  #
23
- # outcome = ExampleInteraction.run(a: 1, b: 2, c: 3)
24
+ # outcome = ExampleInteraction.run(a: true)
24
25
  # if outcome.valid?
25
- # p outcome.result
26
+ # outcome.result
26
27
  # else
27
- # p outcome.errors
28
+ # outcome.errors
28
29
  # end
29
30
  class Base
30
- include ActiveModel
31
+ include ActiveModelable
32
+ include Runnable
33
+
34
+ validate :input_errors
35
+
36
+ class << self
37
+ include Hashable
38
+ include Missable
39
+
40
+ # Get or set the description.
41
+ #
42
+ # @example
43
+ # core.desc
44
+ # # => nil
45
+ # core.desc('Description!')
46
+ # core.desc
47
+ # # => "Description!"
48
+ #
49
+ # @param desc [String, nil] What to set the description to.
50
+ #
51
+ # @return [String, nil] The description.
52
+ def desc(desc = nil)
53
+ if desc.nil?
54
+ unless instance_variable_defined?(:@_interaction_desc)
55
+ @_interaction_desc = nil
56
+ end
57
+ else
58
+ @_interaction_desc = desc
59
+ end
31
60
 
32
- extend Core
33
- extend MethodMissing
34
- extend OverloadHash
61
+ @_interaction_desc
62
+ end
35
63
 
36
- validate :input_errors, :runtime_errors
64
+ # Get all the filters defined on this interaction.
65
+ #
66
+ # @return [Hash{Symbol => Filter}]
67
+ def filters
68
+ @_interaction_filters ||= {}
69
+ end
37
70
 
38
- # @param inputs [Hash{Symbol => Object}] Attribute values to set.
39
- #
40
- # @private
41
- def initialize(inputs = {})
42
- fail ArgumentError, 'inputs must be a hash' unless inputs.is_a?(Hash)
71
+ # @private
72
+ def method_missing(*args, &block)
73
+ super do |klass, names, options|
74
+ fail InvalidFilterError, 'missing attribute name' if names.empty?
43
75
 
44
- @_interaction_errors = Errors.new(self)
45
- @_interaction_result = nil
46
- @_interaction_runtime_errors = nil
76
+ names.each { |name| add_filter(klass, name, options, &block) }
77
+ end
78
+ end
47
79
 
48
- process_inputs(inputs.symbolize_keys)
49
- end
80
+ # @!method run(inputs = {})
81
+ # Runs validations and if there are no errors it will call {#execute}.
82
+ #
83
+ # @param (see ActiveInteraction::Base#initialize)
84
+ #
85
+ # @return [Base]
86
+ loop
87
+
88
+ # @!method run!(inputs = {})
89
+ # Like {.run} except that it returns the value of {#execute} or raises
90
+ # an exception if there were any validation errors.
91
+ #
92
+ # @param (see ActiveInteraction::Base.run)
93
+ #
94
+ # @return (see ActiveInteraction::Runnable::ClassMethods#run!)
95
+ #
96
+ # @raise (see ActiveInteraction::Runnable::ClassMethods#run!)
97
+ loop
98
+
99
+ private
100
+
101
+ # @param klass [Class]
102
+ # @param name [Symbol]
103
+ # @param options [Hash]
104
+ def add_filter(klass, name, options, &block)
105
+ fail InvalidFilterError, name.inspect if reserved?(name)
106
+
107
+ filter = klass.new(name, options, &block)
108
+ filters[name] = filter
109
+ attr_accessor name
110
+ define_method("#{name}?") { !public_send(name).nil? }
111
+
112
+ filter.default if filter.default?
113
+ end
50
114
 
51
- # Returns the inputs provided to {.run} or {.run!} after being cast based
52
- # on the filters in the class.
53
- #
54
- # @return [Hash{Symbol => Object}] All inputs passed to {.run} or {.run!}.
55
- #
56
- # @since 0.6.0
57
- def inputs
58
- self.class.filters.each_with_object({}) do |filter, h|
59
- h[filter.name] = send(filter.name)
115
+ # Import filters from another interaction.
116
+ #
117
+ # @param klass [Class] The other interaction.
118
+ # @param options [Hash]
119
+ #
120
+ # @option options [Array<Symbol>, nil] :only Import only these filters.
121
+ # @option options [Array<Symbol>, nil] :except Import all filters except
122
+ # for these.
123
+ #
124
+ # @return (see .filters)
125
+ #
126
+ # @!visibility public
127
+ def import_filters(klass, options = {})
128
+ if options.key?(:only) && options.key?(:except)
129
+ fail ArgumentError, 'given both :only and :except'
130
+ end
131
+
132
+ only = options[:only]
133
+ except = options[:except]
134
+
135
+ other_filters = klass.filters.dup
136
+ other_filters.select! { |k, _| only.include?(k) } if only
137
+ other_filters.reject! { |k, _| except.include?(k) } if except
138
+
139
+ filters.merge!(other_filters)
60
140
  end
61
- end
62
141
 
63
- # Runs the business logic associated with the interaction. The method is
64
- # only run when there are no validation errors. The return value is
65
- # placed into {#result}. This method must be overridden in the subclass.
66
- # This method is run in a transaction if ActiveRecord is available.
67
- #
68
- # @raise [NotImplementedError] if the method is not defined.
69
- #
70
- # @abstract
71
- def execute
72
- fail NotImplementedError
73
- end
142
+ # @param klass [Class]
143
+ def inherited(klass)
144
+ klass.instance_variable_set(:@_interaction_filters, filters.dup)
145
+ end
74
146
 
75
- # Returns the output from {#execute} if there are no validation errors or
76
- # `nil` otherwise.
77
- #
78
- # @return [Object, nil] the output or nil if there were validation errors
79
- def result
80
- @_interaction_result
147
+ # @param symbol [Symbol]
148
+ #
149
+ # @return [Boolean]
150
+ def reserved?(symbol)
151
+ symbol.to_s.start_with?('_interaction_')
152
+ end
81
153
  end
82
154
 
155
+ # @param inputs [Hash{Symbol => Object}] Attribute values to set.
156
+ #
83
157
  # @private
84
- def errors
85
- @_interaction_errors
86
- end
158
+ def initialize(inputs = {})
159
+ fail ArgumentError, 'inputs must be a hash' unless inputs.is_a?(Hash)
87
160
 
88
- # @private
89
- def valid?(*args)
90
- super(*args) || (@_interaction_result = nil)
161
+ process_inputs(inputs.symbolize_keys)
91
162
  end
92
163
 
93
- # Get all the filters defined on this interaction.
164
+ # @!method compose(other, inputs = {})
165
+ # Run another interaction and return its result. If the other interaction
166
+ # fails, halt execution.
94
167
  #
95
- # @return [Filters]
168
+ # @param other (see ActiveInteraction::Runnable#compose)
169
+ # @param inputs (see ActiveInteraction::Base#initialize)
96
170
  #
97
- # @since 0.6.0
98
- def self.filters
99
- @_interaction_filters ||= Filters.new
100
- end
171
+ # @return (see ActiveInteraction::Base.run!)
172
+ loop
101
173
 
102
- # Runs validations and if there are no errors it will call {#execute}.
174
+ # @!method execute
175
+ # @abstract
103
176
  #
104
- # @param (see #initialize)
177
+ # Runs the business logic associated with the interaction. This method is
178
+ # only run when there are no validation errors. The return value is
179
+ # placed into {#result}. This method is run in a transaction if
180
+ # ActiveRecord is available.
105
181
  #
106
- # @return [ActiveInteraction::Base] An instance of the class `run` is
107
- # called on.
108
- def self.run(*args)
109
- new(*args).tap do |interaction|
110
- if interaction.valid?
111
- result = transaction do
112
- begin
113
- interaction.execute
114
- rescue Interrupt
115
- # Inner interaction failed. #compose handles merging errors.
116
- end
117
- end
118
-
119
- if interaction.errors.empty?
120
- interaction.instance_variable_set(:@_interaction_result, result)
121
- else
122
- interaction.instance_variable_set(
123
- :@_interaction_runtime_errors, interaction.errors.dup)
124
- end
125
- end
126
- end
127
- end
182
+ # @raise (see ActiveInteraction::Runnable#execute)
183
+ loop
128
184
 
129
- # @private
130
- def self.method_missing(*args, &block)
131
- super do |klass, names, options|
132
- fail InvalidFilterError, 'missing attribute name' if names.empty?
133
-
134
- names.each do |attribute|
135
- if attribute.to_s.start_with?('_interaction_')
136
- fail InvalidFilterError, attribute.inspect
137
- end
138
-
139
- filter = klass.new(attribute, options, &block)
140
- filters.add(filter)
141
- attr_accessor filter.name
142
-
143
- # This isn't required, but it makes invalid defaults raise errors on
144
- # class definition instead of on execution.
145
- filter.default if filter.has_default?
146
- end
185
+ # Returns the inputs provided to {.run} or {.run!} after being cast based
186
+ # on the filters in the class.
187
+ #
188
+ # @return [Hash{Symbol => Object}] All inputs passed to {.run} or {.run!}.
189
+ def inputs
190
+ self.class.filters.keys.each_with_object({}) do |name, h|
191
+ h[name] = public_send(name)
147
192
  end
148
193
  end
149
194
 
150
195
  private
151
196
 
197
+ # @param inputs [Hash{Symbol => Object}]
152
198
  def process_inputs(inputs)
153
199
  inputs.each do |key, value|
154
- if key.to_s.start_with?('_interaction_')
155
- fail InvalidValueError, key.inspect
156
- end
200
+ fail InvalidValueError, key.inspect if self.class.send(:reserved?, key)
157
201
 
158
202
  instance_variable_set("@#{key}", value)
159
203
  end
160
204
 
161
- self.class.filters.each do |filter|
205
+ self.class.filters.each do |name, filter|
162
206
  begin
163
- send("#{filter.name}=", filter.clean(inputs[filter.name]))
207
+ public_send("#{name}=", filter.clean(inputs[name]))
164
208
  rescue InvalidValueError, MissingValueError
165
209
  # Validators (#input_errors) will add errors if appropriate.
166
210
  end
167
211
  end
168
212
  end
169
213
 
214
+ # @!group Validations
215
+
170
216
  def input_errors
171
217
  Validation.validate(self.class.filters, inputs).each do |error|
172
218
  errors.add_sym(*error)
173
219
  end
174
220
  end
175
-
176
- def runtime_errors
177
- if @_interaction_runtime_errors
178
- errors.merge!(@_interaction_runtime_errors)
179
- end
180
- end
181
-
182
- def compose(interaction, inputs = {})
183
- outcome = interaction.run(inputs)
184
- return outcome.result if outcome.valid?
185
-
186
- # This can't use Errors#merge! because the errors have to be added to
187
- # base.
188
- outcome.errors.full_messages.each do |message|
189
- errors.add(:base, message) unless errors.added?(:base, message)
190
- end
191
-
192
- fail Interrupt
193
- end
194
-
195
- def self.inherited(klass)
196
- new_filters = Filters.new
197
- filters.each { |f| new_filters.add(f) }
198
-
199
- klass.instance_variable_set(:@_interaction_filters, new_filters)
200
- end
201
221
  end
202
222
  end