functional-light-service 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +72 -0
  5. data/.travis.yml +22 -0
  6. data/Appraisals +3 -0
  7. data/CHANGELOG.md +37 -0
  8. data/CODE_OF_CONDUCT.md +22 -0
  9. data/Gemfile +6 -0
  10. data/LICENSE +22 -0
  11. data/README.md +1424 -0
  12. data/Rakefile +12 -0
  13. data/VERSION +1 -0
  14. data/functional-light-service.gemspec +26 -0
  15. data/gemfiles/activesupport_5.gemfile +8 -0
  16. data/gemfiles/activesupport_5.gemfile.lock +82 -0
  17. data/lib/functional-light-service/action.rb +102 -0
  18. data/lib/functional-light-service/configuration.rb +24 -0
  19. data/lib/functional-light-service/context/key_verifier.rb +118 -0
  20. data/lib/functional-light-service/context.rb +165 -0
  21. data/lib/functional-light-service/errors.rb +6 -0
  22. data/lib/functional-light-service/functional/enum.rb +254 -0
  23. data/lib/functional-light-service/functional/maybe.rb +14 -0
  24. data/lib/functional-light-service/functional/monad.rb +66 -0
  25. data/lib/functional-light-service/functional/null.rb +74 -0
  26. data/lib/functional-light-service/functional/option.rb +99 -0
  27. data/lib/functional-light-service/functional/result.rb +122 -0
  28. data/lib/functional-light-service/localization_adapter.rb +44 -0
  29. data/lib/functional-light-service/organizer/execute.rb +14 -0
  30. data/lib/functional-light-service/organizer/iterate.rb +22 -0
  31. data/lib/functional-light-service/organizer/reduce_if.rb +17 -0
  32. data/lib/functional-light-service/organizer/reduce_until.rb +20 -0
  33. data/lib/functional-light-service/organizer/scoped_reducable.rb +13 -0
  34. data/lib/functional-light-service/organizer/verify_call_method_exists.rb +28 -0
  35. data/lib/functional-light-service/organizer/with_callback.rb +26 -0
  36. data/lib/functional-light-service/organizer/with_reducer.rb +71 -0
  37. data/lib/functional-light-service/organizer/with_reducer_factory.rb +18 -0
  38. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +105 -0
  39. data/lib/functional-light-service/organizer.rb +105 -0
  40. data/lib/functional-light-service/testing/context_factory.rb +40 -0
  41. data/lib/functional-light-service/testing.rb +1 -0
  42. data/lib/functional-light-service/version.rb +3 -0
  43. data/lib/functional-light-service.rb +29 -0
  44. data/resources/fail_actions.png +0 -0
  45. data/resources/light-service.png +0 -0
  46. data/resources/organizer_and_actions.png +0 -0
  47. data/resources/skip_actions.png +0 -0
  48. data/spec/acceptance/add_numbers_spec.rb +11 -0
  49. data/spec/acceptance/after_actions_spec.rb +71 -0
  50. data/spec/acceptance/around_each_spec.rb +19 -0
  51. data/spec/acceptance/before_actions_spec.rb +98 -0
  52. data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
  53. data/spec/acceptance/fail_spec.rb +24 -0
  54. data/spec/acceptance/include_warning_spec.rb +29 -0
  55. data/spec/acceptance/log_from_organizer_spec.rb +154 -0
  56. data/spec/acceptance/message_localization_spec.rb +118 -0
  57. data/spec/acceptance/not_having_call_method_warning_spec.rb +39 -0
  58. data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
  59. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +65 -0
  60. data/spec/acceptance/organizer/execute_spec.rb +46 -0
  61. data/spec/acceptance/organizer/iterate_spec.rb +37 -0
  62. data/spec/acceptance/organizer/reduce_if_spec.rb +51 -0
  63. data/spec/acceptance/organizer/reduce_until_spec.rb +43 -0
  64. data/spec/acceptance/organizer/with_callback_spec.rb +110 -0
  65. data/spec/acceptance/rollback_spec.rb +132 -0
  66. data/spec/acceptance/skip_all_warning_spec.rb +20 -0
  67. data/spec/acceptance/testing/context_factory_spec.rb +54 -0
  68. data/spec/action_expected_keys_spec.rb +63 -0
  69. data/spec/action_expects_and_promises_spec.rb +93 -0
  70. data/spec/action_promised_keys_spec.rb +122 -0
  71. data/spec/action_spec.rb +89 -0
  72. data/spec/context/inspect_spec.rb +57 -0
  73. data/spec/context_spec.rb +197 -0
  74. data/spec/examples/amount_spec.rb +77 -0
  75. data/spec/examples/controller_spec.rb +63 -0
  76. data/spec/examples/validate_address_spec.rb +37 -0
  77. data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
  78. data/spec/lib/deterministic/currify_spec.rb +88 -0
  79. data/spec/lib/deterministic/monad_axioms.rb +44 -0
  80. data/spec/lib/deterministic/monad_spec.rb +45 -0
  81. data/spec/lib/deterministic/null_spec.rb +58 -0
  82. data/spec/lib/deterministic/option_spec.rb +133 -0
  83. data/spec/lib/deterministic/result/failure_spec.rb +65 -0
  84. data/spec/lib/deterministic/result/result_map_spec.rb +154 -0
  85. data/spec/lib/deterministic/result/result_shared.rb +24 -0
  86. data/spec/lib/deterministic/result/success_spec.rb +41 -0
  87. data/spec/lib/deterministic/result_spec.rb +63 -0
  88. data/spec/lib/enum_spec.rb +112 -0
  89. data/spec/localization_adapter_spec.rb +83 -0
  90. data/spec/organizer/with_reducer_spec.rb +56 -0
  91. data/spec/organizer_key_aliases_spec.rb +29 -0
  92. data/spec/organizer_spec.rb +93 -0
  93. data/spec/readme_spec.rb +47 -0
  94. data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
  95. data/spec/sample/calculates_tax_spec.rb +30 -0
  96. data/spec/sample/looks_up_tax_percentage_action_spec.rb +53 -0
  97. data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
  98. data/spec/sample/tax/calculates_order_tax_action.rb +9 -0
  99. data/spec/sample/tax/calculates_tax.rb +11 -0
  100. data/spec/sample/tax/looks_up_tax_percentage_action.rb +27 -0
  101. data/spec/sample/tax/provides_free_shipping_action.rb +10 -0
  102. data/spec/spec_helper.rb +24 -0
  103. data/spec/support.rb +1 -0
  104. data/spec/test_doubles.rb +552 -0
  105. data/spec/testing/context_factory/iterate_spec.rb +39 -0
  106. data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
  107. data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
  108. data/spec/testing/context_factory/with_callback_spec.rb +38 -0
  109. data/spec/testing/context_factory_spec.rb +55 -0
  110. metadata +285 -0
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunctionalLightService::Result do
4
+ include FunctionalLightService::Prelude::Result
5
+
6
+ it "can't call Result#new directly" do
7
+ msg = "private method `new' called for FunctionalLightService::Result:Class"
8
+ expect { described_class.new(1) }.to raise_error(NoMethodError, msg)
9
+ end
10
+
11
+ it "fmap" do
12
+ expect(Success(1).fmap { |n| n + 1 }).to eq Success(2)
13
+ expect(Failure(0).fmap { |n| n + 1 }).to eq Failure(1)
14
+ end
15
+
16
+ it "map" do
17
+ expect(Success(1).map { |n| Success(n + 1) }).to eq Success(2)
18
+ expect(Failure(0).map { |n| Success(n + 1) }).to eq Failure(0)
19
+ end
20
+
21
+ it "+" do
22
+ expect(Success([1]) + Failure([2])).to eq Failure([2])
23
+ expect(Success(1) + Success(1)).to eq Success(2)
24
+ expect(Failure(2) + Success(1)).to eq Failure(2)
25
+ expect(Failure([2]) + Failure([3]) + Success(1)).to eq Failure([2, 3])
26
+ expect(Success([1]) + Success([1])).to eq Success([1, 1])
27
+ expect { Success([1]) + Success(1) }.to raise_error TypeError
28
+ end
29
+
30
+ subject { Success(1) }
31
+ # specify { expect(subject).to be_an_instance_of described_class }
32
+ specify { expect(subject).to be_success }
33
+ specify { expect(subject).not_to be_failure }
34
+ specify { expect(subject.success?).to be_truthy }
35
+ specify { expect(subject.failure?).to be_falsey }
36
+
37
+ specify { expect(subject).to be_a described_class }
38
+ # specify { expect(subject).to eq(described_class.new(1)) }
39
+ specify { expect(subject.fmap { |v| v + 1 }).to eq Success(2) }
40
+ specify { expect(subject.map { |v| Failure(v + 1) }).to eq Failure(2) }
41
+ specify { expect(subject.map_err { |v| Failure(v + 1) }).to eq Success(1) }
42
+
43
+ specify do
44
+ expect(subject.pipe do |r|
45
+ raise RuntimeError unless r == Success(1)
46
+ end).to eq Success(1)
47
+ end
48
+
49
+ specify { expect(subject.or(Success(2))).to eq Success(1) }
50
+ specify { expect(subject.or_else { Success(2) }).to eq Success(1) }
51
+
52
+ specify { expect(subject.and(Success(2))).to eq Success(2) }
53
+ specify { expect(subject.and(Failure(2))).to eq Failure(2) }
54
+ specify { expect(subject.and_then { Success(2) }).to eq Success(2) }
55
+ specify { expect(subject.and_then { Failure(2) }).to eq Failure(2) }
56
+
57
+ it "try!" do
58
+ expect(described_class.try! { 1 }).to eq Success(1)
59
+ expect(described_class.try! { raise "error" }.inspect).to \
60
+ eq Failure(RuntimeError.new("error")).inspect
61
+ end
62
+ end
63
+
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ describe FunctionalLightService::Enum do
4
+ include FunctionalLightService
5
+
6
+ it "can't use value" do
7
+ expect do
8
+ InvalidEnum = FunctionalLightService.enum do
9
+ Unary(:value)
10
+ end
11
+ end .to raise_error ArgumentError
12
+ end
13
+
14
+ context "Nullary, Unary, Binary" do
15
+ MyEnym = FunctionalLightService.enum do
16
+ Nullary()
17
+ Unary(:a)
18
+ Binary(:a, :b)
19
+ end
20
+
21
+ it "can't instantiate parent" do
22
+ expect { MyEnym.new }.to \
23
+ raise_error NoMethodError, "private method `new' called for MyEnym:Class"
24
+ end
25
+
26
+ it "Nullary" do
27
+ n = MyEnym.Nullary
28
+
29
+ expect(n).to be_a MyEnym
30
+ expect(n).to be_a MyEnym::Nullary
31
+ expect(n.name).to eq "Nullary"
32
+ expect { n.value }.to raise_error NoMethodError
33
+ expect(n.inspect).to eq "Nullary"
34
+ expect(n.to_s).to eq ""
35
+ expect(n.fmap {}).to eq n
36
+ end
37
+
38
+ it "Unary" do
39
+ u = MyEnym::Unary(1)
40
+
41
+ expect(u).to be_a MyEnym
42
+ expect(u).to be_a MyEnym::Unary
43
+ expect(u.name).to eq "Unary"
44
+ expect(u.a).to eq 1
45
+ expect(u.value).to eq 1
46
+ expect(u.inspect).to eq "Unary(1)"
47
+ expect(u.to_s).to eq "1"
48
+ end
49
+
50
+ it "Binary" do
51
+ # hash
52
+ b = MyEnym::Binary(:a => 1, :b => 2)
53
+ expect(b).to be_a MyEnym
54
+ expect(b).to be_a MyEnym::Binary
55
+ expect(b.name).to eq "Binary"
56
+ expect(b.inspect).to eq "Binary(a: 1, b: 2)"
57
+
58
+ expect(b.a).to eq 1
59
+ expect(b.b).to eq 2
60
+ expect(b.value).to eq(:a => 1, :b => 2)
61
+
62
+ # values only
63
+ b = MyEnym::Binary(1, 2)
64
+ expect(b.value).to eq(:a => 1, :b => 2)
65
+
66
+ # other names are ok
67
+ b = MyEnym::Binary(:c => 1, :d => 2)
68
+ expect(b.value).to eq(:a => 1, :b => 2)
69
+
70
+ expect { MyEnym::Binary(1) }.to raise_error ArgumentError
71
+ end
72
+
73
+ it "generated enum" do
74
+ expect(MyEnym.variants.sort).to eq %i[Unary Binary Nullary].sort
75
+ expect(MyEnym.constants.sort.inspect).to eq %i[Matcher Unary Binary Nullary].sort.to_s
76
+
77
+ b = MyEnym::Binary(:a => 1, :b => 2)
78
+
79
+ res =
80
+ MyEnym.match(b) do
81
+ Nullary() { 0 }
82
+ Unary() { |a| a }
83
+ Binary() { |x, y| [x, y] }
84
+ end
85
+
86
+ expect(res).to eq [1, 2]
87
+
88
+ res =
89
+ b.match do
90
+ Nullary() { 0 }
91
+ Unary() { |a| a }
92
+ Binary() { |x, y| [x, y] }
93
+ end
94
+
95
+ expect(res).to eq [1, 2]
96
+
97
+ expect do
98
+ b.match do
99
+ Nullary # Nullary is treated as a constant
100
+ end
101
+ end.to raise_error(NameError)
102
+
103
+ expect do
104
+ b.match do
105
+ Nullary()
106
+ Unary()
107
+ Binary()
108
+ end
109
+ end.to raise_error ArgumentError, "No block given to `Nullary`"
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,83 @@
1
+ require "spec_helper"
2
+ require 'test_doubles'
3
+
4
+ describe FunctionalLightService::LocalizationAdapter do
5
+ let(:action_class) { TestDoubles::AnAction }
6
+ let(:adapter) { described_class.new }
7
+
8
+ describe "#failure" do
9
+ subject { adapter.failure(message_or_key, action_class) }
10
+
11
+ context "when provided a Symbol" do
12
+ let(:message_or_key) { :not_found }
13
+
14
+ it "translates the message" do
15
+ expected_scope = "test_doubles/an_action.light_service.failures"
16
+
17
+ expect(I18n).to receive(:t)
18
+ .with(message_or_key, :scope => expected_scope)
19
+ .and_return("message")
20
+
21
+ expect(subject).to eq("message")
22
+ end
23
+
24
+ it "allows passing interpolation options to I18n layer" do
25
+ expect(I18n).to receive(:t)
26
+ .with(message_or_key, hash_including(:i18n_variable => "value"))
27
+ .and_return("message")
28
+
29
+ subject = adapter.failure(message_or_key,
30
+ action_class,
31
+ :i18n_variable => "value")
32
+
33
+ expect(subject).to eq("message")
34
+ end
35
+ end
36
+
37
+ context "when provided a String" do
38
+ let(:message_or_key) { "action failed" }
39
+
40
+ it "returns the message" do
41
+ expect(subject).to eq(message_or_key)
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#success" do
47
+ subject { adapter.success(message_or_key, action_class) }
48
+
49
+ context "when provided a Symbol" do
50
+ let(:message_or_key) { :not_found }
51
+
52
+ it "translates the message" do
53
+ expected_scope = "test_doubles/an_action.light_service.successes"
54
+
55
+ expect(I18n).to receive(:t)
56
+ .with(message_or_key, :scope => expected_scope)
57
+ .and_return("message")
58
+
59
+ expect(subject).to eq("message")
60
+ end
61
+
62
+ it "allows passing interpolation options to I18n layer" do
63
+ expect(I18n).to receive(:t)
64
+ .with(message_or_key, hash_including(:i18n_variable => "value"))
65
+ .and_return("message")
66
+
67
+ subject = adapter.success(message_or_key,
68
+ action_class,
69
+ :i18n_variable => "value")
70
+
71
+ expect(subject).to eq("message")
72
+ end
73
+ end
74
+
75
+ context "when provided a String" do
76
+ let(:message_or_key) { "action failed" }
77
+
78
+ it "returns the message" do
79
+ expect(subject).to eq(message_or_key)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ describe FunctionalLightService::Organizer::WithReducer do
5
+ let(:context) { FunctionalLightService::Context.make }
6
+ let(:action1) { TestDoubles::NullAction }
7
+ let(:action2) { TestDoubles::NullAction.clone }
8
+ let(:actions) { [action1, action2] }
9
+
10
+ before { context.current_action = action2 }
11
+
12
+ it "reduces the provided actions" do
13
+ result = described_class.new.with(context).reduce(actions)
14
+
15
+ expect(result).to eq(context)
16
+ expect(result).to be_success
17
+ end
18
+
19
+ it "executes a handler around each action and continues reducing" do
20
+ expect(action1).to receive(:execute).with(context).and_return(context)
21
+
22
+ result = described_class.new.with(context)
23
+ .around_each(TestDoubles::AroundEachNullHandler)
24
+ .reduce([action1])
25
+
26
+ expect(result).to eq(context)
27
+ expect(result).to be_success
28
+ end
29
+
30
+ context "when FailWithRollbackError is caught" do
31
+ it "reduces the rollback" do
32
+ expect(action1).to receive(:execute).with(context).and_return(context)
33
+ expect(action2).to receive(:execute).with(context) do
34
+ raise FunctionalLightService::FailWithRollbackError
35
+ end
36
+ expect(action1).to receive(:rollback).with(context).and_return(context)
37
+ expect(action2).to receive(:rollback).with(context).and_return(context)
38
+
39
+ result = described_class.new.with(context).reduce(actions)
40
+
41
+ expect(result).to eq(context)
42
+ end
43
+
44
+ it "reduces the rollback with an action without `rollback`" do
45
+ expect(action1).to receive(:execute).with(context).and_return(context)
46
+ expect(action2).to receive(:execute).with(context) do
47
+ raise FunctionalLightService::FailWithRollbackError
48
+ end
49
+ expect(action2).to receive(:rollback).with(context).and_return(context)
50
+
51
+ result = described_class.new.with(context).reduce(actions)
52
+
53
+ expect(result).to eq(context)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ describe "organizer aliases macro" do
5
+ let(:organizer_with_alias) do
6
+ Class.new do
7
+ extend FunctionalLightService::Organizer
8
+
9
+ aliases :promised_key => :expected_key
10
+
11
+ def self.call(ctx = {})
12
+ with(ctx).reduce(
13
+ [
14
+ TestDoubles::PromisesPromisedKeyAction,
15
+ TestDoubles::ExpectsExpectedKeyAction
16
+ ]
17
+ )
18
+ end
19
+ end
20
+ end
21
+
22
+ context "when aliases is invoked" do
23
+ it "makes aliases available to the actions" do
24
+ result = organizer_with_alias.call
25
+ expect(result[:expected_key]).to eq(result[:promised_key])
26
+ expect(result.expected_key).to eq(result[:promised_key])
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ describe FunctionalLightService::Organizer do
5
+ let(:ctx) { FunctionalLightService::Context.make(:user => user) }
6
+ let(:user) { double(:user) }
7
+
8
+ context "when #with is called with hash" do
9
+ before do
10
+ expect(TestDoubles::AnAction).to receive(:execute)
11
+ .with(ctx)
12
+ .and_return(ctx)
13
+ expect(TestDoubles::AnotherAction).to receive(:execute)
14
+ .with(ctx)
15
+ .and_return(ctx)
16
+ end
17
+
18
+ it "implicitly creates a Context" do
19
+ result = TestDoubles::AnOrganizer.call(:user => user)
20
+ expect(result).to eq(ctx)
21
+ end
22
+ end
23
+
24
+ context "when #with is called with Context" do
25
+ before do
26
+ expect(TestDoubles::AnAction).to receive(:execute)
27
+ .with(ctx)
28
+ .and_return(ctx)
29
+ expect(TestDoubles::AnotherAction).to receive(:execute)
30
+ .with(ctx)
31
+ .and_return(ctx)
32
+ end
33
+
34
+ it "uses that Context without recreating it" do
35
+ result = TestDoubles::AnOrganizer.call(ctx)
36
+ expect(result).to eq(ctx)
37
+ end
38
+ end
39
+
40
+ context "when no Actions are specified" do
41
+ it "throws a Runtime error" do
42
+ expect { TestDoubles::AnOrganizer.do_something_with_no_actions(ctx) }.to \
43
+ raise_error(RuntimeError, "No action(s) were provided")
44
+ end
45
+ end
46
+
47
+ context "when aliases are declared" do
48
+ let(:organizer) do
49
+ Class.new do
50
+ extend FunctionalLightService::Organizer
51
+ aliases :foo => :bar
52
+
53
+ def self.call
54
+ with.reduce(TestDoubles::AnAction)
55
+ end
56
+ end
57
+ end
58
+
59
+ it "merges the aliases into the data" do
60
+ with_reducer = double(:reduce => true)
61
+
62
+ allow(described_class::WithReducerFactory).to receive(:make)
63
+ .and_return(with_reducer)
64
+
65
+ expect(with_reducer).to receive(:with)
66
+ .with(hash_including(:_aliases => { :foo => :bar }))
67
+ .and_return(with_reducer)
68
+
69
+ organizer.call
70
+ end
71
+ end
72
+
73
+ context "when an organizer is nested and reduced within another" do
74
+ let(:reduced) { TestDoubles::NestingOrganizer.call(ctx) }
75
+ let(:organizer_result) do
76
+ TestDoubles::NotExplicitlyReturningContextOrganizer.call(ctx)
77
+ end
78
+
79
+ it "reduces an organizer which returns something" do
80
+ expect(organizer_result).to eq([1, 2, 3])
81
+ end
82
+
83
+ it "adds :foo and :bar to the context" do
84
+ reduced
85
+ expect(ctx[:foo]).to eq([1, 2, 3])
86
+ expect(ctx[:bar]).to eq(ctx[:foo])
87
+ end
88
+
89
+ it "returns the context" do
90
+ expect(reduced).to eq(ctx)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ # rubocop:disable Style/MixinUsage
4
+ include FunctionalLightService::Prelude::Result
5
+ # rubocop:enable Style/MixinUsage
6
+
7
+ Success(1).to_s # => "1"
8
+ Success(Success(1)) # => Success(1)
9
+
10
+ Failure(1).to_s # => "1"
11
+ Failure(Failure(1)) # => Failure(1)
12
+
13
+ Success(1).fmap { |v| v + 1 } # => Success(2)
14
+ Failure(1).fmap { |v| v - 1 } # => Failure(0)
15
+
16
+ Threenum = FunctionalLightService.enum do
17
+ Nullary()
18
+ Unary(:a)
19
+ Binary(:a, :b)
20
+ end
21
+
22
+ FunctionalLightService.impl(Threenum) do
23
+ def sum
24
+ match do
25
+ Nullary() { 0 }
26
+ Unary() { |u| u }
27
+ Binary() { |a, b| a + b }
28
+ end
29
+ end
30
+
31
+ def +(other)
32
+ match do
33
+ Nullary() { other.sum }
34
+ Unary() { |_a| sum + other.sum }
35
+ Binary() { |_a, _b| sum + other.sum }
36
+ end
37
+ end
38
+ end
39
+
40
+ describe Threenum do
41
+ it "works" do
42
+ expect(Threenum.Nullary + Threenum.Unary(1)).to eq 1
43
+ expect(Threenum.Nullary + Threenum.Binary(2, 3)).to eq 5
44
+ expect(Threenum.Unary(1) + Threenum.Binary(2, 3)).to eq 6
45
+ expect(Threenum.Binary(2, 3) + Threenum.Binary(2, 3)).to eq 10
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+ require_relative 'tax/calculates_order_tax_action'
3
+
4
+ describe CalculatesOrderTaxAction do
5
+ let(:order) { double('order') }
6
+ let(:context) do
7
+ data = { :order => order, :tax_percentage => 7.2 }
8
+ ::FunctionalLightService::Context.make(data)
9
+ end
10
+
11
+ it "calculates the tax based on the tax percentage" do
12
+ allow(order).to receive_messages(:total => 100)
13
+ expect(order).to receive(:tax=).with 7.2
14
+ CalculatesOrderTaxAction.execute(context)
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require_relative 'tax/calculates_tax'
3
+ require_relative 'tax/looks_up_tax_percentage_action'
4
+ require_relative 'tax/calculates_order_tax_action'
5
+ require_relative 'tax/provides_free_shipping_action'
6
+
7
+ describe CalculatesTax do
8
+ let(:order) { double('order') }
9
+ let(:ctx) { FunctionalLightService::Context.make(:user => nil) }
10
+
11
+ it "calls the actions in order" do
12
+ allow(FunctionalLightService::Context).to receive(:make)
13
+ .with(:order => order)
14
+ .and_return(ctx)
15
+
16
+ allow(LooksUpTaxPercentageAction).to receive(:execute)
17
+ .with(ctx)
18
+ .and_return(ctx)
19
+ allow(CalculatesOrderTaxAction).to receive(:execute)
20
+ .with(ctx)
21
+ .and_return(ctx)
22
+ allow(ProvidesFreeShippingAction).to receive(:execute)
23
+ .with(ctx)
24
+ .and_return(ctx)
25
+
26
+ result = CalculatesTax.call(order)
27
+
28
+ expect(result).to eq(ctx)
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require_relative 'tax/looks_up_tax_percentage_action'
3
+
4
+ class TaxRange; end
5
+
6
+ describe LooksUpTaxPercentageAction do
7
+ let(:region) { double('region') }
8
+ let(:order) do
9
+ order = double('order')
10
+ allow(order).to receive_messages(:region => region)
11
+ allow(order).to receive_messages(:total => 200)
12
+ order
13
+ end
14
+ let(:context) do
15
+ ::FunctionalLightService::Context.make(:order => order)
16
+ end
17
+ let(:tax_percentage) { double('tax_percentage') }
18
+ let(:tax_ranges) { double('tax_ranges') }
19
+
20
+ context "when the tax_ranges were not found" do
21
+ it "sets the context to failure" do
22
+ allow(TaxRange).to receive(:for_region).with(region).and_return nil
23
+ LooksUpTaxPercentageAction.execute(context)
24
+
25
+ expect(context).to be_failure
26
+ expect(context.message).to eq "The tax ranges were not found"
27
+ end
28
+ end
29
+
30
+ context "when the tax_percentage is not found" do
31
+ it "sets the context to failure" do
32
+ allow(TaxRange).to receive(:for_region).with(region).and_return tax_ranges
33
+ allow(tax_ranges).to receive_messages(:for_total => nil)
34
+
35
+ LooksUpTaxPercentageAction.execute(context)
36
+
37
+ expect(context).to be_failure
38
+ expect(context.message).to eq "The tax percentage was not found"
39
+ end
40
+ end
41
+
42
+ context "when the tax_percentage is found" do
43
+ it "sets the tax_percentage in context" do
44
+ allow(TaxRange).to receive(:for_region).with(region).and_return tax_ranges
45
+ allow(tax_ranges).to receive_messages(:for_total => 25)
46
+
47
+ LooksUpTaxPercentageAction.execute(context)
48
+
49
+ expect(context).to be_success
50
+ expect(context.fetch(:tax_percentage)).to eq 25
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require_relative 'tax/provides_free_shipping_action'
3
+
4
+ describe ProvidesFreeShippingAction do
5
+ let(:order) { double('order') }
6
+ let(:ctx) { { :order => order } }
7
+
8
+ context "when the order total with tax is > 200" do
9
+ specify "order gets free shipping" do
10
+ allow(order).to receive_messages(:total_with_tax => 201)
11
+ expect(order).to receive(:provide_free_shipping!)
12
+
13
+ ProvidesFreeShippingAction.execute(ctx)
14
+ end
15
+ end
16
+
17
+ context "when the order total with tax is <= 200" do
18
+ specify "order gets free shipping" do
19
+ allow(order).to receive_messages(:total_with_tax => 200)
20
+ expect(order).not_to receive(:provide_free_shipping!)
21
+
22
+ ProvidesFreeShippingAction.execute(ctx)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ class CalculatesOrderTaxAction
2
+ extend ::FunctionalLightService::Action
3
+ expects :order, :tax_percentage
4
+
5
+ executed do |ctx|
6
+ order_total = (ctx.order.total * (ctx.tax_percentage / 100))
7
+ ctx.order.tax = order_total.round(2)
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ class CalculatesTax
2
+ extend FunctionalLightService::Organizer
3
+
4
+ def self.call(order)
5
+ with(:order => order).reduce(
6
+ LooksUpTaxPercentageAction,
7
+ CalculatesOrderTaxAction,
8
+ ProvidesFreeShippingAction
9
+ )
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ class LooksUpTaxPercentageAction
2
+ extend FunctionalLightService::Action
3
+ expects :order
4
+ promises :tax_percentage
5
+
6
+ executed do |ctx|
7
+ tax_ranges = TaxRange.for_region(ctx.order.region)
8
+ ctx.tax_percentage = 0
9
+
10
+ next ctx if object_is_nil?(tax_ranges, ctx, 'The tax ranges were not found')
11
+
12
+ ctx.tax_percentage = tax_ranges.for_total(ctx.order.total)
13
+
14
+ error_message = 'The tax percentage was not found'
15
+ next ctx if object_is_nil?(ctx.tax_percentage, ctx, error_message)
16
+ end
17
+
18
+ def self.object_is_nil?(object, ctx, message)
19
+ if object.nil?
20
+ ctx.fail!(message)
21
+ return true
22
+ end
23
+
24
+ false
25
+ end
26
+ private_class_method :object_is_nil?
27
+ end
@@ -0,0 +1,10 @@
1
+ class ProvidesFreeShippingAction
2
+ extend FunctionalLightService::Action
3
+ expects :order
4
+
5
+ executed do |context|
6
+ order = context.order
7
+
8
+ order.provide_free_shipping! if order.total_with_tax > 200
9
+ end
10
+ end