active_interaction 0.5.0 → 0.6.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -3
  3. data/README.md +8 -6
  4. data/lib/active_interaction.rb +5 -3
  5. data/lib/active_interaction/active_model.rb +29 -0
  6. data/lib/active_interaction/base.rb +82 -116
  7. data/lib/active_interaction/errors.rb +79 -5
  8. data/lib/active_interaction/filter.rb +195 -21
  9. data/lib/active_interaction/filters.rb +26 -0
  10. data/lib/active_interaction/filters/array_filter.rb +22 -25
  11. data/lib/active_interaction/filters/boolean_filter.rb +12 -12
  12. data/lib/active_interaction/filters/date_filter.rb +32 -5
  13. data/lib/active_interaction/filters/date_time_filter.rb +34 -7
  14. data/lib/active_interaction/filters/file_filter.rb +12 -9
  15. data/lib/active_interaction/filters/float_filter.rb +13 -11
  16. data/lib/active_interaction/filters/hash_filter.rb +36 -17
  17. data/lib/active_interaction/filters/integer_filter.rb +13 -11
  18. data/lib/active_interaction/filters/model_filter.rb +15 -15
  19. data/lib/active_interaction/filters/string_filter.rb +19 -8
  20. data/lib/active_interaction/filters/symbol_filter.rb +29 -0
  21. data/lib/active_interaction/filters/time_filter.rb +38 -16
  22. data/lib/active_interaction/method_missing.rb +18 -0
  23. data/lib/active_interaction/overload_hash.rb +1 -0
  24. data/lib/active_interaction/validation.rb +19 -0
  25. data/lib/active_interaction/version.rb +1 -1
  26. data/spec/active_interaction/active_model_spec.rb +33 -0
  27. data/spec/active_interaction/base_spec.rb +54 -48
  28. data/spec/active_interaction/errors_spec.rb +99 -0
  29. data/spec/active_interaction/filter_spec.rb +12 -20
  30. data/spec/active_interaction/filters/array_filter_spec.rb +50 -28
  31. data/spec/active_interaction/filters/boolean_filter_spec.rb +15 -15
  32. data/spec/active_interaction/filters/date_filter_spec.rb +30 -18
  33. data/spec/active_interaction/filters/date_time_filter_spec.rb +31 -19
  34. data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
  35. data/spec/active_interaction/filters/float_filter_spec.rb +13 -11
  36. data/spec/active_interaction/filters/hash_filter_spec.rb +38 -29
  37. data/spec/active_interaction/filters/integer_filter_spec.rb +18 -8
  38. data/spec/active_interaction/filters/model_filter_spec.rb +24 -20
  39. data/spec/active_interaction/filters/string_filter_spec.rb +14 -8
  40. data/spec/active_interaction/filters/symbol_filter_spec.rb +24 -0
  41. data/spec/active_interaction/filters/time_filter_spec.rb +33 -69
  42. data/spec/active_interaction/filters_spec.rb +21 -0
  43. data/spec/active_interaction/i18n_spec.rb +0 -15
  44. data/spec/active_interaction/integration/array_interaction_spec.rb +2 -22
  45. data/spec/active_interaction/integration/hash_interaction_spec.rb +5 -25
  46. data/spec/active_interaction/integration/symbol_interaction_spec.rb +5 -0
  47. data/spec/active_interaction/method_missing_spec.rb +69 -0
  48. data/spec/active_interaction/validation_spec.rb +55 -0
  49. data/spec/spec_helper.rb +6 -0
  50. data/spec/support/filters.rb +168 -14
  51. data/spec/support/interactions.rb +11 -13
  52. metadata +31 -13
  53. data/lib/active_interaction/filter_method.rb +0 -13
  54. data/lib/active_interaction/filter_methods.rb +0 -26
  55. data/lib/active_interaction/filters/abstract_date_time_filter.rb +0 -25
  56. data/spec/active_interaction/filter_method_spec.rb +0 -43
  57. data/spec/active_interaction/filter_methods_spec.rb +0 -30
@@ -1,7 +1,81 @@
1
1
  module ActiveInteraction
2
- class InteractionInvalid < ::StandardError; end
3
- class InvalidDefaultValue < ::StandardError; end
4
- class InvalidNestedValue < ::StandardError; end
5
- class InvalidValue < ::StandardError; end
6
- class MissingValue < ::StandardError; end
2
+ # Top-level error class. All other errors subclass this.
3
+ Error = Class.new(StandardError)
4
+
5
+ # Raised if an interaction is invalid.
6
+ InteractionInvalidError = Class.new(Error)
7
+
8
+ # Raised if a class name is invalid.
9
+ InvalidClassError = Class.new(Error)
10
+
11
+ # Raised if a default value is invalid.
12
+ InvalidDefaultError = Class.new(Error)
13
+
14
+ # Raised if a filter has an invalid definition.
15
+ InvalidFilterError = Class.new(Error)
16
+
17
+ # Raised if a user-supplied value is invalid.
18
+ InvalidValueError = Class.new(Error)
19
+
20
+ # Raised if there is no default value.
21
+ NoDefaultError = Class.new(Error)
22
+
23
+ # Raised if a filter cannot be found.
24
+ MissingFilterError = Class.new(Error)
25
+
26
+ # Raised if no value is given.
27
+ MissingValueError = Class.new(Error)
28
+
29
+ # A small extension to provide symbolic error messages to make introspecting
30
+ # and testing easier.
31
+ #
32
+ # @since 0.6.0
33
+ class Errors < ActiveModel::Errors
34
+ # A hash mapping attributes to arrays of symbolic messages.
35
+ #
36
+ # @return [Hash{Symbol => Array<Symbol>}]
37
+ attr_reader :symbolic
38
+
39
+ # Adds a symbolic error message to an attribute.
40
+ #
41
+ # @param attribute [Symbol] The attribute to add an error to.
42
+ # @param symbol [Symbol] The symbolic error to add.
43
+ # @param message [String, Symbol, Proc]
44
+ # @param options [Hash]
45
+ #
46
+ # @example Adding a symbolic error.
47
+ # errors.add_sym(:attribute)
48
+ # errors.symbolic
49
+ # # => {:attribute=>[:invalid]}
50
+ # errors.messages
51
+ # # => {:attribute=>["is invalid"]}
52
+ #
53
+ # @return [Hash{Symbol => Array<Symbol>}]
54
+ #
55
+ # @see ActiveModel::Errors#add
56
+ def add_sym(attribute, symbol = :invalid, message = nil, options = {})
57
+ add(attribute, message || symbol, options)
58
+
59
+ symbolic[attribute] ||= []
60
+ symbolic[attribute] << symbol
61
+ end
62
+
63
+ # @private
64
+ def initialize(*args)
65
+ @symbolic = {}
66
+ super
67
+ end
68
+
69
+ # @private
70
+ def initialize_dup(other)
71
+ @symbolic = other.symbolic.dup
72
+ super
73
+ end
74
+
75
+ # @private
76
+ def clear
77
+ symbolic.clear
78
+ super
79
+ end
80
+ end
7
81
  end
@@ -1,32 +1,206 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module ActiveInteraction
2
- # @!macro [new] attribute_method_params
3
- # @param *attributes [Symbol] One or more attributes to create.
4
- # @param options [Hash]
5
- # @option options [Boolean] :allow_nil Allow a `nil` value.
6
- # @option options [Object] :default Value to use if `nil` is given.
4
+ # @!macro [new] filter_method_params
5
+ # @param *attributes [Array<Symbol>] attributes to create
6
+ # @param options [Hash{Symbol => Object}]
7
+ #
8
+ # @option options [Object] :default fallback value if `nil` is given
7
9
 
8
- # @private
10
+ # Describes an input filter for an interaction.
11
+ #
12
+ # @since 0.6.0
9
13
  class Filter
10
- def self.factory(type)
11
- klass = "#{type.to_s.camelize}Filter"
14
+ # @return [Regexp]
15
+ CLASS_REGEXP = /\AActiveInteraction::([A-Z]\w*)Filter\z/
16
+ private_constant :CLASS_REGEXP
17
+
18
+ # @return [Hash{Symbol => Class}]
19
+ CLASSES = {}
20
+ private_constant :CLASSES
21
+
22
+ # @return [Filters]
23
+ attr_reader :filters
24
+
25
+ # @return [Symbol]
26
+ attr_reader :name
27
+
28
+ # @return [Hash{Symbol => Object}]
29
+ attr_reader :options
30
+
31
+ # Filters that allow sub-filters, like arrays and hashes, must be able to
32
+ # use `hash` as a part of their DSL. To keep things consistent, `hash` is
33
+ # undefined on all filters. Realistically, {#name} should be unique
34
+ # enough to use in place of {#hash}.
35
+ undef_method :hash
12
36
 
13
- raise NoMethodError unless ActiveInteraction.const_defined?(klass)
37
+ class << self
38
+ # Get the filter associated with a symbol.
39
+ #
40
+ # @example
41
+ # ActiveInteraction::Filter.factory(:boolean)
42
+ # # => ActiveInteraction::BooleanFilter
43
+ #
44
+ # @example
45
+ # ActiveInteraction::Filter.factory(:invalid)
46
+ # # => ActiveInteraction::MissingFilterError: :invalid
47
+ #
48
+ # @param slug [Symbol]
49
+ #
50
+ # @return [Class]
51
+ #
52
+ # @raise [MissingFilterError] if the slug doesn't map to a filter
53
+ #
54
+ # @see .slug
55
+ def factory(slug)
56
+ CLASSES.fetch(slug)
57
+ rescue KeyError
58
+ raise MissingFilterError, slug.inspect
59
+ end
60
+
61
+ # Convert the class name into a short symbol.
62
+ #
63
+ # @example
64
+ # ActiveInteraction::BooleanFilter.slug
65
+ # # => :boolean
66
+ #
67
+ # @example
68
+ # ActiveInteraction::Filter.slug
69
+ # # => ActiveInteraction::InvalidClassError: ActiveInteraction::Filter
70
+ #
71
+ # @return [Symbol]
72
+ #
73
+ # @raise [InvalidClassError] if the filter doesn't have a valid slug
74
+ #
75
+ # @see .factory
76
+ def slug
77
+ match = CLASS_REGEXP.match(name)
78
+ raise InvalidClassError, name unless match
79
+ match.captures.first.underscore.to_sym
80
+ end
14
81
 
15
- ActiveInteraction.const_get(klass)
82
+ # @param klass [Class]
83
+ #
84
+ # @return [nil]
85
+ #
86
+ # @private
87
+ def inherited(klass)
88
+ begin
89
+ CLASSES[klass.slug] = klass
90
+ rescue InvalidClassError
91
+ end
92
+
93
+ super
94
+ end
16
95
  end
17
96
 
18
- def self.prepare(key, value, options = {}, &block)
97
+ # @param name [Symbol]
98
+ # @param options [Hash{Symbol => Object}]
99
+ #
100
+ # @option options [Object] :default fallback value to use if `nil` is given
101
+ def initialize(name, options = {}, &block)
102
+ @name = name
103
+ @options = options.dup
104
+ @filters = Filters.new
105
+
106
+ instance_eval(&block) if block_given?
107
+ end
108
+
109
+ # Convert a value into the expected type. If no value is given, fall back
110
+ # to the default value.
111
+ #
112
+ # @example
113
+ # ActiveInteraction::Filter.new(:example).clean(nil)
114
+ # # => ActiveInteraction::MissingValueError: example
115
+ #
116
+ # @example
117
+ # ActiveInteraction::Filter.new(:example).clean(0)
118
+ # # => ActiveInteraction::InvalidValueError: example: 0
119
+ #
120
+ # @example
121
+ # ActiveInteraction::Filter.new(:example, default: nil).clean(nil)
122
+ # # => nil
123
+ #
124
+ # @example
125
+ # ActiveInteraction::Filter.new(:example, default: 0).clean(nil)
126
+ # # => ActiveInteraction::InvalidDefault: example: 0
127
+ #
128
+ # @param value [Object]
129
+ #
130
+ # @return [Object]
131
+ #
132
+ # @raise (see #cast)
133
+ # @raise (see #default)
134
+ #
135
+ # @see #default
136
+ def clean(value)
137
+ value = cast(value)
138
+ if value.nil?
139
+ default
140
+ else
141
+ value
142
+ end
143
+ end
144
+
145
+ # Get the default value.
146
+ #
147
+ # @example
148
+ # ActiveInteraction::Filter.new(:example, default: nil).default
149
+ # # => nil
150
+ #
151
+ # @example
152
+ # ActiveInteraction::Filter.new(:example, default: 0).default
153
+ # # => ActiveInteraction::InvalidDefaultError: example: 0
154
+ #
155
+ # @example
156
+ # ActiveInteraction::Filter.new(:example).default
157
+ # # => ActiveInteraction::NoDefaultError: example
158
+ #
159
+ # @return [Object]
160
+ #
161
+ # @raise [InvalidDefaultError] if the default value is invalid
162
+ # @raise [NoDefaultError] if there is no default value
163
+ def default
164
+ raise NoDefaultError, name unless has_default?
165
+
166
+ cast(options[:default])
167
+ rescue InvalidValueError, MissingValueError
168
+ raise InvalidDefaultError, "#{name}: #{options[:default].inspect}"
169
+ end
170
+
171
+ # Tells if this filter has a default value.
172
+ #
173
+ # @example
174
+ # filter = ActiveInteraction::Filter.new(:example)
175
+ # filter.has_default?
176
+ # # => false
177
+ #
178
+ # @example
179
+ # filter = ActiveInteraction::Filter.new(:example, default: nil)
180
+ # filter.has_default?
181
+ # # => true
182
+ #
183
+ # @return [Boolean]
184
+ def has_default?
185
+ options.has_key?(:default)
186
+ end
187
+
188
+ # @param value [Object]
189
+ #
190
+ # @return [nil]
191
+ #
192
+ # @raise [InvalidValueError] if the value is invalid
193
+ # @raise [MissingValueError] if the value is missing and the input is required
194
+ #
195
+ # @private
196
+ def cast(value)
19
197
  case value
20
- when NilClass
21
- if options[:allow_nil]
22
- nil
23
- elsif options.has_key?(:default)
24
- options[:default]
25
- else
26
- raise MissingValue
27
- end
28
- else
29
- raise InvalidValue
198
+ when NilClass
199
+ raise MissingValueError, name unless has_default?
200
+
201
+ nil
202
+ else
203
+ raise InvalidValueError, "#{name}: #{value.inspect}"
30
204
  end
31
205
  end
32
206
  end
@@ -0,0 +1,26 @@
1
+ module ActiveInteraction
2
+ # A collection of {Filter}s.
3
+ #
4
+ # @since 0.6.0
5
+ class Filters
6
+ include Enumerable
7
+
8
+ def initialize
9
+ @filters = []
10
+ end
11
+
12
+ # @return [Enumerator]
13
+ def each(&block)
14
+ @filters.each(&block)
15
+ end
16
+
17
+ # @param filter [Filter]
18
+ #
19
+ # @return [Filters]
20
+ def add(filter)
21
+ @filters << filter
22
+
23
+ self
24
+ end
25
+ end
26
+ end
@@ -3,8 +3,8 @@ module ActiveInteraction
3
3
  # Creates accessors for the attributes and ensures that values passed to
4
4
  # the attributes are Arrays.
5
5
  #
6
- # @macro attribute_method_params
7
- # @param block [Proc] A filter method to apply to each element.
6
+ # @macro filter_method_params
7
+ # @param block [Proc] filter method to apply to each element
8
8
  #
9
9
  # @example
10
10
  # array :ids
@@ -16,43 +16,40 @@ module ActiveInteraction
16
16
  #
17
17
  # @example An Array of Integers where some or all are nil
18
18
  # array :ids do
19
- # integer allow_nil: true
19
+ # integer default: nil
20
20
  # end
21
21
  #
22
+ # @since 0.1.0
23
+ #
22
24
  # @method self.array(*attributes, options = {}, &block)
23
25
  end
24
26
 
25
27
  # @private
26
28
  class ArrayFilter < Filter
27
- def self.prepare(key, value, options = {}, &block)
29
+ include MethodMissing
30
+
31
+ def cast(value)
28
32
  case value
29
- when Array
30
- convert_values(value, &block)
31
- else
32
- super
33
+ when Array
34
+ return value if filters.none?
35
+
36
+ filter = filters.first
37
+ value.map { |e| filter.clean(e) }
38
+ else
39
+ super
33
40
  end
34
41
  end
35
42
 
36
- def self.convert_values(values, &block)
37
- return values.dup unless block_given?
43
+ def method_missing(*args, &block)
44
+ super do |klass, names, options|
45
+ filter = klass.new(name, options, &block)
38
46
 
39
- method = get_filter_method(FilterMethods.evaluate(&block))
40
- values.map do |value|
41
- Filter.factory(method.method_name).
42
- prepare(method.attribute, value, method.options, &method.block)
43
- end
44
- rescue InvalidValue, MissingValue
45
- raise InvalidNestedValue
46
- end
47
- private_class_method :convert_values
47
+ raise InvalidFilterError, 'multiple nested filters' if filters.any?
48
+ raise InvalidFilterError, 'nested name' unless names.empty?
49
+ raise InvalidDefaultError, 'nested default' if filter.has_default?
48
50
 
49
- def self.get_filter_method(filter_methods)
50
- if filter_methods.count > 1
51
- raise ArgumentError, 'Array filter blocks can only contain one filter.'
52
- else
53
- filter_methods.first
51
+ filters.add(filter)
54
52
  end
55
53
  end
56
- private_class_method :get_filter_method
57
54
  end
58
55
  end
@@ -1,29 +1,29 @@
1
1
  module ActiveInteraction
2
2
  class Base
3
3
  # Creates accessors for the attributes and ensures that values passed to
4
- # the attributes are Arrays. The String `"1"` is converted to `true` and
5
- # `"0"` is converted to `false`.
4
+ # the attributes are Booleans. The String `"1"` is converted to `true`
5
+ # and `"0"` is converted to `false`.
6
6
  #
7
- # @macro attribute_method_params
7
+ # @macro filter_method_params
8
8
  #
9
9
  # @example
10
10
  # boolean :subscribed
11
11
  #
12
+ # @since 0.1.0
13
+ #
12
14
  # @method self.boolean(*attributes, options = {})
13
15
  end
14
16
 
15
17
  # @private
16
18
  class BooleanFilter < Filter
17
- def self.prepare(key, value, options = {}, &block)
19
+ def cast(value)
18
20
  case value
19
- when TrueClass, FalseClass
20
- value
21
- when '0'
22
- false
23
- when '1'
24
- true
25
- else
26
- super
21
+ when FalseClass, '0'
22
+ false
23
+ when TrueClass, '1'
24
+ true
25
+ else
26
+ super
27
27
  end
28
28
  end
29
29
  end