active_interaction 1.1.7 → 1.2.0

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