active_interaction 3.8.3 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -0
  3. data/README.md +93 -107
  4. data/lib/active_interaction.rb +1 -7
  5. data/lib/active_interaction/base.rb +23 -26
  6. data/lib/active_interaction/concerns/active_modelable.rb +1 -3
  7. data/lib/active_interaction/concerns/active_recordable.rb +1 -6
  8. data/lib/active_interaction/concerns/hashable.rb +0 -1
  9. data/lib/active_interaction/concerns/missable.rb +0 -1
  10. data/lib/active_interaction/concerns/runnable.rb +6 -12
  11. data/lib/active_interaction/errors.rb +3 -6
  12. data/lib/active_interaction/filter.rb +51 -37
  13. data/lib/active_interaction/filter_column.rb +0 -3
  14. data/lib/active_interaction/filters/abstract_date_time_filter.rb +34 -36
  15. data/lib/active_interaction/filters/abstract_numeric_filter.rb +27 -17
  16. data/lib/active_interaction/filters/array_filter.rb +57 -36
  17. data/lib/active_interaction/filters/boolean_filter.rb +26 -12
  18. data/lib/active_interaction/filters/date_filter.rb +1 -2
  19. data/lib/active_interaction/filters/date_time_filter.rb +1 -2
  20. data/lib/active_interaction/filters/decimal_filter.rb +10 -28
  21. data/lib/active_interaction/filters/file_filter.rb +6 -5
  22. data/lib/active_interaction/filters/float_filter.rb +1 -2
  23. data/lib/active_interaction/filters/hash_filter.rb +37 -27
  24. data/lib/active_interaction/filters/integer_filter.rb +7 -8
  25. data/lib/active_interaction/filters/interface_filter.rb +48 -14
  26. data/lib/active_interaction/filters/object_filter.rb +23 -50
  27. data/lib/active_interaction/filters/record_filter.rb +10 -35
  28. data/lib/active_interaction/filters/string_filter.rb +21 -12
  29. data/lib/active_interaction/filters/symbol_filter.rb +13 -7
  30. data/lib/active_interaction/filters/time_filter.rb +19 -22
  31. data/lib/active_interaction/grouped_input.rb +0 -3
  32. data/lib/active_interaction/inputs.rb +89 -0
  33. data/lib/active_interaction/modules/input_processor.rb +1 -4
  34. data/lib/active_interaction/modules/validation.rb +9 -12
  35. data/lib/active_interaction/version.rb +1 -3
  36. data/spec/active_interaction/base_spec.rb +13 -41
  37. data/spec/active_interaction/concerns/active_modelable_spec.rb +0 -2
  38. data/spec/active_interaction/concerns/active_recordable_spec.rb +0 -2
  39. data/spec/active_interaction/concerns/hashable_spec.rb +1 -3
  40. data/spec/active_interaction/concerns/missable_spec.rb +0 -2
  41. data/spec/active_interaction/concerns/runnable_spec.rb +9 -13
  42. data/spec/active_interaction/errors_spec.rb +4 -25
  43. data/spec/active_interaction/filter_column_spec.rb +0 -2
  44. data/spec/active_interaction/filter_spec.rb +0 -2
  45. data/spec/active_interaction/filters/abstract_date_time_filter_spec.rb +1 -3
  46. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -3
  47. data/spec/active_interaction/filters/array_filter_spec.rb +41 -15
  48. data/spec/active_interaction/filters/boolean_filter_spec.rb +58 -4
  49. data/spec/active_interaction/filters/date_filter_spec.rb +43 -3
  50. data/spec/active_interaction/filters/date_time_filter_spec.rb +43 -3
  51. data/spec/active_interaction/filters/decimal_filter_spec.rb +57 -3
  52. data/spec/active_interaction/filters/file_filter_spec.rb +1 -3
  53. data/spec/active_interaction/filters/float_filter_spec.rb +60 -4
  54. data/spec/active_interaction/filters/hash_filter_spec.rb +19 -9
  55. data/spec/active_interaction/filters/integer_filter_spec.rb +49 -7
  56. data/spec/active_interaction/filters/interface_filter_spec.rb +397 -24
  57. data/spec/active_interaction/filters/object_filter_spec.rb +23 -59
  58. data/spec/active_interaction/filters/record_filter_spec.rb +23 -49
  59. data/spec/active_interaction/filters/string_filter_spec.rb +15 -3
  60. data/spec/active_interaction/filters/symbol_filter_spec.rb +15 -3
  61. data/spec/active_interaction/filters/time_filter_spec.rb +65 -3
  62. data/spec/active_interaction/grouped_input_spec.rb +0 -2
  63. data/spec/active_interaction/i18n_spec.rb +3 -7
  64. data/spec/active_interaction/{modules/input_processor_spec.rb → inputs_spec.rb} +3 -5
  65. data/spec/active_interaction/integration/array_interaction_spec.rb +18 -22
  66. data/spec/active_interaction/integration/boolean_interaction_spec.rb +0 -2
  67. data/spec/active_interaction/integration/date_interaction_spec.rb +0 -2
  68. data/spec/active_interaction/integration/date_time_interaction_spec.rb +0 -2
  69. data/spec/active_interaction/integration/file_interaction_spec.rb +0 -2
  70. data/spec/active_interaction/integration/float_interaction_spec.rb +0 -2
  71. data/spec/active_interaction/integration/hash_interaction_spec.rb +0 -2
  72. data/spec/active_interaction/integration/integer_interaction_spec.rb +0 -2
  73. data/spec/active_interaction/integration/interface_interaction_spec.rb +1 -3
  74. data/spec/active_interaction/integration/object_interaction_spec.rb +0 -2
  75. data/spec/active_interaction/integration/string_interaction_spec.rb +0 -2
  76. data/spec/active_interaction/integration/symbol_interaction_spec.rb +0 -2
  77. data/spec/active_interaction/integration/time_interaction_spec.rb +14 -18
  78. data/spec/active_interaction/modules/validation_spec.rb +1 -3
  79. data/spec/spec_helper.rb +2 -6
  80. data/spec/support/concerns.rb +0 -2
  81. data/spec/support/filters.rb +13 -9
  82. data/spec/support/interactions.rb +22 -14
  83. metadata +77 -52
  84. data/lib/active_interaction/backports.rb +0 -59
  85. data/lib/active_interaction/filters/abstract_filter.rb +0 -19
  86. data/spec/active_interaction/filters/abstract_filter_spec.rb +0 -8
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'active_model'
@@ -7,8 +6,6 @@ require 'active_model'
7
6
  #
8
7
  # @author Aaron Lasseigne <aaron.lasseigne@gmail.com>
9
8
  # @author Taylor Fausak <taylor@fausak.me>
10
- #
11
- # @since 1.0.0
12
9
  module ActiveInteraction
13
10
  end
14
11
 
@@ -22,13 +19,12 @@ require 'active_interaction/concerns/missable'
22
19
  require 'active_interaction/concerns/runnable'
23
20
 
24
21
  require 'active_interaction/grouped_input'
22
+ require 'active_interaction/inputs'
25
23
 
26
- require 'active_interaction/modules/input_processor'
27
24
  require 'active_interaction/modules/validation'
28
25
 
29
26
  require 'active_interaction/filter_column'
30
27
  require 'active_interaction/filter'
31
- require 'active_interaction/filters/abstract_filter'
32
28
  require 'active_interaction/filters/interface_filter'
33
29
  require 'active_interaction/filters/abstract_date_time_filter'
34
30
  require 'active_interaction/filters/abstract_numeric_filter'
@@ -49,8 +45,6 @@ require 'active_interaction/filters/time_filter'
49
45
 
50
46
  require 'active_interaction/base'
51
47
 
52
- require 'active_interaction/backports'
53
-
54
48
  I18n.load_path.unshift(
55
49
  *Dir.glob(
56
50
  File.expand_path(
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'active_support/core_ext/hash/indifferent_access'
@@ -28,7 +27,7 @@ module ActiveInteraction
28
27
  # else
29
28
  # outcome.errors
30
29
  # end
31
- class Base # rubocop:disable Metrics/ClassLength
30
+ class Base
32
31
  include ActiveModelable
33
32
  include ActiveRecordable
34
33
  include Runnable
@@ -74,9 +73,7 @@ module ActiveInteraction
74
73
  # @return [String, nil] The description.
75
74
  def desc(desc = nil)
76
75
  if desc.nil?
77
- unless instance_variable_defined?(:@_interaction_desc)
78
- @_interaction_desc = nil
79
- end
76
+ @_interaction_desc = nil unless instance_variable_defined?(:@_interaction_desc)
80
77
  else
81
78
  @_interaction_desc = desc
82
79
  end
@@ -88,17 +85,21 @@ module ActiveInteraction
88
85
  #
89
86
  # @return [Hash{Symbol => Filter}]
90
87
  def filters
88
+ # rubocop:disable Naming/MemoizedInstanceVariableName
91
89
  @_interaction_filters ||= {}
90
+ # rubocop:enable Naming/MemoizedInstanceVariableName
92
91
  end
93
92
 
94
93
  # @private
95
- def method_missing(*args, &block) # rubocop:disable Style/MethodMissing
94
+ # rubocop:disable Style/MissingRespondToMissing
95
+ def method_missing(*args, &block)
96
96
  super do |klass, names, options|
97
97
  raise InvalidFilterError, 'missing attribute name' if names.empty?
98
98
 
99
99
  names.each { |name| add_filter(klass, name, options, &block) }
100
100
  end
101
101
  end
102
+ # rubocop:enable Style/MissingRespondToMissing
102
103
 
103
104
  private
104
105
 
@@ -106,9 +107,7 @@ module ActiveInteraction
106
107
  # @param name [Symbol]
107
108
  # @param options [Hash]
108
109
  def add_filter(klass, name, options, &block)
109
- if InputProcessor.reserved?(name)
110
- raise InvalidFilterError, %("#{name}" is a reserved name)
111
- end
110
+ raise InvalidFilterError, %("#{name}" is a reserved name) if ActiveInteraction::Inputs.reserved?(name)
112
111
 
113
112
  initialize_filter(klass.new(name, options, &block))
114
113
  end
@@ -133,7 +132,7 @@ module ActiveInteraction
133
132
  other_filters.select! { |k, _| [*only].include?(k) } if only
134
133
  other_filters.reject! { |k, _| [*except].include?(k) } if except
135
134
 
136
- other_filters.values.each { |filter| initialize_filter(filter) }
135
+ other_filters.each_value { |filter| initialize_filter(filter) }
137
136
  end
138
137
 
139
138
  # @param klass [Class]
@@ -146,13 +145,10 @@ module ActiveInteraction
146
145
  # @param filter [Filter]
147
146
  def initialize_filter(filter)
148
147
  attribute = filter.name
149
- if filters.key?(attribute)
150
- warn "WARNING: Redefining #{name}##{attribute} filter"
151
- end
148
+ warn "WARNING: Redefining #{name}##{attribute} filter" if filters.key?(attribute)
152
149
  filters[attribute] = filter
153
150
 
154
151
  attr_accessor attribute
155
- define_method("#{attribute}?") { !public_send(attribute).nil? }
156
152
 
157
153
  eagerly_evaluate_default(filter)
158
154
  end
@@ -195,9 +191,10 @@ module ActiveInteraction
195
191
  #
196
192
  # @return [Hash{Symbol => Object}] All inputs passed to {.run} or {.run!}.
197
193
  def inputs
198
- self.class.filters.keys.each_with_object({}) do |name, h|
199
- h[name] = public_send(name)
200
- end
194
+ @inputs ||= self.class.filters
195
+ .each_key.with_object(ActiveInteraction::Inputs.new) do |name, h|
196
+ h[name] = public_send(name)
197
+ end.freeze
201
198
  end
202
199
 
203
200
  # Returns `true` if the given key was in the hash passed to {.run}.
@@ -294,10 +291,12 @@ module ActiveInteraction
294
291
  @_interaction_inputs = inputs
295
292
 
296
293
  inputs.each do |key, value|
297
- populate_reader(key, value) unless InputProcessor.reserved?(key)
294
+ next if ActiveInteraction::Inputs.reserved?(key)
295
+
296
+ populate_reader(key, value)
298
297
  end
299
298
 
300
- populate_filters(InputProcessor.process(inputs))
299
+ populate_filters(ActiveInteraction::Inputs.process(inputs))
301
300
  end
302
301
 
303
302
  def populate_reader(key, value)
@@ -306,18 +305,16 @@ module ActiveInteraction
306
305
 
307
306
  def populate_filters(inputs)
308
307
  self.class.filters.each do |name, filter|
309
- begin
310
- public_send("#{name}=", filter.clean(inputs[name], self))
311
- rescue InvalidValueError, MissingValueError, NoDefaultError
312
- nil # #type_check will add errors if appropriate.
313
- end
308
+ public_send("#{name}=", filter.clean(inputs[name], self))
309
+ rescue InvalidValueError, MissingValueError, NoDefaultError
310
+ nil # #type_check will add errors if appropriate.
314
311
  end
315
312
  end
316
313
 
317
314
  def type_check
318
315
  run_callbacks(:type_check) do
319
- Validation.validate(self, self.class.filters, inputs).each do |error|
320
- errors.add(*error)
316
+ Validation.validate(self, self.class.filters, inputs).each do |attr, type, kwargs = {}|
317
+ errors.add(attr, type, **kwargs)
321
318
  end
322
319
  end
323
320
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -34,8 +33,7 @@ module ActiveInteraction
34
33
  false
35
34
  end
36
35
 
37
- #
38
- module ClassMethods
36
+ module ClassMethods # rubocop:disable Style/Documentation
39
37
  # @return [Symbol]
40
38
  #
41
39
  # @see ActiveModel::Translation#i18n_scope
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -24,8 +23,6 @@ module ActiveInteraction
24
23
  # # => nil
25
24
  #
26
25
  # @return [FilterColumn, nil]
27
- #
28
- # @since 1.2.0
29
26
  def column_for_attribute(name)
30
27
  filter = self.class.filters[name]
31
28
  FilterColumn.intern(filter.database_column_type) if filter
@@ -49,9 +46,7 @@ module ActiveInteraction
49
46
  # # => false
50
47
  #
51
48
  # @return [Boolean]
52
- #
53
- # @since 1.5.0
54
- def has_attribute?(name) # rubocop:disable Style/PredicateName
49
+ def has_attribute?(name) # rubocop:disable Naming/PredicateName
55
50
  self.class.filters.key?(name.to_sym)
56
51
  end
57
52
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
@@ -46,9 +45,7 @@ module ActiveInteraction
46
45
 
47
46
  # @return [Boolean]
48
47
  def valid?(*)
49
- if instance_variable_defined?(:@_interaction_valid)
50
- return @_interaction_valid
51
- end
48
+ return @_interaction_valid if instance_variable_defined?(:@_interaction_valid)
52
49
 
53
50
  super
54
51
  end
@@ -75,12 +72,10 @@ module ActiveInteraction
75
72
  return self.result = nil unless valid?
76
73
 
77
74
  self.result = run_callbacks(:execute) do
78
- begin
79
- execute
80
- rescue Interrupt => interrupt
81
- errors.backtrace = interrupt.errors.backtrace || interrupt.backtrace
82
- errors.merge!(interrupt.errors)
83
- end
75
+ execute
76
+ rescue Interrupt => e
77
+ errors.backtrace = e.errors.backtrace || e.backtrace
78
+ errors.merge!(e.errors)
84
79
  end
85
80
  end
86
81
 
@@ -98,8 +93,7 @@ module ActiveInteraction
98
93
  raise e
99
94
  end
100
95
 
101
- #
102
- module ClassMethods
96
+ module ClassMethods # rubocop:disable Style/Documentation
103
97
  def new(*)
104
98
  super.tap do |instance|
105
99
  {
@@ -1,17 +1,15 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- #
5
3
  module ActiveInteraction
6
4
  # Top-level error class. All other errors subclass this.
7
5
  #
8
6
  # @return [Class]
9
7
  Error = Class.new(StandardError)
10
8
 
11
- # Raised if a class name is invalid.
9
+ # Raised if a constant name is invalid.
12
10
  #
13
11
  # @return [Class]
14
- InvalidClassError = Class.new(Error)
12
+ InvalidNameError = Class.new(Error)
15
13
 
16
14
  # Raised if a converter is invalid.
17
15
  #
@@ -151,9 +149,8 @@ module ActiveInteraction
151
149
  if attribute?(attribute) || attribute == :base
152
150
  options = detail.dup
153
151
  error = options.delete(:error)
154
- options[:message] = message
155
152
 
156
- add(attribute, error, options) unless added?(attribute, error, options)
153
+ add(attribute, error, **options.merge(message: message)) unless added?(attribute, error, **options)
157
154
  else
158
155
  merge_message!(attribute, message)
159
156
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'active_support/inflector'
@@ -78,16 +77,16 @@ module ActiveInteraction
78
77
  # to the default value.
79
78
  #
80
79
  # @example
81
- # ActiveInteraction::Filter.new(:example).clean(nil)
80
+ # ActiveInteraction::Filter.new(:example).clean(nil, nil)
82
81
  # # => ActiveInteraction::MissingValueError: example
83
82
  # @example
84
- # ActiveInteraction::Filter.new(:example).clean(0)
83
+ # ActiveInteraction::Filter.new(:example).clean(0, nil)
85
84
  # # => ActiveInteraction::InvalidValueError: example: 0
86
85
  # @example
87
- # ActiveInteraction::Filter.new(:example, default: nil).clean(nil)
86
+ # ActiveInteraction::Filter.new(:example, default: nil).clean(nil, nil)
88
87
  # # => nil
89
88
  # @example
90
- # ActiveInteraction::Filter.new(:example, default: 0).clean(nil)
89
+ # ActiveInteraction::Filter.new(:example, default: 0).clean(nil, nil)
91
90
  # # => ActiveInteraction::InvalidDefaultError: example: 0
92
91
  #
93
92
  # @param value [Object]
@@ -95,7 +94,9 @@ module ActiveInteraction
95
94
  #
96
95
  # @return [Object]
97
96
  #
98
- # @raise (see #cast)
97
+ # @raise [MissingValueError] If the value is missing and there is no
98
+ # default.
99
+ # @raise [InvalidValueError] If the value is invalid.
99
100
  # @raise (see #default)
100
101
  def clean(value, context)
101
102
  value = cast(value, context)
@@ -120,7 +121,7 @@ module ActiveInteraction
120
121
  #
121
122
  # @param context [Base, nil]
122
123
  #
123
- # @return (see #raw_default)
124
+ # @return [Object]
124
125
  #
125
126
  # @raise [NoDefaultError] If the default is missing.
126
127
  # @raise [InvalidDefaultError] If the default is invalid.
@@ -131,8 +132,8 @@ module ActiveInteraction
131
132
  raise InvalidValueError if value.is_a?(GroupedInput)
132
133
 
133
134
  cast(value, context)
134
- rescue InvalidNestedValueError => error
135
- raise InvalidDefaultError, "#{name}: #{value.inspect} (#{error})"
135
+ rescue InvalidNestedValueError => e
136
+ raise InvalidDefaultError, "#{name}: #{value.inspect} (#{e})"
136
137
  rescue InvalidValueError, MissingValueError
137
138
  raise InvalidDefaultError, "#{name}: #{value.inspect}"
138
139
  end
@@ -162,27 +163,6 @@ module ActiveInteraction
162
163
  options.key?(:default)
163
164
  end
164
165
 
165
- # @param value [Object]
166
- # @param _interaction [Base, nil]
167
- #
168
- # @return [Object]
169
- #
170
- # @raise [MissingValueError] If the value is missing and there is no
171
- # default.
172
- # @raise [InvalidValueError] If the value is invalid.
173
- #
174
- # @private
175
- def cast(value, _interaction)
176
- case value
177
- when NilClass
178
- raise MissingValueError, name unless default?
179
-
180
- nil
181
- else
182
- raise InvalidValueError, "#{name}: #{describe(value)}"
183
- end
184
- end
185
-
186
166
  # Gets the type of database column that would represent the filter data.
187
167
  #
188
168
  # @example
@@ -194,25 +174,59 @@ module ActiveInteraction
194
174
  #
195
175
  # @return [Symbol] A database column type. If no sensible mapping exists,
196
176
  # returns `:string`.
197
- #
198
- # @since 1.2.0
199
177
  def database_column_type
200
178
  :string
201
179
  end
202
180
 
203
181
  private
204
182
 
205
- # @param value [Object]
206
- # @return [String]
183
+ # rubocop:disable Metrics/MethodLength
184
+ def cast(value, context, convert: true, reconstantize: true)
185
+ if matches?(value)
186
+ adjust_output(value, context)
187
+ # we can't use `nil?` because BasicObject doesn't have it
188
+ elsif value == nil # rubocop:disable Style/NilComparison
189
+ raise MissingValueError, name unless default?
190
+
191
+ nil
192
+ elsif reconstantize
193
+ send(__method__, value, context,
194
+ convert: convert,
195
+ reconstantize: false
196
+ )
197
+ elsif convert
198
+ send(__method__, convert(value), context,
199
+ convert: false,
200
+ reconstantize: reconstantize
201
+ )
202
+ else
203
+ raise InvalidValueError, "#{name}: #{describe(value)}"
204
+ end
205
+ end
206
+ # rubocop:enable Metrics/MethodLength
207
+
208
+ def matches?(_value)
209
+ false
210
+ end
211
+
212
+ def adjust_output(value, _context)
213
+ value
214
+ end
215
+
216
+ def convert(value)
217
+ value
218
+ end
219
+
220
+ def klass
221
+ @klass ||= Object.const_get(self.class.slug.to_s.camelize, false)
222
+ end
223
+
207
224
  def describe(value)
208
225
  value.inspect
209
226
  rescue NoMethodError
210
227
  "(Object doesn't support #inspect)"
211
228
  end
212
229
 
213
- # @param context [Base, nil]
214
- #
215
- # @return [Object]
216
230
  def raw_default(context)
217
231
  value = options.fetch(:default)
218
232
  return value unless value.is_a?(Proc)
@@ -1,10 +1,7 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ActiveInteraction
5
4
  # A minimal implementation of an `ActiveRecord::ConnectionAdapters::Column`.
6
- #
7
- # @since 1.2.0
8
5
  class FilterColumn
9
6
  # @return [nil]
10
7
  attr_reader :limit