functional-light-service 0.3.4 → 0.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/project-build.yml +47 -0
- data/.rubocop.yml +103 -15
- data/.solargraph.yml +11 -0
- data/Appraisals +6 -2
- data/CHANGELOG.md +128 -0
- data/Gemfile +1 -3
- data/README.md +1492 -1424
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/functional-light-service.gemspec +19 -8
- data/gemfiles/dry_inflector_0_2_1.gemfile +5 -0
- data/gemfiles/i18n_1_8_11.gemfile +5 -0
- data/lib/functional-light-service/context/key_verifier.rb +2 -2
- data/lib/functional-light-service/context.rb +152 -154
- data/lib/functional-light-service/functional/enum.rb +2 -6
- data/lib/functional-light-service/functional/maybe.rb +1 -0
- data/lib/functional-light-service/functional/null.rb +1 -1
- data/lib/functional-light-service/functional/option.rb +0 -2
- data/lib/functional-light-service/functional/result.rb +2 -2
- data/lib/functional-light-service/organizer/with_reducer.rb +6 -0
- data/lib/functional-light-service/organizer/with_reducer_factory.rb +1 -1
- data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +5 -2
- data/lib/functional-light-service/organizer.rb +10 -0
- data/lib/functional-light-service/testing/context_factory.rb +2 -0
- data/lib/functional-light-service/version.rb +1 -1
- data/spec/acceptance/fail_spec.rb +42 -16
- data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
- data/spec/acceptance/organizer/iterate_spec.rb +7 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
- data/spec/action_spec.rb +8 -0
- data/spec/context/inspect_spec.rb +6 -21
- data/spec/context_spec.rb +1 -1
- data/spec/lib/deterministic/monad_axioms.rb +2 -0
- data/spec/lib/deterministic/monad_spec.rb +2 -0
- data/spec/lib/deterministic/null_spec.rb +2 -0
- data/spec/lib/deterministic/option_spec.rb +18 -14
- data/spec/lib/enum_spec.rb +3 -1
- data/spec/organizer_spec.rb +21 -0
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
- data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
- data/spec/spec_helper.rb +8 -5
- data/spec/test_doubles.rb +56 -9
- metadata +156 -26
- data/.travis.yml +0 -24
- data/gemfiles/dry_inflector_0_2.gemfile +0 -8
- data/gemfiles/i18n_1_8.gemfile +0 -8
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe FunctionalLightService::Organizer do
|
|
4
|
+
class TestAddToContext
|
|
5
|
+
extend FunctionalLightService::Organizer
|
|
6
|
+
|
|
7
|
+
def self.call(context = FunctionalLightService::Context.make)
|
|
8
|
+
with(context).reduce(steps)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.steps
|
|
12
|
+
[
|
|
13
|
+
# This will add the `:number` key to the context
|
|
14
|
+
# with the value of 0, so it's available for
|
|
15
|
+
# AddsOneAction
|
|
16
|
+
add_to_context(:number => 0),
|
|
17
|
+
TestDoubles::AddsOneAction,
|
|
18
|
+
add_to_context(:something => "hello")
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "adds items to the context on the fly" do
|
|
24
|
+
result = TestAddToContext.call
|
|
25
|
+
|
|
26
|
+
expect(result).to be_success
|
|
27
|
+
expect(result.number).to eq(1)
|
|
28
|
+
expect(result[:something]).to eq("hello")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -20,6 +20,13 @@ RSpec.describe FunctionalLightService::Organizer do
|
|
|
20
20
|
expect(result.number).to eq(5)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
it "knows that it's being iterated from within an organizer" do
|
|
24
|
+
result = TestDoubles::TestIterate.call(:number => 1,
|
|
25
|
+
:counters => [1, 2, 3, 4])
|
|
26
|
+
|
|
27
|
+
expect(result.organized_by).to eq TestDoubles::TestIterate
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
it 'will not iterate over a failed context' do
|
|
24
31
|
empty_context.fail!('Something bad happened')
|
|
25
32
|
|
|
@@ -48,4 +48,42 @@ RSpec.describe FunctionalLightService::Organizer do
|
|
|
48
48
|
result = TestReduceIf.call(empty_context)
|
|
49
49
|
expect(result).to be_success
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
it "knows that it's being conditionally reduced from within an organizer" do
|
|
53
|
+
result = TestReduceIf.call(:number => 2)
|
|
54
|
+
|
|
55
|
+
expect(result.organized_by).to eq TestReduceIf
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'skips actions within in its own scope' do
|
|
59
|
+
org = Class.new do
|
|
60
|
+
extend FunctionalLightService::Organizer
|
|
61
|
+
|
|
62
|
+
def self.call
|
|
63
|
+
reduce(actions)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.actions
|
|
67
|
+
[
|
|
68
|
+
reduce_if(
|
|
69
|
+
->(c) { !c.nil? },
|
|
70
|
+
[
|
|
71
|
+
execute(->(c) { c[:first_reduce_if] = true }),
|
|
72
|
+
execute(->(c) { c.skip_remaining! }),
|
|
73
|
+
execute(->(c) { c[:second_reduce_if] = true })
|
|
74
|
+
]
|
|
75
|
+
),
|
|
76
|
+
execute(->(c) { c[:last_outside] = true })
|
|
77
|
+
]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
result = org.call
|
|
82
|
+
|
|
83
|
+
aggregate_failures do
|
|
84
|
+
expect(result[:first_reduce_if]).to be true
|
|
85
|
+
expect(result[:second_reduce_if]).to be_nil
|
|
86
|
+
expect(result[:last_outside]).to be true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
51
89
|
end
|
|
@@ -40,4 +40,10 @@ RSpec.describe FunctionalLightService::Organizer do
|
|
|
40
40
|
result = TestReduceUntil.call(empty_context)
|
|
41
41
|
expect(result).to be_success
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
it "is expected to know its organizer when reducing until a condition" do
|
|
45
|
+
result = TestReduceUntil.call(:number => 1)
|
|
46
|
+
|
|
47
|
+
expect(result.organized_by).to eq TestReduceUntil
|
|
48
|
+
end
|
|
43
49
|
end
|
data/spec/action_spec.rb
CHANGED
|
@@ -68,6 +68,14 @@ describe FunctionalLightService::Action do
|
|
|
68
68
|
expect(result.to_hash).to eq(:number => 2)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
context "when called directly" do
|
|
72
|
+
it "is expected to not be organized" do
|
|
73
|
+
result = TestDoubles::AddsTwoActionWithFetch.execute(context)
|
|
74
|
+
|
|
75
|
+
expect(result.organized_by).to be_nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
71
79
|
context "when invoked with hash" do
|
|
72
80
|
it "creates FunctionalLightService::Context implicitly" do
|
|
73
81
|
ctx = { :some_key => "some value" }
|
|
@@ -13,13 +13,8 @@ RSpec.describe FunctionalLightService::Context do
|
|
|
13
13
|
describe '#inspect' do
|
|
14
14
|
it 'inspects the hash with all the fields' do
|
|
15
15
|
inspected_context =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
+ 'message: \'\', ' \
|
|
19
|
-
+ 'error_code: nil, ' \
|
|
20
|
-
+ 'skip_remaining: false, ' \
|
|
21
|
-
+ 'aliases: {}' \
|
|
22
|
-
+ ')'
|
|
16
|
+
"FunctionalLightService::Context({}, success: true, message: '', error_code: nil, " \
|
|
17
|
+
"skip_remaining: false, aliases: {})"
|
|
23
18
|
|
|
24
19
|
expect(context.inspect).to eq(inspected_context)
|
|
25
20
|
end
|
|
@@ -28,13 +23,8 @@ RSpec.describe FunctionalLightService::Context do
|
|
|
28
23
|
context.fail!('There was an error')
|
|
29
24
|
|
|
30
25
|
inspected_context =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
+ 'message: \'There was an error\', ' \
|
|
34
|
-
+ 'error_code: nil, ' \
|
|
35
|
-
+ 'skip_remaining: false, ' \
|
|
36
|
-
+ 'aliases: {}' \
|
|
37
|
-
+ ')'
|
|
26
|
+
"FunctionalLightService::Context({}, success: false, message: 'There was an error', " \
|
|
27
|
+
"error_code: nil, skip_remaining: false, aliases: {})"
|
|
38
28
|
|
|
39
29
|
expect(context.inspect).to eq(inspected_context)
|
|
40
30
|
end
|
|
@@ -43,13 +33,8 @@ RSpec.describe FunctionalLightService::Context do
|
|
|
43
33
|
context.skip_remaining!('No need to process')
|
|
44
34
|
|
|
45
35
|
inspected_context =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
+ 'message: \'No need to process\', ' \
|
|
49
|
-
+ 'error_code: nil, ' \
|
|
50
|
-
+ 'skip_remaining: true, ' \
|
|
51
|
-
+ 'aliases: {}' \
|
|
52
|
-
+ ')'
|
|
36
|
+
"FunctionalLightService::Context({}, success: true, message: 'No need to process', " \
|
|
37
|
+
"error_code: nil, skip_remaining: true, aliases: {})"
|
|
53
38
|
|
|
54
39
|
expect(context.inspect).to eq(inspected_context)
|
|
55
40
|
end
|
data/spec/context_spec.rb
CHANGED
|
@@ -167,7 +167,7 @@ RSpec.describe FunctionalLightService::Context do
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
it "allows a default block value for #fetch" do
|
|
170
|
-
expect(context.fetch(:madeup
|
|
170
|
+
expect(context.fetch(:madeup, :default)).to eq(:default)
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
context "when aliases are included via .make" do
|
|
@@ -34,7 +34,9 @@ shared_examples 'a Monad' do
|
|
|
34
34
|
|
|
35
35
|
it '#bind must return a monad' do
|
|
36
36
|
expect(monad.new(1).bind { |v| monad.new(v) }).to eq monad.new(1)
|
|
37
|
+
# rubocop:disable Lint/EmptyBlock
|
|
37
38
|
expect { monad.new(1).bind {} }.to raise_error(FunctionalLightService::Monad::NotMonadError)
|
|
39
|
+
# rubocop:enable Lint/EmptyBlock
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
it '#new must return a monad' do
|
|
@@ -21,8 +21,10 @@ describe FunctionalLightService::Monad do
|
|
|
21
21
|
|
|
22
22
|
context '#bind' do
|
|
23
23
|
it "raises an error if the passed function does not return a monad of the same class" do
|
|
24
|
+
# rubocop:disable Lint/EmptyBlock
|
|
24
25
|
expect { Identity.new(1).bind {} }.to \
|
|
25
26
|
raise_error(FunctionalLightService::Monad::NotMonadError)
|
|
27
|
+
# rubocop:enable Lint/EmptyBlock
|
|
26
28
|
end
|
|
27
29
|
specify { expect(Identity.new(1).bind { |value| Identity.new(value) }).to eq Identity.new(1) }
|
|
28
30
|
|
|
@@ -38,7 +38,9 @@ describe Null do
|
|
|
38
38
|
null = Null.instance
|
|
39
39
|
expect(null.to_str).to eq ""
|
|
40
40
|
expect(null.to_ary).to eq []
|
|
41
|
+
# rubocop:disable Style/StringConcatenation
|
|
41
42
|
expect("" + null).to eq ""
|
|
43
|
+
# rubocop:enable Style/StringConcatenation
|
|
42
44
|
|
|
43
45
|
a, b, c = null
|
|
44
46
|
expect(a).to be_nil
|
|
@@ -72,27 +72,31 @@ describe FunctionalLightService::Option do
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
it "match" do
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
expect(
|
|
76
|
+
# rubocop:disable Lint/UnusedBlockArgument
|
|
77
|
+
# rubocop:disable Lint/EmptyBlock
|
|
78
|
+
Some(0).match do
|
|
79
|
+
Some(where { s == 1 }) { |s| 99 }
|
|
80
|
+
Some(where { s == 0 }) { |s| s + 1 }
|
|
81
|
+
None() {}
|
|
82
|
+
end
|
|
83
|
+
).to eq 1
|
|
82
84
|
|
|
83
85
|
expect(
|
|
84
86
|
Some(1).match do
|
|
85
87
|
None() { 0 }
|
|
86
|
-
Some() { |
|
|
88
|
+
Some() { |s| 1 }
|
|
87
89
|
end
|
|
88
90
|
).to eq 1
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
expect(
|
|
93
|
+
Some(1).match do
|
|
94
|
+
None() { 0 }
|
|
95
|
+
Some(where { s.is_a? Integer }) { |s| 1 }
|
|
96
|
+
end
|
|
97
|
+
# rubocop:enable Lint/UnusedBlockArgument
|
|
98
|
+
# rubocop:enable Lint/EmptyBlock
|
|
99
|
+
).to eq 1
|
|
96
100
|
|
|
97
101
|
expect(
|
|
98
102
|
None.match do
|
data/spec/lib/enum_spec.rb
CHANGED
|
@@ -8,7 +8,7 @@ describe FunctionalLightService::Enum do
|
|
|
8
8
|
InvalidEnum = FunctionalLightService.enum do
|
|
9
9
|
Unary(:value)
|
|
10
10
|
end
|
|
11
|
-
end
|
|
11
|
+
end.to raise_error ArgumentError
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
context "Nullary, Unary, Binary" do
|
|
@@ -32,7 +32,9 @@ describe FunctionalLightService::Enum do
|
|
|
32
32
|
expect { n.value }.to raise_error NoMethodError
|
|
33
33
|
expect(n.inspect).to eq "Nullary"
|
|
34
34
|
expect(n.to_s).to eq ""
|
|
35
|
+
# rubocop:disable Lint/EmptyBlock
|
|
35
36
|
expect(n.fmap {}).to eq n
|
|
37
|
+
# rubocop:enable Lint/EmptyBlock
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
it "Unary" do
|
data/spec/organizer_spec.rb
CHANGED
|
@@ -19,6 +19,11 @@ describe FunctionalLightService::Organizer do
|
|
|
19
19
|
result = TestDoubles::AnOrganizer.call(:user => user)
|
|
20
20
|
expect(result).to eq(ctx)
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
it "sets itself as the organizer" do
|
|
24
|
+
result = TestDoubles::AnOrganizer.call(:user => user)
|
|
25
|
+
expect(result.organized_by).to eq TestDoubles::AnOrganizer
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
context "when #with is called with Context" do
|
|
@@ -90,4 +95,20 @@ describe FunctionalLightService::Organizer do
|
|
|
90
95
|
expect(reduced).to eq(ctx)
|
|
91
96
|
end
|
|
92
97
|
end
|
|
98
|
+
|
|
99
|
+
context "can add items to the context" do
|
|
100
|
+
specify 'with #add_to_context' do
|
|
101
|
+
result = TestDoubles::AnOrganizerThatAddsToContext.call
|
|
102
|
+
expect(result[:strongest_avenger]).to eq :thor
|
|
103
|
+
expect(result[:last_jedi]).to eq "Rey"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
context "can assign key aliaeses" do
|
|
108
|
+
it 'with #add_aliases' do
|
|
109
|
+
result = TestDoubles::AnOrganizerThatAddsAliases.call
|
|
110
|
+
expect(result[:foo]).to eq :bar
|
|
111
|
+
expect(result[:baz]).to eq :bar
|
|
112
|
+
end
|
|
113
|
+
end
|
|
93
114
|
end
|
|
@@ -15,7 +15,7 @@ describe ProvidesFreeShippingAction do
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
context "when the order total with tax is <= 200" do
|
|
18
|
-
specify "order
|
|
18
|
+
specify "order does not get free shipping" do
|
|
19
19
|
allow(order).to receive_messages(:total_with_tax => 200)
|
|
20
20
|
expect(order).not_to receive(:provide_free_shipping!)
|
|
21
21
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -10,11 +10,14 @@ if ENV['RUN_COVERAGE_REPORT']
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
SimpleCov.minimum_coverage_by_file 90
|
|
13
|
+
|
|
14
|
+
require "simplecov-cobertura"
|
|
15
|
+
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
require
|
|
16
|
-
require
|
|
18
|
+
require "functional-light-service"
|
|
19
|
+
require "functional-light-service/testing"
|
|
17
20
|
require "functional-light-service/functional/null"
|
|
18
|
-
require
|
|
19
|
-
require
|
|
20
|
-
require
|
|
21
|
+
require "support"
|
|
22
|
+
require "test_doubles"
|
|
23
|
+
require "stringio"
|
data/spec/test_doubles.rb
CHANGED
|
@@ -45,6 +45,7 @@ module TestDoubles
|
|
|
45
45
|
|
|
46
46
|
class TestLogger
|
|
47
47
|
attr_accessor :logs
|
|
48
|
+
|
|
48
49
|
def initialize
|
|
49
50
|
@logs = []
|
|
50
51
|
end
|
|
@@ -83,8 +84,13 @@ module TestDoubles
|
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
|
|
86
|
-
class AnAction
|
|
87
|
-
|
|
87
|
+
class AnAction
|
|
88
|
+
extend FunctionalLightService::Action
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class AnotherAction
|
|
92
|
+
extend FunctionalLightService::Action
|
|
93
|
+
end
|
|
88
94
|
|
|
89
95
|
class AnOrganizer
|
|
90
96
|
extend FunctionalLightService::Organizer
|
|
@@ -166,14 +172,18 @@ module TestDoubles
|
|
|
166
172
|
promises :latte
|
|
167
173
|
|
|
168
174
|
executed do |context|
|
|
175
|
+
context.fail!("Can't make a latte from a milk that's very hot!") if context.milk == :very_hot
|
|
176
|
+
|
|
177
|
+
if context.milk == :super_hot
|
|
178
|
+
error_message = "Can't make a latte from a milk that's super hot!"
|
|
179
|
+
context.fail_with_rollback!(error_message)
|
|
180
|
+
end
|
|
181
|
+
|
|
169
182
|
context[:latte] = "#{context.coffee} - with lots of #{context.milk}"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
context.fail_with_rollback!("Can't make a latte from a milk that's super hot!")
|
|
175
|
-
when "5%" then
|
|
176
|
-
context.skip_remaining!("Can't make a latte with a fatty milk like that!")
|
|
183
|
+
|
|
184
|
+
if context.milk == "5%"
|
|
185
|
+
msg = "Can't make a latte with a fatty milk like that!"
|
|
186
|
+
context.skip_remaining!(msg)
|
|
177
187
|
next context
|
|
178
188
|
end
|
|
179
189
|
end
|
|
@@ -472,7 +482,9 @@ module TestDoubles
|
|
|
472
482
|
class NullAction
|
|
473
483
|
extend FunctionalLightService::Action
|
|
474
484
|
|
|
485
|
+
# rubocop:disable Lint/EmptyBlock
|
|
475
486
|
executed { |_ctx| }
|
|
487
|
+
# rubocop:enable Lint/EmptyBlock
|
|
476
488
|
end
|
|
477
489
|
|
|
478
490
|
class TestIterate
|
|
@@ -549,4 +561,39 @@ module TestDoubles
|
|
|
549
561
|
ctx.total += ctx.number
|
|
550
562
|
end
|
|
551
563
|
end
|
|
564
|
+
|
|
565
|
+
class CapitalizeMessage
|
|
566
|
+
extend FunctionalLightService::Action
|
|
567
|
+
expects :a_message
|
|
568
|
+
promises :final_message
|
|
569
|
+
|
|
570
|
+
executed do |ctx|
|
|
571
|
+
ctx.final_message = ctx.a_message.upcase
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
class AnOrganizerThatAddsToContext
|
|
576
|
+
extend FunctionalLightService::Organizer
|
|
577
|
+
def self.call
|
|
578
|
+
with.reduce(actions)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def self.actions
|
|
582
|
+
[add_to_context(
|
|
583
|
+
:strongest_avenger => :thor,
|
|
584
|
+
:last_jedi => "Rey"
|
|
585
|
+
)]
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
class AnOrganizerThatAddsAliases
|
|
590
|
+
extend FunctionalLightService::Organizer
|
|
591
|
+
def self.call
|
|
592
|
+
with(:foo => :bar).reduce(actions)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def self.actions
|
|
596
|
+
[add_aliases(:foo => :baz)]
|
|
597
|
+
end
|
|
598
|
+
end
|
|
552
599
|
end
|