active_interaction 1.1.7 → 1.2.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -1
  3. data/README.md +3 -2
  4. data/lib/active_interaction.rb +7 -1
  5. data/lib/active_interaction/base.rb +61 -13
  6. data/lib/active_interaction/concerns/runnable.rb +2 -18
  7. data/lib/active_interaction/concerns/transactable.rb +72 -0
  8. data/lib/active_interaction/errors.rb +7 -0
  9. data/lib/active_interaction/filter.rb +23 -2
  10. data/lib/active_interaction/filter_column.rb +59 -0
  11. data/lib/active_interaction/filters/abstract_date_time_filter.rb +14 -0
  12. data/lib/active_interaction/filters/abstract_filter.rb +4 -7
  13. data/lib/active_interaction/filters/abstract_numeric_filter.rb +4 -0
  14. data/lib/active_interaction/filters/boolean_filter.rb +4 -0
  15. data/lib/active_interaction/filters/date_time_filter.rb +3 -0
  16. data/lib/active_interaction/filters/decimal_filter.rb +54 -0
  17. data/lib/active_interaction/filters/file_filter.rb +4 -0
  18. data/lib/active_interaction/filters/time_filter.rb +4 -0
  19. data/lib/active_interaction/grouped_input.rb +24 -0
  20. data/lib/active_interaction/locale/en.yml +1 -0
  21. data/lib/active_interaction/modules/input_processor.rb +40 -0
  22. data/lib/active_interaction/version.rb +1 -1
  23. data/spec/active_interaction/base_spec.rb +90 -29
  24. data/spec/active_interaction/concerns/runnable_spec.rb +0 -26
  25. data/spec/active_interaction/concerns/transactable_spec.rb +114 -0
  26. data/spec/active_interaction/filter_column_spec.rb +96 -0
  27. data/spec/active_interaction/filter_spec.rb +15 -11
  28. data/spec/active_interaction/filters/array_filter_spec.rb +13 -5
  29. data/spec/active_interaction/filters/boolean_filter_spec.rb +6 -0
  30. data/spec/active_interaction/filters/date_filter_spec.rb +76 -5
  31. data/spec/active_interaction/filters/date_time_filter_spec.rb +87 -5
  32. data/spec/active_interaction/filters/decimal_filter_spec.rb +70 -0
  33. data/spec/active_interaction/filters/file_filter_spec.rb +11 -3
  34. data/spec/active_interaction/filters/float_filter_spec.rb +12 -4
  35. data/spec/active_interaction/filters/hash_filter_spec.rb +16 -8
  36. data/spec/active_interaction/filters/integer_filter_spec.rb +12 -4
  37. data/spec/active_interaction/filters/model_filter_spec.rb +12 -5
  38. data/spec/active_interaction/filters/string_filter_spec.rb +11 -3
  39. data/spec/active_interaction/filters/symbol_filter_spec.rb +10 -2
  40. data/spec/active_interaction/filters/time_filter_spec.rb +87 -5
  41. data/spec/active_interaction/grouped_input_spec.rb +19 -0
  42. data/spec/active_interaction/modules/input_processor_spec.rb +75 -0
  43. data/spec/support/filters.rb +6 -0
  44. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ebbd60d2d495d3011785f7fb78478f6b6b4a2d9
4
- data.tar.gz: e52b7f93aff786d4a8be65e36121ecc5b30eaf3b
3
+ metadata.gz: 1c1d5b914798cc9b8b70729d645f8ef4a0d4031f
4
+ data.tar.gz: ae3a8b0db8afdd311a44d9c73e1fcb2788d11086
5
5
  SHA512:
6
- metadata.gz: b9b58d141cca46ad7b9b0dba1373c9e0cb159cc04d9831b0660df7d1941347d94b1e63b8f9f063d0425240173757f4fff00453650299d46f52059b5d49ccb1f9
7
- data.tar.gz: 14bee4cebb9847f4eb759016e9846e7171d6a7647b78516c4389686dbb1e6ed7f8e94438a63d1a83c028f446c86d4289f57ededca0af5afaabc0887ff742d60f
6
+ metadata.gz: 95496b5a5f1b4d837b7d2e1231c38ae6522aac69fd0bcc5f5c70739f26428299e4aaf89d43791558b97c5f90b5e4e422b9ade2db76aec0371da27fcb078a9fa8
7
+ data.tar.gz: 7fcb37ddb4a3bd9beb005401448304314a6996fe99c0b4e6ddcb1f684c456cd861fcfdaddc75aa7a7ff24571f1a52f2783406c6ff8af519d5295d6c1287df61d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # [Master][]
2
2
 
3
+ # [1.2.0][] (2014-04-30)
4
+
5
+ - Add a decimal filter.
6
+ - Add support for disabling and modifying transactions through the
7
+ `transaction` helper method.
8
+ - Add support for `column_for_attribute` which provides better interoperability with gems like Formtastic and Simple Form.
9
+
3
10
  # [1.1.7][] (2014-04-30)
4
11
 
5
12
  - Fix a bug that leaked validators among all child classes.
@@ -186,7 +193,8 @@
186
193
 
187
194
  - Initial release.
188
195
 
189
- [master]: https://github.com/orgsync/active_interaction/compare/v1.1.7...master
196
+ [master]: https://github.com/orgsync/active_interaction/compare/v1.2.0...master
197
+ [1.2.0]: https://github.com/orgsync/active_interaction/compare/v1.1.7...v1.2.0
190
198
  [1.1.7]: https://github.com/orgsync/active_interaction/compare/v1.1.6...v1.1.7
191
199
  [1.1.6]: https://github.com/orgsync/active_interaction/compare/v1.1.5...v1.1.6
192
200
  [1.1.5]: https://github.com/orgsync/active_interaction/compare/v1.1.4...v1.1.5
data/README.md CHANGED
@@ -25,7 +25,7 @@ This project uses [semantic versioning][13].
25
25
  Add it to your Gemfile:
26
26
 
27
27
  ```ruby
28
- gem 'active_interaction', '~> 1.1'
28
+ gem 'active_interaction', '~> 1.2'
29
29
  ```
30
30
 
31
31
  And then execute:
@@ -94,7 +94,7 @@ end
94
94
  You may have noticed that ActiveInteraction::Base quacks like
95
95
  ActiveRecord::Base. It can use validations from your Rails application
96
96
  and check option validity with `valid?`. Any errors are added to
97
- `errors` which works exactly like an ActiveRecord model. Additionally,
97
+ `errors` which works exactly like an ActiveRecord model. By default,
98
98
  everything within the `execute` method is run in a transaction if
99
99
  ActiveRecord is available.
100
100
 
@@ -247,6 +247,7 @@ hsilgne:
247
247
  boolean: naeloob
248
248
  date: etad
249
249
  date_time: emit etad
250
+ decimal: lamiced
250
251
  file: elif
251
252
  float: taolf
252
253
  hash: hsah
@@ -8,10 +8,15 @@ require 'active_interaction/errors'
8
8
  require 'active_interaction/concerns/active_modelable'
9
9
  require 'active_interaction/concerns/hashable'
10
10
  require 'active_interaction/concerns/missable'
11
+ require 'active_interaction/concerns/transactable'
11
12
  require 'active_interaction/concerns/runnable'
12
13
 
14
+ require 'active_interaction/grouped_input'
15
+
16
+ require 'active_interaction/modules/input_processor'
13
17
  require 'active_interaction/modules/validation'
14
18
 
19
+ require 'active_interaction/filter_column'
15
20
  require 'active_interaction/filter'
16
21
  require 'active_interaction/filters/abstract_filter'
17
22
  require 'active_interaction/filters/abstract_date_time_filter'
@@ -20,6 +25,7 @@ require 'active_interaction/filters/array_filter'
20
25
  require 'active_interaction/filters/boolean_filter'
21
26
  require 'active_interaction/filters/date_filter'
22
27
  require 'active_interaction/filters/date_time_filter'
28
+ require 'active_interaction/filters/decimal_filter'
23
29
  require 'active_interaction/filters/file_filter'
24
30
  require 'active_interaction/filters/float_filter'
25
31
  require 'active_interaction/filters/hash_filter'
@@ -41,5 +47,5 @@ I18n.load_path << File.expand_path(
41
47
  #
42
48
  # @since 1.0.0
43
49
  #
44
- # @version 1.1.7
50
+ # @version 1.2.0
45
51
  module ActiveInteraction end
@@ -58,6 +58,28 @@ module ActiveInteraction
58
58
  #
59
59
  # @raise (see ActiveInteraction::Runnable::ClassMethods#run!)
60
60
 
61
+ # @!method transaction(enable, options = {})
62
+ # Configure transactions by enabling or disabling them and setting
63
+ # their options.
64
+ #
65
+ # @example Disable transactions
66
+ # Class.new(ActiveInteraction::Base) do
67
+ # transaction false
68
+ # end
69
+ #
70
+ # @example Use different transaction options
71
+ # Class.new(ActiveInteraction::Base) do
72
+ # transaction true, isolation: :serializable
73
+ # end
74
+ #
75
+ # @param enable [Boolean] Should transactions be enabled?
76
+ # @param options [Hash] Options to pass to
77
+ # `ActiveRecord::Base.transaction`.
78
+ #
79
+ # @return [nil]
80
+ #
81
+ # @since 1.2.0
82
+
61
83
  # Get or set the description.
62
84
  #
63
85
  # @example
@@ -104,7 +126,7 @@ module ActiveInteraction
104
126
  # @param name [Symbol]
105
127
  # @param options [Hash]
106
128
  def add_filter(klass, name, options, &block)
107
- fail InvalidFilterError, name.inspect if reserved?(name)
129
+ fail InvalidFilterError, name.inspect if InputProcessor.reserved?(name)
108
130
 
109
131
  initialize_filter(klass.new(name, options, &block))
110
132
  end
@@ -148,13 +170,6 @@ module ActiveInteraction
148
170
 
149
171
  filter.default if filter.default?
150
172
  end
151
-
152
- # @param symbol [Symbol]
153
- #
154
- # @return [Boolean]
155
- def reserved?(symbol)
156
- symbol.to_s.start_with?('_interaction_')
157
- end
158
173
  end
159
174
 
160
175
  # @param inputs [Hash{Symbol => Object}] Attribute values to set.
@@ -166,6 +181,31 @@ module ActiveInteraction
166
181
  process_inputs(inputs.symbolize_keys)
167
182
  end
168
183
 
184
+ # Returns the column object for the named filter.
185
+ #
186
+ # @param name [Symbol] The name of a filter.
187
+ #
188
+ # @example
189
+ # class Interaction < ActiveInteraction::Base
190
+ # string :email, default: nil
191
+ #
192
+ # def execute; end
193
+ # end
194
+ #
195
+ # Interaction.new.column_for_attribute(:email)
196
+ # # => #<ActiveInteraction::FilterColumn:0x007faebeb2a6c8 @type=:string>
197
+ #
198
+ # Interaction.new.column_for_attribute(:not_a_filter)
199
+ # # => nil
200
+ #
201
+ # @return [FilterColumn, nil]
202
+ #
203
+ # @since 1.2.0
204
+ def column_for_attribute(name)
205
+ filter = self.class.filters[name]
206
+ FilterColumn.intern(filter.database_column_type) if filter
207
+ end
208
+
169
209
  # @!method compose(other, inputs = {})
170
210
  # Run another interaction and return its result. If the other interaction
171
211
  # fails, halt execution.
@@ -179,9 +219,9 @@ module ActiveInteraction
179
219
  # @abstract
180
220
  #
181
221
  # Runs the business logic associated with the interaction. This method is
182
- # only run when there are no validation errors. The return value is
183
- # placed into {#result}. This method is run in a transaction if
184
- # ActiveRecord is available.
222
+ # only run when there are no validation errors. The return value is
223
+ # placed into {#result}. By default, this method is run in a transaction
224
+ # if ActiveRecord is available (see {.transaction}).
185
225
  #
186
226
  # @raise (see ActiveInteraction::Runnable#execute)
187
227
 
@@ -200,11 +240,19 @@ module ActiveInteraction
200
240
  # @param inputs [Hash{Symbol => Object}]
201
241
  def process_inputs(inputs)
202
242
  inputs.each do |key, value|
203
- fail InvalidValueError, key.inspect if self.class.send(:reserved?, key)
243
+ fail InvalidValueError, key.inspect if InputProcessor.reserved?(key)
204
244
 
205
- instance_variable_set("@#{key}", value) if respond_to?(key)
245
+ populate_reader(key, value)
206
246
  end
207
247
 
248
+ populate_filters(InputProcessor.process(inputs))
249
+ end
250
+
251
+ def populate_reader(key, value)
252
+ instance_variable_set("@#{key}", value) if respond_to?(key)
253
+ end
254
+
255
+ def populate_filters(inputs)
208
256
  self.class.filters.each do |name, filter|
209
257
  begin
210
258
  public_send("#{name}=", filter.clean(inputs[name]))
@@ -1,22 +1,5 @@
1
1
  # coding: utf-8
2
2
 
3
- begin
4
- require 'active_record'
5
- rescue LoadError
6
- # rubocop:disable Documentation
7
- module ActiveRecord
8
- Rollback = Class.new(ActiveInteraction::Error)
9
-
10
- class Base
11
- def self.transaction(*)
12
- yield
13
- rescue Rollback
14
- # rollbacks are silently swallowed
15
- end
16
- end
17
- end
18
- end
19
-
20
3
  module ActiveInteraction
21
4
  # @abstract Include and override {#execute} to implement a custom Runnable
22
5
  # class.
@@ -30,6 +13,7 @@ module ActiveInteraction
30
13
  module Runnable
31
14
  extend ActiveSupport::Concern
32
15
  include ActiveModel::Validations
16
+ include ActiveInteraction::Transactable
33
17
 
34
18
  included do
35
19
  define_callbacks :execute
@@ -98,7 +82,7 @@ module ActiveInteraction
98
82
  def run
99
83
  return unless valid?
100
84
 
101
- self.result = ActiveRecord::Base.transaction do
85
+ self.result = transaction do
102
86
  begin
103
87
  run_callbacks(:execute) { execute }
104
88
  rescue Interrupt => interrupt
@@ -0,0 +1,72 @@
1
+ # coding: utf-8
2
+
3
+ begin
4
+ require 'active_record'
5
+ rescue LoadError
6
+ module ActiveRecord # rubocop:disable Documentation
7
+ Rollback = Class.new(ActiveInteraction::Error)
8
+
9
+ class Base # rubocop:disable Documentation
10
+ def self.transaction(*)
11
+ yield
12
+ rescue Rollback
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ module ActiveInteraction
19
+ # @private
20
+ #
21
+ # Execute code in a transaction. If ActiveRecord isn't available, don't do
22
+ # anything special.
23
+ #
24
+ # @since 1.2.0
25
+ module Transactable
26
+ extend ActiveSupport::Concern
27
+
28
+ # @yield []
29
+ def transaction
30
+ return unless block_given?
31
+
32
+ if self.class.transaction?
33
+ ActiveRecord::Base.transaction(self.class.transaction_options) do
34
+ yield
35
+ end
36
+ else
37
+ yield
38
+ end
39
+ end
40
+
41
+ module ClassMethods # rubocop:disable Documentation
42
+ # @param enable [Boolean]
43
+ # @param options [Hash]
44
+ #
45
+ # @return [nil]
46
+ def transaction(enable, options = {})
47
+ @_interaction_transaction_enabled = enable
48
+ @_interaction_transaction_options = options
49
+
50
+ nil
51
+ end
52
+
53
+ # @return [Boolean]
54
+ def transaction?
55
+ unless defined?(@_interaction_transaction_enabled)
56
+ @_interaction_transaction_enabled = true
57
+ end
58
+
59
+ @_interaction_transaction_enabled
60
+ end
61
+
62
+ # @return [Hash]
63
+ def transaction_options
64
+ unless defined?(@_interaction_transaction_options)
65
+ @_interaction_transaction_options = {}
66
+ end
67
+
68
+ @_interaction_transaction_options
69
+ end
70
+ end
71
+ end
72
+ end
@@ -47,6 +47,13 @@ module ActiveInteraction
47
47
  # @return [Class]
48
48
  NoDefaultError = Class.new(Error)
49
49
 
50
+ # Raised if a reserved name is used.
51
+ #
52
+ # @return [Class]
53
+ #
54
+ # @since 1.2.0
55
+ ReservedNameError = Class.new(Error)
56
+
50
57
  # Raised if a user-supplied value to a nested hash input is invalid.
51
58
  #
52
59
  # @return [Class]
@@ -67,9 +67,11 @@ module ActiveInteraction
67
67
  #
68
68
  # @see .factory
69
69
  def slug
70
- match = CLASS_REGEXP.match(name)
70
+ return @slug if defined?(@slug)
71
+
72
+ match = name[CLASS_REGEXP, 1]
71
73
  fail InvalidClassError, name unless match
72
- match.captures.first.underscore.to_sym
74
+ @slug = match.underscore.to_sym
73
75
  end
74
76
 
75
77
  # @param klass [Class]
@@ -145,6 +147,8 @@ module ActiveInteraction
145
147
  fail NoDefaultError, name unless default?
146
148
 
147
149
  value = raw_default
150
+ fail InvalidValueError if value.is_a?(GroupedInput)
151
+
148
152
  cast(value)
149
153
  rescue InvalidValueError, MissingValueError
150
154
  raise InvalidDefaultError, "#{name}: #{value.inspect}"
@@ -195,6 +199,23 @@ module ActiveInteraction
195
199
  end
196
200
  end
197
201
 
202
+ # Gets the type of database column that would represent the filter data.
203
+ #
204
+ # @example
205
+ # ActiveInteraction::TimeFilter.new(Time.now).database_column_type
206
+ # # => :datetime
207
+ # @example
208
+ # ActiveInteraction::Filter.new(:example).database_column_type
209
+ # # => :string
210
+ #
211
+ # @return [Symbol] A database column type. If no sensible mapping exists,
212
+ # returns `:string`.
213
+ #
214
+ # @since 1.2.0
215
+ def database_column_type
216
+ :string
217
+ end
218
+
198
219
  private
199
220
 
200
221
  # @return [Object]
@@ -0,0 +1,59 @@
1
+ # coding: utf-8
2
+
3
+ module ActiveInteraction
4
+ # A minimal implementation of an `ActiveRecord::ConnectionAdapters::Column`.
5
+ #
6
+ # @since 1.2.0
7
+ class FilterColumn
8
+ # @return [nil]
9
+ attr_reader :limit
10
+
11
+ # @return [Symbol]
12
+ attr_reader :type
13
+
14
+ class << self
15
+ # Find or create the `FilterColumn` for a specific type.
16
+ #
17
+ # @param type [Symbol] A database column type.
18
+ #
19
+ # @example
20
+ # FilterColumn.intern(:string)
21
+ # # => #<ActiveInteraction::FilterColumn:0x007feeaa649c @type=:string>
22
+ #
23
+ # FilterColumn.intern(:string)
24
+ # # => #<ActiveInteraction::FilterColumn:0x007feeaa649c @type=:string>
25
+ #
26
+ # FilterColumn.intern(:boolean)
27
+ # # => #<ActiveInteraction::FilterColumn:0x007feeab8a08 @type=:boolean>
28
+ #
29
+ # @return [FilterColumn]
30
+ def intern(type)
31
+ @columns ||= {}
32
+ @columns[type] ||= new(type)
33
+ end
34
+
35
+ private :new
36
+ end
37
+
38
+ # @param type [type] The database column type.
39
+ #
40
+ # @private
41
+ def initialize(type)
42
+ @type = type
43
+ end
44
+
45
+ # Returns `true` if the column is either of type :integer or :float.
46
+ #
47
+ # @return [Boolean]
48
+ def number?
49
+ [:integer, :float].include?(type)
50
+ end
51
+
52
+ # Returns `true` if the column is of type :string.
53
+ #
54
+ # @return [Boolean]
55
+ def text?
56
+ type == :string
57
+ end
58
+ end
59
+ end
@@ -17,11 +17,17 @@ module ActiveInteraction
17
17
  value
18
18
  when String
19
19
  convert(value)
20
+ when GroupedInput
21
+ convert(stringify(value))
20
22
  else
21
23
  super
22
24
  end
23
25
  end
24
26
 
27
+ def database_column_type
28
+ self.class.slug
29
+ end
30
+
25
31
  private
26
32
 
27
33
  def convert(value)
@@ -49,5 +55,13 @@ module ActiveInteraction
49
55
  def klasses
50
56
  [klass]
51
57
  end
58
+
59
+ # @return [String]
60
+ def stringify(value)
61
+ date = %w[1 2 3].map { |key| value[key] }.join('-')
62
+ time = %w[4 5 6].map { |key| value[key] }.join(':')
63
+
64
+ "#{date} #{time}"
65
+ end
52
66
  end
53
67
  end