active_interaction 0.5.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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