functional-light-service 6.0.0 → 6.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +34 -0
- data/lib/functional-light-service/functional/sequencer.rb +144 -0
- data/lib/functional-light-service/version.rb +1 -1
- data/lib/functional-light-service.rb +1 -0
- data/spec/lib/deterministic/sequencer_spec.rb +506 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 436934533c3db4aa6ac6e3ce7410b67454fe0e7f3f8b516e9b6ba7674e86fc29
|
|
4
|
+
data.tar.gz: c4a36634905f4253a0d32bf9068b70a236f67291e43e20d02f3617679a797ef7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ba109d1688a3e2e3c1904682d55e800cd841fa7870364f04916d9f5528ac0e954fe28f650261b1217c0d0819f0df0c1143331fd82f72a1dfc6726549a95cc82
|
|
7
|
+
data.tar.gz: 4da5cc5beae3e452b7400ce696a05a7ab5e69c354d27cd3c3e918c86ef47166789a7349a9967215ca53f35cb4d00dac986ac109d673cee00e07abace42b6569f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 6.1.0 (2026-07-03)
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Sequencer: do-notation per Result con in_sequence/get/let/and_then/observe/and_yield, portato dalla gem deterministic (MIT) e incluso nel Prelude ( 2026-07-03 ) [ sphynx79]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
## 6.0.0 (2026-07-03)
|
|
2
9
|
|
|
3
10
|
Release maggiore guidata da un audit tecnico completo (vedi `AUDIT-functional-light-service.md`
|
data/README.md
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* [Usage](#functional-usage)
|
|
29
29
|
* [Result: Success & Failure](#functional-usage-success-failure)
|
|
30
30
|
* [Result Chaining](#functional-usage-chaining)
|
|
31
|
+
* [Sequencing (do-notation)](#functional-usage-sequencing)
|
|
31
32
|
* [Complex Example in a Builder Action](#functional-usage-complex-action)
|
|
32
33
|
* [Pattern matching](#functional-usage-pattern-matching)
|
|
33
34
|
* [Option](#functional-usage-option)
|
|
@@ -1150,6 +1151,39 @@ Success(params) >>
|
|
|
1150
1151
|
build_response
|
|
1151
1152
|
```
|
|
1152
1153
|
|
|
1154
|
+
### Sequencing (do-notation) – `in_sequence` <a name="functional-usage-sequencing"></a>
|
|
1155
|
+
|
|
1156
|
+
When a pipeline needs the intermediate values of earlier steps, chaining alone
|
|
1157
|
+
gets awkward. `in_sequence` (ported from the [deterministic](https://github.com/pzol/deterministic)
|
|
1158
|
+
gem, MIT License) gives you a do-notation style block: each step returns a
|
|
1159
|
+
`Result`, the sequence short-circuits on the first `Failure`, and values bound
|
|
1160
|
+
with `get`/`let` are available to all subsequent steps by name.
|
|
1161
|
+
|
|
1162
|
+
```ruby
|
|
1163
|
+
class DownloadRemit
|
|
1164
|
+
include FunctionalLightService::Prelude
|
|
1165
|
+
|
|
1166
|
+
def call(row)
|
|
1167
|
+
in_sequence do
|
|
1168
|
+
get(:url) { extract_url(row) } # binds the Success value to :url
|
|
1169
|
+
get(:file) { fetch(url) } # :url is available here
|
|
1170
|
+
let(:name) { File.basename(url) } # binds a plain (non-Result) value
|
|
1171
|
+
and_then { validate(file) } # step without binding
|
|
1172
|
+
observe { logger.info("got #{name}") } # side effect, return value ignored
|
|
1173
|
+
and_yield { Success(name) } # final result of the sequence
|
|
1174
|
+
end
|
|
1175
|
+
end
|
|
1176
|
+
end
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
* `get(:name) { ... }` – runs a step returning a `Result`; on `Success` binds the
|
|
1180
|
+
unwrapped value to `name`, on `Failure` stops the sequence and returns it.
|
|
1181
|
+
* `let(:name) { ... }` – binds the block's plain return value (no `Result` involved).
|
|
1182
|
+
* `and_then { ... }` – runs a step returning a `Result` without binding its value.
|
|
1183
|
+
* `observe { ... }` – runs a side effect; its return value is ignored.
|
|
1184
|
+
* `and_yield { ... }` – mandatory final step; its `Result` is the value of the
|
|
1185
|
+
whole `in_sequence` block.
|
|
1186
|
+
|
|
1153
1187
|
#### Complex Example in a Builder Action <a name="functional-usage-complex-action"></a>
|
|
1154
1188
|
|
|
1155
1189
|
```ruby
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module FunctionalLightService
|
|
6
|
+
# Do-notation for Result: chains steps that return Success/Failure,
|
|
7
|
+
# short-circuiting on the first Failure. Ported from the deterministic
|
|
8
|
+
# gem (MIT License, Copyright (c) Piotr Zolnierek and contributors,
|
|
9
|
+
# https://github.com/pzol/deterministic).
|
|
10
|
+
#
|
|
11
|
+
# in_sequence do
|
|
12
|
+
# get(:user) { fetch_user(id) } # binds the Success value to :user
|
|
13
|
+
# let(:name) { user.fetch(:name) } # binds a plain (non-Result) value
|
|
14
|
+
# and_then { validate(user) } # step without binding
|
|
15
|
+
# observe { log(name) } # side effect, return value ignored
|
|
16
|
+
# and_yield { Success(name) } # final result of the sequence
|
|
17
|
+
# end
|
|
18
|
+
module Sequencer
|
|
19
|
+
class InvalidSequenceError < StandardError; end
|
|
20
|
+
|
|
21
|
+
module Operation
|
|
22
|
+
Get = Struct.new(:block, :name)
|
|
23
|
+
Let = Struct.new(:block, :name)
|
|
24
|
+
AndThen = Struct.new(:block)
|
|
25
|
+
Observe = Struct.new(:block)
|
|
26
|
+
AndYield = Struct.new(:block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def in_sequence(&)
|
|
30
|
+
sequencer = Sequencer.new(self)
|
|
31
|
+
sequencer.instance_eval(&)
|
|
32
|
+
sequencer.yield
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Sequencer
|
|
36
|
+
def initialize(instance)
|
|
37
|
+
@operations = []
|
|
38
|
+
@operation_wrapper = OperationWrapper.new(instance)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get(name, &block)
|
|
42
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
43
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
44
|
+
|
|
45
|
+
@operations << Operation::Get.new(block, name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def let(name, &block)
|
|
49
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
50
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
51
|
+
|
|
52
|
+
@operations << Operation::Let.new(block, name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def and_then(&block)
|
|
56
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
57
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
58
|
+
|
|
59
|
+
@operations << Operation::AndThen.new(block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def observe(&block)
|
|
63
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
64
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
65
|
+
|
|
66
|
+
@operations << Operation::Observe.new(block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def and_yield(&block)
|
|
70
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
71
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
72
|
+
|
|
73
|
+
@operations << Operation::AndYield.new(block)
|
|
74
|
+
|
|
75
|
+
prepare_sequenced_operations
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def yield
|
|
79
|
+
raise InvalidSequenceError, 'and_yield not called' unless @sequenced_operations
|
|
80
|
+
|
|
81
|
+
@operation_wrapper.instance_eval(&@sequenced_operations)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
87
|
+
def prepare_sequenced_operations
|
|
88
|
+
operations = @operations
|
|
89
|
+
|
|
90
|
+
@sequenced_operations = ->(_) do
|
|
91
|
+
operations.reduce(Result::Success.new(nil)) do |last_result, operation|
|
|
92
|
+
last_result.map do
|
|
93
|
+
case operation
|
|
94
|
+
when Operation::Get
|
|
95
|
+
result = instance_eval(&operation.block)
|
|
96
|
+
result.map do |output|
|
|
97
|
+
# Runs in the context of the OperationWrapper, so the
|
|
98
|
+
# bound value is stored within the wrapper itself.
|
|
99
|
+
@gotten_results[operation.name] = output
|
|
100
|
+
result
|
|
101
|
+
end
|
|
102
|
+
when Operation::Let
|
|
103
|
+
@gotten_results[operation.name] = instance_eval(&operation.block)
|
|
104
|
+
last_result
|
|
105
|
+
when Operation::Observe
|
|
106
|
+
instance_eval(&operation.block)
|
|
107
|
+
last_result
|
|
108
|
+
when Operation::AndThen, Operation::AndYield
|
|
109
|
+
instance_eval(&operation.block)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# OperationWrapper proxies all method calls to the wrapped instance, but
|
|
119
|
+
# first checks if the name of the called method matches a value stored
|
|
120
|
+
# within @gotten_results and returns the value if it does.
|
|
121
|
+
class OperationWrapper < SimpleDelegator
|
|
122
|
+
def initialize(*args)
|
|
123
|
+
super
|
|
124
|
+
@gotten_results = {}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def method_missing(name, *args, **kwargs, &)
|
|
128
|
+
if @gotten_results.key?(name)
|
|
129
|
+
@gotten_results[name]
|
|
130
|
+
else
|
|
131
|
+
super
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def respond_to_missing?(name, include_private = false)
|
|
136
|
+
@gotten_results.key?(name) || super
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
module Prelude
|
|
142
|
+
include Sequencer
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -12,6 +12,7 @@ require 'functional-light-service/functional/enum'
|
|
|
12
12
|
require 'functional-light-service/functional/result'
|
|
13
13
|
require 'functional-light-service/functional/option'
|
|
14
14
|
require 'functional-light-service/functional/null'
|
|
15
|
+
require 'functional-light-service/functional/sequencer'
|
|
15
16
|
require 'functional-light-service/errors'
|
|
16
17
|
require 'functional-light-service/configuration'
|
|
17
18
|
require 'functional-light-service/localization_adapter'
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe FunctionalLightService::Sequencer do
|
|
4
|
+
include FunctionalLightService::Prelude::Result
|
|
5
|
+
|
|
6
|
+
let(:test_class) { Class.new { include FunctionalLightService::Sequencer } }
|
|
7
|
+
let(:test_instance) { test_class.new }
|
|
8
|
+
let(:arbitrary_success) { Success(double) }
|
|
9
|
+
|
|
10
|
+
# This mock method makes #arbitrary_success available within the operations
|
|
11
|
+
# in the test sequences
|
|
12
|
+
before { allow(test_instance).to receive(:arbitrary_success).and_return(arbitrary_success) }
|
|
13
|
+
|
|
14
|
+
it 'requires #and_yield to be specified' do
|
|
15
|
+
expect do
|
|
16
|
+
in_sequence do
|
|
17
|
+
get(:_) { arbitrary_success }
|
|
18
|
+
end
|
|
19
|
+
end.to raise_error(described_class::InvalidSequenceError, 'and_yield not called')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'does not allow calling #and_yield multiple times' do
|
|
23
|
+
expect do
|
|
24
|
+
in_sequence do
|
|
25
|
+
and_yield { arbitrary_success }
|
|
26
|
+
and_yield { arbitrary_success }
|
|
27
|
+
end
|
|
28
|
+
end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'does not allow calling #get after #and_yield' do
|
|
32
|
+
expect do
|
|
33
|
+
in_sequence do
|
|
34
|
+
and_yield { arbitrary_success }
|
|
35
|
+
get(:_) { arbitrary_success }
|
|
36
|
+
end
|
|
37
|
+
end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'does not allow calling #let after #and_yield' do
|
|
41
|
+
expect do
|
|
42
|
+
in_sequence do
|
|
43
|
+
and_yield { arbitrary_success }
|
|
44
|
+
let(:_) { arbitrary_success }
|
|
45
|
+
end
|
|
46
|
+
end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'does not allow calling #and_then after #and_yield' do
|
|
50
|
+
expect do
|
|
51
|
+
in_sequence do
|
|
52
|
+
and_yield { arbitrary_success }
|
|
53
|
+
and_then { arbitrary_success }
|
|
54
|
+
end
|
|
55
|
+
end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'does not allow calling #observe after #and_yield' do
|
|
59
|
+
expect do
|
|
60
|
+
in_sequence do
|
|
61
|
+
and_yield { arbitrary_success }
|
|
62
|
+
observe { arbitrary_success }
|
|
63
|
+
end
|
|
64
|
+
end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'requires a block for every operation' do
|
|
68
|
+
expect { in_sequence { get(:_) } }.to raise_error(ArgumentError, 'no block given')
|
|
69
|
+
expect { in_sequence { let(:_) } }.to raise_error(ArgumentError, 'no block given')
|
|
70
|
+
expect { in_sequence { and_then } }.to raise_error(ArgumentError, 'no block given')
|
|
71
|
+
expect { in_sequence { observe } }.to raise_error(ArgumentError, 'no block given')
|
|
72
|
+
expect { in_sequence { and_yield } }.to raise_error(ArgumentError, 'no block given')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'when #and_yield succeeds' do
|
|
76
|
+
let(:yielder_result) { Success('yield') }
|
|
77
|
+
before { allow(test_instance).to receive(:yielder).and_return(yielder_result) }
|
|
78
|
+
|
|
79
|
+
it "returns and_yield's result" do
|
|
80
|
+
result = in_sequence do
|
|
81
|
+
and_yield { yielder }
|
|
82
|
+
end
|
|
83
|
+
expect(result).to eq(yielder_result)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'ignores the return value of the #in_sequence block' do
|
|
87
|
+
result = in_sequence do
|
|
88
|
+
and_yield { yielder }
|
|
89
|
+
'a different return value'
|
|
90
|
+
end
|
|
91
|
+
expect(result).to eq(yielder_result)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'when #and_yield fails' do
|
|
96
|
+
let(:yielder_result) { Failure('yield') }
|
|
97
|
+
before { allow(test_instance).to receive(:yielder).and_return(yielder_result) }
|
|
98
|
+
|
|
99
|
+
it "returns and_yield's result" do
|
|
100
|
+
result = in_sequence do
|
|
101
|
+
and_yield { yielder }
|
|
102
|
+
end
|
|
103
|
+
expect(result).to eq(yielder_result)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
context 'when #get succeeds' do
|
|
108
|
+
let(:getter_result) { Success('get') }
|
|
109
|
+
before { allow(test_instance).to receive(:getter).and_return(getter_result) }
|
|
110
|
+
|
|
111
|
+
it 'its result is available in a subsequent #get' do
|
|
112
|
+
allow(test_instance).to receive(:second_getter).and_return(arbitrary_success)
|
|
113
|
+
|
|
114
|
+
in_sequence do
|
|
115
|
+
get(:get_result) { getter }
|
|
116
|
+
get(:_) { second_getter(get_result) }
|
|
117
|
+
and_yield { arbitrary_success }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
expect(test_instance).to have_received(:second_getter).with(getter_result.value)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'its result is not available in a previous #get' do
|
|
124
|
+
allow(test_instance).to receive(:second_getter)
|
|
125
|
+
|
|
126
|
+
expect do
|
|
127
|
+
in_sequence do
|
|
128
|
+
get(:_) { second_getter(get_result) }
|
|
129
|
+
get(:get_result) { getter }
|
|
130
|
+
and_yield { arbitrary_success }
|
|
131
|
+
end
|
|
132
|
+
end.to raise_error(NameError)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'its result is available in a subsequent #and_then' do
|
|
136
|
+
allow(test_instance).to receive(:and_then_function).and_return(arbitrary_success)
|
|
137
|
+
|
|
138
|
+
in_sequence do
|
|
139
|
+
get(:get_result) { getter }
|
|
140
|
+
and_then { and_then_function(get_result) }
|
|
141
|
+
and_yield { arbitrary_success }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
expect(test_instance).to have_received(:and_then_function).with(getter_result.value)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'its result is available in a subsequent #observe' do
|
|
148
|
+
allow(test_instance).to receive(:observer)
|
|
149
|
+
|
|
150
|
+
in_sequence do
|
|
151
|
+
get(:get_result) { getter }
|
|
152
|
+
observe { observer(get_result) }
|
|
153
|
+
and_yield { arbitrary_success }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
expect(test_instance).to have_received(:observer).with(getter_result.value)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'its result is available in #and_yield' do
|
|
160
|
+
allow(test_instance).to receive(:yielder).and_return(arbitrary_success)
|
|
161
|
+
|
|
162
|
+
in_sequence do
|
|
163
|
+
get(:get_result) { getter }
|
|
164
|
+
and_yield { yielder(get_result) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
expect(test_instance).to have_received(:yielder).with(getter_result.value)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context 'when multiple #gets succeed' do
|
|
172
|
+
let(:first_getter_result) { Success('get1') }
|
|
173
|
+
let(:second_getter_result) { Success('get2') }
|
|
174
|
+
|
|
175
|
+
before do
|
|
176
|
+
allow(test_instance).to receive(:first_getter).and_return(first_getter_result)
|
|
177
|
+
allow(test_instance).to receive(:second_getter).and_return(second_getter_result)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'both results are available in subsequent operations' do
|
|
181
|
+
allow(test_instance).to receive(:yielder).and_return(arbitrary_success)
|
|
182
|
+
|
|
183
|
+
in_sequence do
|
|
184
|
+
get(:first_get_result) { first_getter }
|
|
185
|
+
get(:second_get_result) { second_getter }
|
|
186
|
+
and_yield { yielder(first_get_result, second_get_result) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
expect(test_instance).to have_received(:yielder)
|
|
190
|
+
.with(first_getter_result.value, second_getter_result.value)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
context 'when #get fails' do
|
|
195
|
+
let(:getter_result) { Failure('get') }
|
|
196
|
+
before { allow(test_instance).to receive(:getter).and_return(getter_result) }
|
|
197
|
+
|
|
198
|
+
it 'does not invoke subsequent #gets' do
|
|
199
|
+
allow(test_instance).to receive(:second_getter).and_return(arbitrary_success)
|
|
200
|
+
|
|
201
|
+
in_sequence do
|
|
202
|
+
get(:get_result) { getter }
|
|
203
|
+
get(:_) { second_getter(get_result) }
|
|
204
|
+
and_yield { arbitrary_success }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
expect(test_instance).not_to have_received(:second_getter)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'does not invoke subsequent #and_thens' do
|
|
211
|
+
allow(test_instance).to receive(:and_then_function).and_return(arbitrary_success)
|
|
212
|
+
|
|
213
|
+
in_sequence do
|
|
214
|
+
get(:get_result) { getter }
|
|
215
|
+
and_then { and_then_function(get_result) }
|
|
216
|
+
and_yield { arbitrary_success }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
expect(test_instance).not_to have_received(:and_then_function)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'does not invoke subsequent #observes' do
|
|
223
|
+
allow(test_instance).to receive(:observer)
|
|
224
|
+
|
|
225
|
+
in_sequence do
|
|
226
|
+
get(:get_result) { getter }
|
|
227
|
+
observe { observer(get_result) }
|
|
228
|
+
and_yield { arbitrary_success }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
expect(test_instance).not_to have_received(:observer)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'does not invoke #and_yield' do
|
|
235
|
+
allow(test_instance).to receive(:yielder).and_return(arbitrary_success)
|
|
236
|
+
|
|
237
|
+
in_sequence do
|
|
238
|
+
get(:get_result) { getter }
|
|
239
|
+
and_yield { yielder }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
expect(test_instance).not_to have_received(:yielder)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'returns the failure' do
|
|
246
|
+
result = in_sequence do
|
|
247
|
+
get(:get_result) { getter }
|
|
248
|
+
and_yield { arbitrary_success }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
expect(result).to eq(getter_result)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
context 'when #let succeeds' do
|
|
256
|
+
let(:object) { { :a => 'a' } }
|
|
257
|
+
before { allow(test_instance).to receive(:object).and_return(object) }
|
|
258
|
+
|
|
259
|
+
it 'its result is available in a subsequent #let' do
|
|
260
|
+
allow(test_instance).to receive(:test_function)
|
|
261
|
+
|
|
262
|
+
in_sequence do
|
|
263
|
+
let(:a) { object.fetch(:a) }
|
|
264
|
+
let(:_) { test_function(a) }
|
|
265
|
+
and_yield { arbitrary_success }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
expect(test_instance).to have_received(:test_function).with(object.fetch(:a))
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'its result is available in a subsequent #and_then' do
|
|
272
|
+
allow(test_instance).to receive(:and_then_function).and_return(arbitrary_success)
|
|
273
|
+
|
|
274
|
+
in_sequence do
|
|
275
|
+
let(:a) { object.fetch(:a) }
|
|
276
|
+
and_then { and_then_function(a) }
|
|
277
|
+
and_yield { arbitrary_success }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
expect(test_instance).to have_received(:and_then_function).with(object.fetch(:a))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'its result is available in a subsequent #observe' do
|
|
284
|
+
allow(test_instance).to receive(:observer)
|
|
285
|
+
|
|
286
|
+
in_sequence do
|
|
287
|
+
let(:a) { object.fetch(:a) }
|
|
288
|
+
observe { observer(a) }
|
|
289
|
+
and_yield { arbitrary_success }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
expect(test_instance).to have_received(:observer).with(object.fetch(:a))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it 'its result is available in #and_yield' do
|
|
296
|
+
allow(test_instance).to receive(:yielder).and_return(arbitrary_success)
|
|
297
|
+
|
|
298
|
+
in_sequence do
|
|
299
|
+
let(:a) { object.fetch(:a) }
|
|
300
|
+
and_yield { yielder(a) }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
expect(test_instance).to have_received(:yielder).with(object.fetch(:a))
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
context 'when #let raises an error' do
|
|
308
|
+
let(:object) { { :a => 'a' } }
|
|
309
|
+
before { allow(test_instance).to receive(:object).and_return(object) }
|
|
310
|
+
|
|
311
|
+
it 'bubbles the raised error' do
|
|
312
|
+
expect do
|
|
313
|
+
in_sequence do
|
|
314
|
+
let(:b) { object.fetch(:b) }
|
|
315
|
+
and_yield { arbitrary_success }
|
|
316
|
+
end
|
|
317
|
+
end.to raise_error(KeyError)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'does not invoke #and_yield' do
|
|
321
|
+
allow(test_instance).to receive(:yielder).and_return(arbitrary_success)
|
|
322
|
+
|
|
323
|
+
begin
|
|
324
|
+
in_sequence do
|
|
325
|
+
let(:b) { object.fetch(:b) }
|
|
326
|
+
and_yield { yielder }
|
|
327
|
+
end
|
|
328
|
+
rescue KeyError
|
|
329
|
+
# Ignore
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
expect(test_instance).not_to have_received(:yielder)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
context 'when #and_then succeeds' do
|
|
337
|
+
let(:and_then_result) { Success('and_then') }
|
|
338
|
+
before { allow(test_instance).to receive(:and_then_function).and_return(and_then_result) }
|
|
339
|
+
|
|
340
|
+
it 'continues the sequence' do
|
|
341
|
+
allow(test_instance).to receive(:another_step).and_return(arbitrary_success)
|
|
342
|
+
|
|
343
|
+
in_sequence do
|
|
344
|
+
and_then { and_then_function }
|
|
345
|
+
and_then { another_step }
|
|
346
|
+
and_yield { arbitrary_success }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
expect(test_instance).to have_received(:another_step)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
context 'when #and_then fails' do
|
|
354
|
+
let(:and_then_result) { Failure('and_then') }
|
|
355
|
+
before { allow(test_instance).to receive(:and_then_function).and_return(and_then_result) }
|
|
356
|
+
|
|
357
|
+
it 'does not continue the sequence' do
|
|
358
|
+
allow(test_instance).to receive(:another_step).and_return(arbitrary_success)
|
|
359
|
+
|
|
360
|
+
in_sequence do
|
|
361
|
+
and_then { and_then_function }
|
|
362
|
+
and_then { another_step }
|
|
363
|
+
and_yield { arbitrary_success }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
expect(test_instance).not_to have_received(:another_step)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it 'returns the failure' do
|
|
370
|
+
result = in_sequence do
|
|
371
|
+
and_then { and_then_function }
|
|
372
|
+
and_yield { arbitrary_success }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
expect(result).to eq(and_then_result)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
context 'when #observe returns a failure' do
|
|
380
|
+
let(:observe_result) { Failure('observe') }
|
|
381
|
+
before { allow(test_instance).to receive(:observer).and_return(observe_result) }
|
|
382
|
+
|
|
383
|
+
it 'its return value is ignored and the sequence continues' do
|
|
384
|
+
allow(test_instance).to receive(:another_step).and_return(arbitrary_success)
|
|
385
|
+
|
|
386
|
+
in_sequence do
|
|
387
|
+
observe { observer }
|
|
388
|
+
and_then { another_step }
|
|
389
|
+
and_yield { arbitrary_success }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
expect(test_instance).to have_received(:another_step)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it 'does not allow calling methods outside of the wrapped instance' do
|
|
397
|
+
expect do
|
|
398
|
+
in_sequence do
|
|
399
|
+
and_yield { top_level_test_method }
|
|
400
|
+
end
|
|
401
|
+
end.to raise_error(NameError)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
context 'when including FunctionalLightService::Prelude' do
|
|
405
|
+
let(:test_class) { Class.new { include FunctionalLightService::Prelude } }
|
|
406
|
+
|
|
407
|
+
it '#in_sequence is available' do
|
|
408
|
+
expect do
|
|
409
|
+
in_sequence do
|
|
410
|
+
and_yield { arbitrary_success }
|
|
411
|
+
end
|
|
412
|
+
end.not_to raise_error
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
context 'readme example' do
|
|
417
|
+
let(:test_class) do
|
|
418
|
+
Class.new do
|
|
419
|
+
include FunctionalLightService::Prelude
|
|
420
|
+
|
|
421
|
+
# rubocop:disable Metrics/AbcSize
|
|
422
|
+
def call(input)
|
|
423
|
+
in_sequence do
|
|
424
|
+
get(:sanitized_input) { sanitize(input) }
|
|
425
|
+
and_then { validate(sanitized_input) }
|
|
426
|
+
get(:user) { get_user_from_db(sanitized_input) }
|
|
427
|
+
let(:name) { user.fetch(:name) }
|
|
428
|
+
observe { log('user name', name) }
|
|
429
|
+
get(:request) { build_request(sanitized_input, user) }
|
|
430
|
+
observe { log('sending request', request) }
|
|
431
|
+
get(:response) { send_request(request) }
|
|
432
|
+
observe { log('got response', response) }
|
|
433
|
+
and_yield { format_response(response) }
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
# rubocop:enable Metrics/AbcSize
|
|
437
|
+
|
|
438
|
+
def sanitize(input)
|
|
439
|
+
sanitized_input = input
|
|
440
|
+
Success(sanitized_input)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def validate(sanitized_input)
|
|
444
|
+
Success(sanitized_input)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def get_user_from_db(sanitized_input)
|
|
448
|
+
Success(:type => :admin, :id => sanitized_input.fetch(:id), :name => 'John')
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def build_request(sanitized_input, user)
|
|
452
|
+
Success(:input => sanitized_input, :user => user)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def log(message, data)
|
|
456
|
+
# logger.info(message, data)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def send_request(_request)
|
|
460
|
+
Success(:status => 200)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def format_response(response)
|
|
464
|
+
Success(:response => response, :message => 'it worked')
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it 'returns expected result' do
|
|
470
|
+
result = test_instance.call(:id => 1)
|
|
471
|
+
|
|
472
|
+
expect(result).to eq(Success(
|
|
473
|
+
:response => { :status => 200 },
|
|
474
|
+
:message => 'it worked'
|
|
475
|
+
))
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
it 'logs expected values' do
|
|
479
|
+
allow(test_instance).to receive(:log).and_call_original
|
|
480
|
+
|
|
481
|
+
test_instance.call(:id => 1)
|
|
482
|
+
|
|
483
|
+
expect(test_instance).to have_received(:log)
|
|
484
|
+
.with('user name', 'John')
|
|
485
|
+
.ordered
|
|
486
|
+
expect(test_instance).to have_received(:log)
|
|
487
|
+
.with('sending request',
|
|
488
|
+
:input => { :id => 1 },
|
|
489
|
+
:user => { :type => :admin, :id => 1, :name => 'John' })
|
|
490
|
+
.ordered
|
|
491
|
+
expect(test_instance).to have_received(:log)
|
|
492
|
+
.with('got response', :status => 200)
|
|
493
|
+
.ordered
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def in_sequence(&block)
|
|
498
|
+
test_instance.instance_eval do
|
|
499
|
+
in_sequence(&block)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def top_level_test_method
|
|
505
|
+
:empty
|
|
506
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: functional-light-service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Boscolo Michele
|
|
@@ -231,6 +231,7 @@ files:
|
|
|
231
231
|
- lib/functional-light-service/functional/null.rb
|
|
232
232
|
- lib/functional-light-service/functional/option.rb
|
|
233
233
|
- lib/functional-light-service/functional/result.rb
|
|
234
|
+
- lib/functional-light-service/functional/sequencer.rb
|
|
234
235
|
- lib/functional-light-service/localization_adapter.rb
|
|
235
236
|
- lib/functional-light-service/organizer.rb
|
|
236
237
|
- lib/functional-light-service/organizer/execute.rb
|
|
@@ -290,6 +291,7 @@ files:
|
|
|
290
291
|
- spec/lib/deterministic/result/result_shared.rb
|
|
291
292
|
- spec/lib/deterministic/result/success_spec.rb
|
|
292
293
|
- spec/lib/deterministic/result_spec.rb
|
|
294
|
+
- spec/lib/deterministic/sequencer_spec.rb
|
|
293
295
|
- spec/lib/edge_cases_spec.rb
|
|
294
296
|
- spec/lib/enum_spec.rb
|
|
295
297
|
- spec/lib/native_pattern_matching_spec.rb
|
|
@@ -332,7 +334,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
332
334
|
- !ruby/object:Gem::Version
|
|
333
335
|
version: '0'
|
|
334
336
|
requirements: []
|
|
335
|
-
rubygems_version:
|
|
337
|
+
rubygems_version: 4.0.3
|
|
336
338
|
specification_version: 4
|
|
337
339
|
summary: A service skeleton with an emphasis on simplicity with a pinch a functional
|
|
338
340
|
programming
|