active_interaction 0.10.2 → 1.0.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/README.md +32 -35
  4. data/lib/active_interaction.rb +14 -6
  5. data/lib/active_interaction/base.rb +155 -135
  6. data/lib/active_interaction/concerns/active_modelable.rb +46 -0
  7. data/lib/active_interaction/{modules/overload_hash.rb → concerns/hashable.rb} +5 -1
  8. data/lib/active_interaction/concerns/missable.rb +47 -0
  9. data/lib/active_interaction/concerns/runnable.rb +156 -0
  10. data/lib/active_interaction/errors.rb +50 -14
  11. data/lib/active_interaction/filter.rb +33 -45
  12. data/lib/active_interaction/filters/abstract_date_time_filter.rb +24 -15
  13. data/lib/active_interaction/filters/abstract_filter.rb +18 -0
  14. data/lib/active_interaction/filters/abstract_numeric_filter.rb +13 -8
  15. data/lib/active_interaction/filters/array_filter.rb +42 -35
  16. data/lib/active_interaction/filters/boolean_filter.rb +8 -11
  17. data/lib/active_interaction/filters/date_filter.rb +11 -15
  18. data/lib/active_interaction/filters/date_time_filter.rb +11 -15
  19. data/lib/active_interaction/filters/file_filter.rb +11 -11
  20. data/lib/active_interaction/filters/float_filter.rb +7 -15
  21. data/lib/active_interaction/filters/hash_filter.rb +18 -24
  22. data/lib/active_interaction/filters/integer_filter.rb +7 -14
  23. data/lib/active_interaction/filters/model_filter.rb +13 -14
  24. data/lib/active_interaction/filters/string_filter.rb +11 -14
  25. data/lib/active_interaction/filters/symbol_filter.rb +6 -9
  26. data/lib/active_interaction/filters/time_filter.rb +13 -16
  27. data/lib/active_interaction/modules/validation.rb +9 -4
  28. data/lib/active_interaction/version.rb +5 -2
  29. data/spec/active_interaction/base_spec.rb +109 -4
  30. data/spec/active_interaction/{modules/active_model_spec.rb → concerns/active_modelable_spec.rb} +15 -3
  31. data/spec/active_interaction/{modules/overload_hash_spec.rb → concerns/hashable_spec.rb} +2 -3
  32. data/spec/active_interaction/{modules/method_missing_spec.rb → concerns/missable_spec.rb} +38 -3
  33. data/spec/active_interaction/concerns/runnable_spec.rb +192 -0
  34. data/spec/active_interaction/filters/abstract_filter_spec.rb +8 -0
  35. data/spec/active_interaction/filters/abstract_numeric_filter_spec.rb +1 -1
  36. data/spec/active_interaction/modules/validation_spec.rb +2 -2
  37. data/spec/support/concerns.rb +15 -0
  38. data/spec/support/filters.rb +5 -5
  39. data/spec/support/interactions.rb +6 -5
  40. metadata +47 -73
  41. data/lib/active_interaction/filters.rb +0 -28
  42. data/lib/active_interaction/modules/active_model.rb +0 -32
  43. data/lib/active_interaction/modules/core.rb +0 -70
  44. data/lib/active_interaction/modules/method_missing.rb +0 -20
  45. data/spec/active_interaction/filters_spec.rb +0 -23
  46. data/spec/active_interaction/modules/core_spec.rb +0 -114
@@ -2,25 +2,18 @@
2
2
 
3
3
  module ActiveInteraction
4
4
  class Base
5
- # Creates accessors for the attributes and ensures that values passed to
6
- # the attributes are Integers. String values are converted into Integers.
5
+ # @!method self.integer(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes are Integers. String values are converted into
8
+ # Integers.
7
9
  #
8
- # @macro filter_method_params
10
+ # @!macro filter_method_params
9
11
  #
10
- # @example
11
- # integer :quantity
12
- #
13
- # @since 0.1.0
14
- #
15
- # @method self.integer(*attributes, options = {})
12
+ # @example
13
+ # integer :quantity
16
14
  end
17
15
 
18
16
  # @private
19
17
  class IntegerFilter < AbstractNumericFilter
20
- private
21
-
22
- def klass
23
- Integer
24
- end
25
18
  end
26
19
  end
@@ -2,22 +2,18 @@
2
2
 
3
3
  module ActiveInteraction
4
4
  class Base
5
- # Creates accessors for the attributes and ensures that values passed to
6
- # the attributes are the correct class.
5
+ # @!method self.model(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes are the correct class.
7
8
  #
8
- # @macro filter_method_params
9
- # @option options [Class, String, Symbol] :class (use the attribute name)
10
- # Class name used to ensure the value.
9
+ # @!macro filter_method_params
10
+ # @option options [Class, String, Symbol] :class (use the attribute name)
11
+ # Class name used to ensure the value.
11
12
  #
12
- # @example Ensures that the class is `Account`
13
- # model :account
14
- #
15
- # @example Ensures that the class is `User`
16
- # model :account, class: User
17
- #
18
- # @since 0.1.0
19
- #
20
- # @method self.model(*attributes, options = {})
13
+ # @example
14
+ # model :account
15
+ # @example
16
+ # model :account, class: User
21
17
  end
22
18
 
23
19
  # @private
@@ -33,6 +29,9 @@ module ActiveInteraction
33
29
 
34
30
  private
35
31
 
32
+ # @return [Class]
33
+ #
34
+ # @raise [InvalidClassError]
36
35
  def klass
37
36
  klass_name = options.fetch(:class, name).to_s.classify
38
37
  klass_name.constantize
@@ -2,22 +2,18 @@
2
2
 
3
3
  module ActiveInteraction
4
4
  class Base
5
- # Creates accessors for the attributes and ensures that values passed to
6
- # the attributes are Strings.
5
+ # @!method self.string(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes are Strings.
7
8
  #
8
- # @macro filter_method_params
9
- # @option options [Boolean] :strip (true) strip leading and trailing
10
- # whitespace
9
+ # @!macro filter_method_params
10
+ # @option options [Boolean] :strip (true) strip leading and trailing
11
+ # whitespace
11
12
  #
12
- # @example
13
- # string :first_name
14
- #
15
- # @example
16
- # string :first_name, strip: false
17
- #
18
- # @since 0.1.0
19
- #
20
- # @method self.string(*attributes, options = {})
13
+ # @example
14
+ # string :first_name
15
+ # @example
16
+ # string :first_name, strip: false
21
17
  end
22
18
 
23
19
  # @private
@@ -33,6 +29,7 @@ module ActiveInteraction
33
29
 
34
30
  private
35
31
 
32
+ # @return [Boolean]
36
33
  def strip?
37
34
  options.fetch(:strip, true)
38
35
  end
@@ -2,17 +2,14 @@
2
2
 
3
3
  module ActiveInteraction
4
4
  class Base
5
- # Creates accessors for the attributes and ensures that values passed to
6
- # the attributes are Symbols. Strings will be converted to Symbols.
5
+ # @!method self.symbol(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes are Symbols. Strings will be converted to Symbols.
7
8
  #
8
- # @macro filter_method_params
9
+ # @!macro filter_method_params
9
10
  #
10
- # @example
11
- # symbol :condiment
12
- #
13
- # @since 0.6.0
14
- #
15
- # @method self.symbol(*attributes, options = {})
11
+ # @example
12
+ # symbol :condiment
16
13
  end
17
14
 
18
15
  # @private
@@ -2,24 +2,21 @@
2
2
 
3
3
  module ActiveInteraction
4
4
  class Base
5
- # Creates accessors for the attributes and ensures that values passed to
6
- # the attributes are Times. Numeric values are processed using `at`.
7
- # Strings are processed using `parse` unless the format option is given,
8
- # in which case they will be processed with `strptime`. If `Time.zone` is
9
- # available it will be used so that the values are time zone aware.
5
+ # @!method self.time(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes are Times. Numeric values are processed using `at`.
8
+ # Strings are processed using `parse` unless the format option is
9
+ # given, in which case they will be processed with `strptime`. If
10
+ # `Time.zone` is available it will be used so that the values are time
11
+ # zone aware.
10
12
  #
11
- # @macro filter_method_params
12
- # @option options [String] :format parse strings using this format string
13
+ # @!macro filter_method_params
14
+ # @option options [String] :format parse strings using this format string
13
15
  #
14
- # @example
15
- # time :start_date
16
- #
17
- # @example
18
- # time :start_date, format: '%Y-%m-%dT%H:%M:%S%z'
19
- #
20
- # @since 0.1.0
21
- #
22
- # @method self.time(*attributes, options = {})
16
+ # @example
17
+ # time :start_date
18
+ # @example
19
+ # time :start_date, format: '%Y-%m-%dT%H:%M:%S%Z'
23
20
  end
24
21
 
25
22
  # @private
@@ -1,22 +1,27 @@
1
1
  # coding: utf-8
2
2
 
3
3
  module ActiveInteraction
4
+ # Validates inputs using filters.
5
+ #
4
6
  # @private
5
7
  module Validation
8
+ # @param filters [Hash{Symbol => Filter}]
9
+ # @param inputs [Hash{Symbol => Object}]
6
10
  def self.validate(filters, inputs)
7
- filters.each_with_object([]) do |filter, errors|
11
+ filters.each_with_object([]) do |(name, filter), errors|
8
12
  begin
9
- filter.cast(inputs[filter.name])
13
+ filter.cast(inputs[name])
10
14
  rescue InvalidValueError
11
- errors << [filter.name, :invalid, nil, type: type(filter)]
15
+ errors << [name, :invalid, nil, type: type(filter)]
12
16
  rescue MissingValueError
13
- errors << [filter.name, :missing]
17
+ errors << [name, :missing]
14
18
  end
15
19
  end
16
20
  end
17
21
 
18
22
  private
19
23
 
24
+ # @param filter [Filter]
20
25
  def self.type(filter)
21
26
  I18n.translate("#{Base.i18n_scope}.types.#{filter.class.slug}")
22
27
  end
@@ -1,6 +1,9 @@
1
1
  # coding: utf-8
2
2
 
3
- # rubocop:disable Documentation
3
+ #
4
4
  module ActiveInteraction
5
- VERSION = Gem::Version.new('0.10.2')
5
+ # The version number.
6
+ #
7
+ # @return [Gem::Version]
8
+ ActiveInteraction::VERSION = Gem::Version.new('1.0.0')
6
9
  end
@@ -118,6 +118,23 @@ describe ActiveInteraction::Base do
118
118
  end
119
119
  end
120
120
 
121
+ describe '.desc' do
122
+ let(:desc) { SecureRandom.hex }
123
+
124
+ it 'returns nil' do
125
+ expect(described_class.desc).to be_nil
126
+ end
127
+
128
+ it 'returns the description' do
129
+ expect(described_class.desc(desc)).to eq desc
130
+ end
131
+
132
+ it 'saves the description' do
133
+ described_class.desc(desc)
134
+ expect(described_class.desc).to eq desc
135
+ end
136
+ end
137
+
121
138
  describe '.method_missing(filter_type, *args, &block)' do
122
139
  it 'raises an error for an invalid filter type' do
123
140
  expect do
@@ -246,10 +263,10 @@ describe ActiveInteraction::Base do
246
263
  expect(result[:thing]).to eq thing
247
264
  end
248
265
 
249
- it 'calls transaction' do
250
- allow(described_class).to receive(:transaction)
266
+ it 'calls ActiveRecord::Base.transaction' do
267
+ allow(ActiveRecord::Base).to receive(:transaction)
251
268
  outcome
252
- expect(described_class).to have_received(:transaction).once
269
+ expect(ActiveRecord::Base).to have_received(:transaction)
253
270
  .with(no_args)
254
271
  end
255
272
  end
@@ -331,7 +348,7 @@ describe ActiveInteraction::Base do
331
348
  let(:described_class) { InteractionWithFilter }
332
349
 
333
350
  def filters(klass)
334
- klass.filters.map(&:name)
351
+ klass.filters.keys
335
352
  end
336
353
 
337
354
  it 'includes the filters from the superclass' do
@@ -344,4 +361,92 @@ describe ActiveInteraction::Base do
344
361
  expect(filters(described_class)).to_not include :other_thing
345
362
  end
346
363
  end
364
+
365
+ context 'predicates' do
366
+ let(:described_class) { InteractionWithFilter }
367
+
368
+ it 'responds to the predicate' do
369
+ expect(interaction.respond_to?(:thing?)).to be_true
370
+ end
371
+
372
+ context 'without a value' do
373
+ it 'returns false' do
374
+ expect(interaction.thing?).to be_false
375
+ end
376
+ end
377
+
378
+ context 'with a value' do
379
+ let(:thing) { rand }
380
+
381
+ before do
382
+ inputs.merge!(thing: thing)
383
+ end
384
+
385
+ it 'returns true' do
386
+ expect(interaction.thing?).to be_true
387
+ end
388
+ end
389
+ end
390
+
391
+ describe '.import_filters' do
392
+ let(:described_class) do
393
+ Class.new(TestInteraction) do
394
+ import_filters AddInteraction
395
+ end
396
+ end
397
+
398
+ it 'imports the filters' do
399
+ expect(described_class.filters).to eq AddInteraction.filters
400
+ end
401
+
402
+ context 'with :only' do
403
+ let(:described_class) do
404
+ Class.new(TestInteraction) do
405
+ import_filters AddInteraction, only: [:x]
406
+ end
407
+ end
408
+
409
+ it 'does not modify the source' do
410
+ filters = AddInteraction.filters.dup
411
+ described_class
412
+ expect(AddInteraction.filters).to eq filters
413
+ end
414
+
415
+ it 'imports the filters' do
416
+ expect(described_class.filters).to eq AddInteraction.filters
417
+ .select { |k, _| k == :x }
418
+ end
419
+ end
420
+
421
+ context 'with :except' do
422
+ let(:described_class) do
423
+ Class.new(TestInteraction) do
424
+ import_filters AddInteraction, except: [:x]
425
+ end
426
+ end
427
+
428
+ it 'does not modify the source' do
429
+ filters = AddInteraction.filters.dup
430
+ described_class
431
+ expect(AddInteraction.filters).to eq filters
432
+ end
433
+
434
+ it 'imports the filters' do
435
+ expect(described_class.filters).to eq AddInteraction.filters
436
+ .reject { |k, _| k == :x }
437
+ end
438
+ end
439
+
440
+ context 'with :only & :except' do
441
+ let(:described_class) do
442
+ Class.new(TestInteraction) do
443
+ import_filters AddInteraction, only: nil, except: nil
444
+ end
445
+ end
446
+
447
+ it 'raises an error' do
448
+ expect { described_class }.to raise_error ArgumentError
449
+ end
450
+ end
451
+ end
347
452
  end
@@ -1,10 +1,22 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require 'spec_helper'
4
+ require 'test/unit/assertions'
4
5
 
5
- describe ActiveInteraction::ActiveModel do
6
- let(:klass) { Class.new { include ActiveInteraction::ActiveModel } }
7
- subject(:instance) { klass.new }
6
+ shared_examples_for 'ActiveModel' do
7
+ include ActiveModel::Lint::Tests
8
+ include Test::Unit::Assertions
9
+
10
+ let(:model) { subject }
11
+
12
+ ActiveModel::Lint::Tests.public_instance_methods
13
+ .grep(/\Atest/) { |m| example(m) { public_send(m) } }
14
+ end
15
+
16
+ describe ActiveInteraction::ActiveModelable do
17
+ include_context 'concerns', ActiveInteraction::ActiveModelable
18
+
19
+ it_behaves_like 'ActiveModel'
8
20
 
9
21
  describe '.i18n_scope' do
10
22
  it 'returns the scope' do
@@ -2,9 +2,8 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe ActiveInteraction::OverloadHash do
6
- let(:klass) { Class.new { include ActiveInteraction::OverloadHash } }
7
- subject(:instance) { klass.new }
5
+ describe ActiveInteraction::Hashable do
6
+ include_context 'concerns', ActiveInteraction::Hashable
8
7
 
9
8
  describe '#hash(*args, &block)' do
10
9
  context 'with no arguments' do
@@ -2,9 +2,44 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe ActiveInteraction::MethodMissing do
6
- let(:klass) { Class.new { include ActiveInteraction::MethodMissing } }
7
- subject(:instance) { klass.new }
5
+ describe ActiveInteraction::Missable do
6
+ include_context 'concerns', ActiveInteraction::Missable
7
+
8
+ describe '#respond_to?(slug, include_all = false)' do
9
+ context 'with invalid slug' do
10
+ let(:slug) { :slug }
11
+
12
+ it 'returns false' do
13
+ expect(instance.respond_to?(slug)).to be_false
14
+ end
15
+ end
16
+
17
+ context 'with valid slug' do
18
+ let(:slug) { :boolean }
19
+
20
+ it 'returns true' do
21
+ expect(instance.respond_to?(slug)).to be_true
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#method(sym)' do
27
+ context 'with invalid slug' do
28
+ let(:slug) { :slug }
29
+
30
+ it 'returns false' do
31
+ expect { instance.method(slug) }.to raise_error NameError
32
+ end
33
+ end
34
+
35
+ context 'with valid slug' do
36
+ let(:slug) { :boolean }
37
+
38
+ it 'returns true' do
39
+ expect(instance.method(slug)).to be_a Method
40
+ end
41
+ end
42
+ end
8
43
 
9
44
  describe '#method_missing' do
10
45
  context 'with invalid slug' do