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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -3
- data/README.md +8 -6
- data/lib/active_interaction.rb +5 -3
- data/lib/active_interaction/active_model.rb +29 -0
- data/lib/active_interaction/base.rb +82 -116
- data/lib/active_interaction/errors.rb +79 -5
- data/lib/active_interaction/filter.rb +195 -21
- data/lib/active_interaction/filters.rb +26 -0
- data/lib/active_interaction/filters/array_filter.rb +22 -25
- data/lib/active_interaction/filters/boolean_filter.rb +12 -12
- data/lib/active_interaction/filters/date_filter.rb +32 -5
- data/lib/active_interaction/filters/date_time_filter.rb +34 -7
- data/lib/active_interaction/filters/file_filter.rb +12 -9
- data/lib/active_interaction/filters/float_filter.rb +13 -11
- data/lib/active_interaction/filters/hash_filter.rb +36 -17
- data/lib/active_interaction/filters/integer_filter.rb +13 -11
- data/lib/active_interaction/filters/model_filter.rb +15 -15
- data/lib/active_interaction/filters/string_filter.rb +19 -8
- data/lib/active_interaction/filters/symbol_filter.rb +29 -0
- data/lib/active_interaction/filters/time_filter.rb +38 -16
- data/lib/active_interaction/method_missing.rb +18 -0
- data/lib/active_interaction/overload_hash.rb +1 -0
- data/lib/active_interaction/validation.rb +19 -0
- data/lib/active_interaction/version.rb +1 -1
- data/spec/active_interaction/active_model_spec.rb +33 -0
- data/spec/active_interaction/base_spec.rb +54 -48
- data/spec/active_interaction/errors_spec.rb +99 -0
- data/spec/active_interaction/filter_spec.rb +12 -20
- data/spec/active_interaction/filters/array_filter_spec.rb +50 -28
- data/spec/active_interaction/filters/boolean_filter_spec.rb +15 -15
- data/spec/active_interaction/filters/date_filter_spec.rb +30 -18
- data/spec/active_interaction/filters/date_time_filter_spec.rb +31 -19
- data/spec/active_interaction/filters/file_filter_spec.rb +7 -7
- data/spec/active_interaction/filters/float_filter_spec.rb +13 -11
- data/spec/active_interaction/filters/hash_filter_spec.rb +38 -29
- data/spec/active_interaction/filters/integer_filter_spec.rb +18 -8
- data/spec/active_interaction/filters/model_filter_spec.rb +24 -20
- data/spec/active_interaction/filters/string_filter_spec.rb +14 -8
- data/spec/active_interaction/filters/symbol_filter_spec.rb +24 -0
- data/spec/active_interaction/filters/time_filter_spec.rb +33 -69
- data/spec/active_interaction/filters_spec.rb +21 -0
- data/spec/active_interaction/i18n_spec.rb +0 -15
- data/spec/active_interaction/integration/array_interaction_spec.rb +2 -22
- data/spec/active_interaction/integration/hash_interaction_spec.rb +5 -25
- data/spec/active_interaction/integration/symbol_interaction_spec.rb +5 -0
- data/spec/active_interaction/method_missing_spec.rb +69 -0
- data/spec/active_interaction/validation_spec.rb +55 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/filters.rb +168 -14
- data/spec/support/interactions.rb +11 -13
- metadata +31 -13
- data/lib/active_interaction/filter_method.rb +0 -13
- data/lib/active_interaction/filter_methods.rb +0 -26
- data/lib/active_interaction/filters/abstract_date_time_filter.rb +0 -25
- data/spec/active_interaction/filter_method_spec.rb +0 -43
- data/spec/active_interaction/filter_methods_spec.rb +0 -30
@@ -1,7 +1,81 @@
|
|
1
1
|
module ActiveInteraction
|
2
|
-
class
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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]
|
3
|
-
# @param *attributes [Symbol]
|
4
|
-
# @param options [Hash]
|
5
|
-
#
|
6
|
-
# @option options [Object] :default
|
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
|
-
#
|
10
|
+
# Describes an input filter for an interaction.
|
11
|
+
#
|
12
|
+
# @since 0.6.0
|
9
13
|
class Filter
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
7
|
-
# @param block [Proc]
|
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
|
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
|
-
|
29
|
+
include MethodMissing
|
30
|
+
|
31
|
+
def cast(value)
|
28
32
|
case value
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
37
|
-
|
43
|
+
def method_missing(*args, &block)
|
44
|
+
super do |klass, names, options|
|
45
|
+
filter = klass.new(name, options, &block)
|
38
46
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
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
|
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
|
19
|
+
def cast(value)
|
18
20
|
case value
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|