active_interaction 1.2.3 → 1.3.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +277 -72
  3. data/README.md +4 -4
  4. data/lib/active_interaction/backports.rb +6 -1
  5. data/lib/active_interaction/base.rb +15 -7
  6. data/lib/active_interaction/concerns/transactable.rb +7 -0
  7. data/lib/active_interaction/errors.rb +3 -3
  8. data/lib/active_interaction/filters/abstract_filter.rb +1 -1
  9. data/lib/active_interaction/filters/array_filter.rb +2 -0
  10. data/lib/active_interaction/filters/decimal_filter.rb +3 -1
  11. data/lib/active_interaction/filters/interface_filter.rb +44 -0
  12. data/lib/active_interaction/filters/model_filter.rb +2 -2
  13. data/lib/active_interaction/filters/time_filter.rb +8 -0
  14. data/lib/active_interaction/locale/en.yml +1 -0
  15. data/lib/active_interaction/version.rb +1 -1
  16. data/lib/active_interaction.rb +2 -2
  17. data/spec/active_interaction/base_spec.rb +91 -30
  18. data/spec/active_interaction/concerns/missable_spec.rb +2 -2
  19. data/spec/active_interaction/concerns/runnable_spec.rb +5 -2
  20. data/spec/active_interaction/concerns/transactable_spec.rb +24 -3
  21. data/spec/active_interaction/errors_spec.rb +1 -1
  22. data/spec/active_interaction/filter_column_spec.rb +5 -5
  23. data/spec/active_interaction/filters/boolean_filter_spec.rb +2 -2
  24. data/spec/active_interaction/filters/interface_filter_spec.rb +46 -0
  25. data/spec/active_interaction/filters/model_filter_spec.rb +11 -0
  26. data/spec/active_interaction/filters/time_filter_spec.rb +20 -0
  27. data/spec/active_interaction/i18n_spec.rb +6 -0
  28. data/spec/active_interaction/integration/interface_interaction_spec.rb +12 -0
  29. data/spec/active_interaction/integration/model_interaction_spec.rb +1 -1
  30. data/spec/active_interaction/modules/input_processor_spec.rb +2 -2
  31. data/spec/active_interaction/modules/validation_spec.rb +2 -2
  32. data/spec/spec_helper.rb +6 -3
  33. metadata +51 -32
@@ -0,0 +1,44 @@
1
+ # coding: utf-8
2
+
3
+ module ActiveInteraction
4
+ class Base
5
+ # @!method self.interface(*attributes, options = {})
6
+ # Creates accessors for the attributes and ensures that values passed to
7
+ # the attributes implement an interface.
8
+ #
9
+ # @!macro filter_method_params
10
+ # @option options [Array<Symbol>] :methods ([]) the methods that objects
11
+ # conforming to this interface should respond to
12
+ #
13
+ # @example
14
+ # interface :anything
15
+ # @example
16
+ # interface :serializer,
17
+ # methods: [:dump, :load]
18
+ end
19
+
20
+ # @private
21
+ class InterfaceFilter < Filter
22
+ register :interface
23
+
24
+ def cast(value)
25
+ matches?(value) ? value : super
26
+ end
27
+
28
+ private
29
+
30
+ # @param object [Object]
31
+ #
32
+ # @return [Boolean]
33
+ def matches?(object)
34
+ methods.all? { |method| object.respond_to?(method) }
35
+ rescue NoMethodError
36
+ false
37
+ end
38
+
39
+ # @return [Array<Symbol>]
40
+ def methods
41
+ options.fetch(:methods, [])
42
+ end
43
+ end
44
+ end
@@ -39,8 +39,8 @@ module ActiveInteraction
39
39
  #
40
40
  # @raise [InvalidClassError]
41
41
  def klass
42
- klass_name = options.fetch(:class, name).to_s.classify
43
- klass_name.constantize
42
+ klass_name = options.fetch(:class, name).to_s.camelize
43
+ Object.const_get(klass_name)
44
44
  rescue NameError
45
45
  raise InvalidClassError, klass_name.inspect
46
46
  end
@@ -26,6 +26,14 @@ module ActiveInteraction
26
26
  alias_method :_klass, :klass
27
27
  private :_klass
28
28
 
29
+ def initialize(name, options = {}, &block)
30
+ if options.key?(:format) && klass != Time
31
+ fail InvalidFilterError, 'format option unsupported with time zones'
32
+ end
33
+
34
+ super
35
+ end
36
+
29
37
  def cast(value)
30
38
  case value
31
39
  when Numeric
@@ -16,6 +16,7 @@ en:
16
16
  float: float
17
17
  hash: hash
18
18
  integer: integer
19
+ interface: interface
19
20
  model: model
20
21
  string: string
21
22
  symbol: symbol
@@ -5,5 +5,5 @@ module ActiveInteraction
5
5
  # The version number.
6
6
  #
7
7
  # @return [Gem::Version]
8
- VERSION = Gem::Version.new('1.2.3')
8
+ VERSION = Gem::Version.new('1.3.0')
9
9
  end
@@ -30,6 +30,7 @@ require 'active_interaction/filters/file_filter'
30
30
  require 'active_interaction/filters/float_filter'
31
31
  require 'active_interaction/filters/hash_filter'
32
32
  require 'active_interaction/filters/integer_filter'
33
+ require 'active_interaction/filters/interface_filter'
33
34
  require 'active_interaction/filters/model_filter'
34
35
  require 'active_interaction/filters/string_filter'
35
36
  require 'active_interaction/filters/symbol_filter'
@@ -41,7 +42,6 @@ require 'active_interaction/backports'
41
42
 
42
43
  I18n.load_path += Dir[File.expand_path(
43
44
  File.join(%w[active_interaction locale *.yml]), File.dirname(__FILE__))]
44
- I18n.default_locale = :en
45
45
 
46
46
  # Manage application specific business logic.
47
47
  #
@@ -50,5 +50,5 @@ I18n.default_locale = :en
50
50
  #
51
51
  # @since 1.0.0
52
52
  #
53
- # @version 1.2.3
53
+ # @version 1.3.0
54
54
  module ActiveInteraction end
@@ -162,13 +162,15 @@ describe ActiveInteraction::Base do
162
162
  expect(described_class.desc).to be_nil
163
163
  end
164
164
 
165
- it 'returns the description' do
166
- expect(described_class.desc(desc)).to eql desc
167
- end
165
+ context 'with a description' do
166
+ it 'returns the description' do
167
+ expect(described_class.desc(desc)).to eql desc
168
+ end
168
169
 
169
- it 'saves the description' do
170
- described_class.desc(desc)
171
- expect(described_class.desc).to eql desc
170
+ it 'saves the description' do
171
+ described_class.desc(desc)
172
+ expect(described_class.desc).to eql desc
173
+ end
172
174
  end
173
175
  end
174
176
 
@@ -439,12 +441,12 @@ describe ActiveInteraction::Base do
439
441
  let(:described_class) { InteractionWithFilter }
440
442
 
441
443
  it 'responds to the predicate' do
442
- expect(interaction.respond_to?(:thing?)).to be_true
444
+ expect(interaction.respond_to?(:thing?)).to be_truthy
443
445
  end
444
446
 
445
447
  context 'without a value' do
446
448
  it 'returns false' do
447
- expect(interaction.thing?).to be_false
449
+ expect(interaction.thing?).to be_falsey
448
450
  end
449
451
  end
450
452
 
@@ -456,16 +458,14 @@ describe ActiveInteraction::Base do
456
458
  end
457
459
 
458
460
  it 'returns true' do
459
- expect(interaction.thing?).to be_true
461
+ expect(interaction.thing?).to be_truthy
460
462
  end
461
463
  end
462
464
  end
463
465
 
464
466
  describe '.import_filters' do
465
- shared_context 'import_filters context' do
467
+ shared_context 'import_filters context' do |only, except|
466
468
  let(:klass) { AddInteraction }
467
- let(:only) { nil }
468
- let(:except) { nil }
469
469
 
470
470
  let(:described_class) do
471
471
  interaction = klass
@@ -477,8 +477,8 @@ describe ActiveInteraction::Base do
477
477
  end
478
478
  end
479
479
 
480
- shared_examples 'import_filters examples' do
481
- include_context 'import_filters context'
480
+ shared_examples 'import_filters examples' do |only, except|
481
+ include_context 'import_filters context', only, except
482
482
 
483
483
  it 'imports the filters' do
484
484
  expect(described_class.filters).to eql klass.filters
@@ -504,42 +504,103 @@ describe ActiveInteraction::Base do
504
504
  end
505
505
 
506
506
  context 'with neither :only nor :except' do
507
- include_examples 'import_filters examples'
507
+ include_examples 'import_filters examples', nil, nil
508
508
  end
509
509
 
510
510
  context 'with :only' do
511
511
  context 'as an Array' do
512
- include_examples 'import_filters examples'
513
-
514
- let(:only) { [:x] }
512
+ include_examples 'import_filters examples', [:x], nil
515
513
  end
516
514
 
517
515
  context 'as an Symbol' do
518
- include_examples 'import_filters examples'
519
-
520
- let(:only) { :x }
516
+ include_examples 'import_filters examples', :x, nil
521
517
  end
522
518
  end
523
519
 
524
520
  context 'with :except' do
525
521
  context 'as an Array' do
526
- include_examples 'import_filters examples'
527
-
528
- let(:except) { [:x] }
522
+ include_examples 'import_filters examples', nil, [:x]
529
523
  end
530
524
 
531
525
  context 'as an Symbol' do
532
- include_examples 'import_filters examples'
533
-
534
- let(:except) { :x }
526
+ include_examples 'import_filters examples', nil, :x
535
527
  end
536
528
  end
537
529
 
538
530
  context 'with :only & :except' do
539
- include_examples 'import_filters examples'
531
+ include_examples 'import_filters examples', [:x], [:x]
532
+ end
533
+ end
534
+
535
+ context 'callbacks' do
536
+ let(:described_class) { Class.new(TestInteraction) }
537
+
538
+ %w[type_check validate execute].each do |name|
539
+ %w[before after around].map(&:to_sym).each do |type|
540
+ it "runs the #{type} #{name} callback" do
541
+ called = false
542
+ described_class.set_callback(name, type) { called = true }
543
+ outcome
544
+ expect(called).to be_truthy
545
+ end
546
+ end
547
+ end
548
+
549
+ context 'with errors during type_check' do
550
+ before do
551
+ described_class.set_callback(:type_check, :before) do
552
+ errors.add(:base)
553
+ end
554
+ end
555
+
556
+ it 'is invalid' do
557
+ expect(outcome).to be_invalid
558
+ end
540
559
 
541
- let(:only) { [:x] }
542
- let(:except) { [:x] }
560
+ it 'does not run validate callbacks' do
561
+ called = false
562
+ described_class.set_callback(:validate, :before) { called = true }
563
+ outcome
564
+ expect(called).to be_falsey
565
+ end
566
+
567
+ it 'does not run execute callbacks' do
568
+ called = false
569
+ described_class.set_callback(:execute, :before) { called = true }
570
+ outcome
571
+ expect(called).to be_falsey
572
+ end
573
+ end
574
+
575
+ context 'with errors during validate' do
576
+ before do
577
+ described_class.set_callback(:validate, :before) do
578
+ errors.add(:base)
579
+ end
580
+ end
581
+
582
+ it 'is invalid' do
583
+ expect(outcome).to be_invalid
584
+ end
585
+
586
+ it 'does not run execute callbacks' do
587
+ called = false
588
+ described_class.set_callback(:execute, :before) { called = true }
589
+ outcome
590
+ expect(called).to be_falsey
591
+ end
592
+ end
593
+
594
+ context 'with errors during execute' do
595
+ before do
596
+ described_class.set_callback(:execute, :before) do
597
+ errors.add(:base)
598
+ end
599
+ end
600
+
601
+ it 'is invalid' do
602
+ expect(outcome).to be_invalid
603
+ end
543
604
  end
544
605
  end
545
606
  end
@@ -10,7 +10,7 @@ describe ActiveInteraction::Missable do
10
10
  let(:slug) { :slug }
11
11
 
12
12
  it 'returns false' do
13
- expect(instance.respond_to?(slug)).to be_false
13
+ expect(instance.respond_to?(slug)).to be_falsey
14
14
  end
15
15
  end
16
16
 
@@ -18,7 +18,7 @@ describe ActiveInteraction::Missable do
18
18
  let(:slug) { :boolean }
19
19
 
20
20
  it 'returns true' do
21
- expect(instance.respond_to?(slug)).to be_true
21
+ expect(instance.respond_to?(slug)).to be_truthy
22
22
  end
23
23
  end
24
24
  end
@@ -57,7 +57,7 @@ describe ActiveInteraction::Runnable do
57
57
  }
58
58
 
59
59
  klass.run
60
- expect(has_run).to be_true
60
+ expect(has_run).to be_truthy
61
61
  end
62
62
  end
63
63
  end
@@ -141,7 +141,10 @@ describe ActiveInteraction::Runnable do
141
141
 
142
142
  it 'does not duplicate errors on subsequent calls' do
143
143
  instance.valid?
144
- expect { instance.valid? }.to_not change { instance.errors.count }.by 1
144
+ count = instance.errors.count
145
+ instance.valid?
146
+
147
+ expect(instance.errors.count).to eql count
145
148
  end
146
149
  end
147
150
  end
@@ -47,25 +47,46 @@ describe ActiveInteraction::Transactable do
47
47
 
48
48
  describe '.transaction?' do
49
49
  it 'defaults to true' do
50
- expect(klass.transaction?).to be_true
50
+ expect(klass.transaction?).to be_truthy
51
51
  end
52
52
 
53
53
  it 'returns the stored value' do
54
54
  klass.transaction(false)
55
- expect(klass.transaction?).to be_false
55
+ expect(klass.transaction?).to be_falsey
56
+ end
57
+
58
+ context 'with a subclass' do
59
+ before { klass.transaction(false) }
60
+
61
+ let(:subclass) { Class.new(klass) }
62
+
63
+ it 'inherits from the superclass' do
64
+ expect(subclass.transaction?).to be_falsey
65
+ end
56
66
  end
57
67
  end
58
68
 
59
69
  describe '.transaction_options' do
70
+ let(:h) { { rand => rand } }
71
+
60
72
  it 'defaults to an empty hash' do
61
73
  expect(klass.transaction_options).to eql({})
62
74
  end
63
75
 
64
76
  it 'returns the stored value' do
65
- h = { rand => rand }
66
77
  klass.transaction(klass.transaction?, h)
67
78
  expect(klass.transaction_options).to eql h
68
79
  end
80
+
81
+ context 'with a subclass' do
82
+ before { klass.transaction(klass.transaction?, h) }
83
+
84
+ let(:subclass) { Class.new(klass) }
85
+
86
+ it 'inherits from the superclass' do
87
+ expect(subclass.transaction_options).to eql h
88
+ end
89
+ end
69
90
  end
70
91
 
71
92
  describe '#transaction' do
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  describe ActiveInteraction::Errors do
6
6
  let(:klass) do
7
7
  Class.new do
8
- include ActiveModel::Model
8
+ include ActiveInteraction::ActiveModelable
9
9
 
10
10
  attr_reader :attribute
11
11
 
@@ -42,7 +42,7 @@ describe ActiveInteraction::FilterColumn do
42
42
  let(:type) { :integer }
43
43
 
44
44
  it 'returns true' do
45
- expect(number?).to be_true
45
+ expect(number?).to be_truthy
46
46
  end
47
47
  end
48
48
 
@@ -50,7 +50,7 @@ describe ActiveInteraction::FilterColumn do
50
50
  let(:type) { :float }
51
51
 
52
52
  it 'returns true' do
53
- expect(number?).to be_true
53
+ expect(number?).to be_truthy
54
54
  end
55
55
  end
56
56
 
@@ -58,7 +58,7 @@ describe ActiveInteraction::FilterColumn do
58
58
  let(:type) { :string }
59
59
 
60
60
  it 'returns false' do
61
- expect(number?).to be_false
61
+ expect(number?).to be_falsey
62
62
  end
63
63
  end
64
64
  end
@@ -72,7 +72,7 @@ describe ActiveInteraction::FilterColumn do
72
72
  let(:type) { :string }
73
73
 
74
74
  it 'returns true' do
75
- expect(text?).to be_true
75
+ expect(text?).to be_truthy
76
76
  end
77
77
  end
78
78
 
@@ -80,7 +80,7 @@ describe ActiveInteraction::FilterColumn do
80
80
  let(:type) { :float }
81
81
 
82
82
  it 'returns false' do
83
- expect(text?).to be_false
83
+ expect(text?).to be_falsey
84
84
  end
85
85
  end
86
86
  end
@@ -10,7 +10,7 @@ describe ActiveInteraction::BooleanFilter, :filter do
10
10
  context 'falsey' do
11
11
  [false, '0', 'false', 'FALSE'].each do |value|
12
12
  it "returns false for #{value.inspect}" do
13
- expect(filter.cast(value)).to be_false
13
+ expect(filter.cast(value)).to be_falsey
14
14
  end
15
15
  end
16
16
  end
@@ -18,7 +18,7 @@ describe ActiveInteraction::BooleanFilter, :filter do
18
18
  context 'truthy' do
19
19
  [true, '1', 'true', 'TRUE'].each do |value|
20
20
  it "returns true for #{value.inspect}" do
21
- expect(filter.cast(value)).to be_true
21
+ expect(filter.cast(value)).to be_truthy
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,46 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+ require 'yaml'
6
+
7
+ describe ActiveInteraction::InterfaceFilter, :filter do
8
+ include_context 'filters'
9
+ it_behaves_like 'a filter'
10
+
11
+ before { options[:methods] = [:dump, :load] }
12
+
13
+ describe '#cast' do
14
+ let(:result) { filter.cast(value) }
15
+
16
+ context 'with an Object' do
17
+ let(:value) { Object.new }
18
+
19
+ it 'raises an error' do
20
+ expect { result }.to raise_error ActiveInteraction::InvalidValueError
21
+ end
22
+ end
23
+
24
+ context 'with JSON' do
25
+ let(:value) { JSON }
26
+
27
+ it 'returns an Array' do
28
+ expect(result).to eql value
29
+ end
30
+ end
31
+
32
+ context 'with YAML' do
33
+ let(:value) { YAML }
34
+
35
+ it 'returns an Hash' do
36
+ expect(result).to eql value
37
+ end
38
+ end
39
+ end
40
+
41
+ describe '#database_column_type' do
42
+ it 'returns :string' do
43
+ expect(filter.database_column_type).to eql :string
44
+ end
45
+ end
46
+ end
@@ -3,6 +3,7 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  class Model; end
6
+ class Things; end
6
7
 
7
8
  describe ActiveInteraction::ModelFilter, :filter do
8
9
  include_context 'filters'
@@ -118,6 +119,16 @@ describe ActiveInteraction::ModelFilter, :filter do
118
119
  end
119
120
  end
120
121
 
122
+ context 'with a plural class' do
123
+ let(:value) { Things.new }
124
+
125
+ before { options[:class] = Things }
126
+
127
+ it 'returns the instance' do
128
+ expect(result).to eql value
129
+ end
130
+ end
131
+
121
132
  context 'with class as an invalid String' do
122
133
  before do
123
134
  options.merge!(class: 'invalid')
@@ -14,6 +14,26 @@ describe ActiveInteraction::TimeFilter, :filter do
14
14
  end
15
15
  end
16
16
 
17
+ describe '#initialize' do
18
+ context 'with a format' do
19
+ before { options[:format] = '%T' }
20
+
21
+ context 'with a time zone' do
22
+ before do
23
+ time_zone = double
24
+ allow(Time).to receive(:zone).and_return(time_zone)
25
+
26
+ time_with_zone = double
27
+ allow(time_zone).to receive(:at).and_return(time_with_zone)
28
+ end
29
+
30
+ it 'raises an error' do
31
+ expect { filter }.to raise_error(ActiveInteraction::InvalidFilterError)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
17
37
  describe '#cast' do
18
38
  let(:result) { filter.cast(value) }
19
39
 
@@ -54,8 +54,11 @@ describe I18nInteraction do
54
54
  include_examples 'translation'
55
55
 
56
56
  before do
57
+ @locale = I18n.locale
57
58
  I18n.locale = :en
58
59
  end
60
+
61
+ after { I18n.locale = @locale }
59
62
  end
60
63
 
61
64
  context 'hsilgne' do
@@ -75,7 +78,10 @@ describe I18nInteraction do
75
78
  }
76
79
  )
77
80
 
81
+ @locale = I18n.locale
78
82
  I18n.locale = 'hsilgne'
79
83
  end
84
+
85
+ after { I18n.locale = @locale }
80
86
  end
81
87
  end
@@ -0,0 +1,12 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+ require 'yaml'
6
+
7
+ describe 'InterfaceInteraction' do
8
+ it_behaves_like 'an interaction',
9
+ :interface,
10
+ -> { [JSON, YAML].sample },
11
+ methods: [:dump, :load]
12
+ end
@@ -3,5 +3,5 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe 'ModelInteraction' do
6
- it_behaves_like 'an interaction', :model, -> { Object }, class: Class
6
+ it_behaves_like 'an interaction', :model, -> { // }, class: Regexp
7
7
  end
@@ -5,11 +5,11 @@ require 'spec_helper'
5
5
  describe ActiveInteraction::InputProcessor do
6
6
  describe '.reserved?(name)' do
7
7
  it 'returns true for anything starting with "_interaction_"' do
8
- expect(described_class.reserved?('_interaction_')).to be_true
8
+ expect(described_class.reserved?('_interaction_')).to be_truthy
9
9
  end
10
10
 
11
11
  it 'returns false for anything else' do
12
- expect(described_class.reserved?(SecureRandom.hex)).to be_false
12
+ expect(described_class.reserved?(SecureRandom.hex)).to be_falsey
13
13
  end
14
14
  end
15
15
 
@@ -21,7 +21,7 @@ describe ActiveInteraction::Validation do
21
21
  let(:inputs) { { name: 1 } }
22
22
 
23
23
  before do
24
- filter.stub(:cast).and_return(1)
24
+ allow(filter).to receive(:cast).and_return(1)
25
25
  end
26
26
 
27
27
  it 'returns no errors' do
@@ -31,7 +31,7 @@ describe ActiveInteraction::Validation do
31
31
 
32
32
  context 'filter throws' do
33
33
  before do
34
- filter.stub(:cast).and_raise(exception)
34
+ allow(filter).to receive(:cast).and_raise(exception)
35
35
  end
36
36
 
37
37
  context 'InvalidValueError' do
data/spec/spec_helper.rb CHANGED
@@ -3,14 +3,17 @@
3
3
  require 'coveralls'
4
4
  Coveralls.wear!
5
5
 
6
+ require 'i18n'
7
+ if I18n.config.respond_to?(:enforce_available_locales)
8
+ I18n.config.enforce_available_locales = true
9
+ I18n.config.available_locales = %w[en hsilgne]
10
+ end
11
+
6
12
  require 'active_interaction'
7
13
 
8
14
  Dir['./spec/support/**/*.rb'].each { |f| require f }
9
15
 
10
16
  RSpec.configure do |config|
11
- config.treat_symbols_as_metadata_keys_with_true_values = true
12
17
  config.run_all_when_everything_filtered = true
13
18
  config.filter_run_including :focus
14
19
  end
15
-
16
- I18n.config.enforce_available_locales = true