functional-light-service 0.2.4

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 (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