functional-light-service 0.4.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 +10 -2
- data/Appraisals +4 -0
- data/CHANGELOG.md +80 -0
- data/Gemfile +0 -2
- data/README.md +1492 -1426
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/functional-light-service.gemspec +5 -0
- data/lib/functional-light-service/context.rb +1 -1
- 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 +3 -0
- data/lib/functional-light-service/organizer.rb +10 -0
- data/lib/functional-light-service/version.rb +1 -1
- 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 +6 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
- data/spec/action_spec.rb +8 -0
- data/spec/lib/deterministic/option_spec.rb +18 -14
- data/spec/organizer_spec.rb +21 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
- data/spec/spec_helper.rb +15 -13
- data/spec/test_doubles.rb +35 -0
- metadata +55 -7
- data/.travis.yml +0 -24
data/README.md
CHANGED
|
@@ -1,1426 +1,1492 @@
|
|
|
1
|
-
# FunctionalLightService
|
|
2
|
-
|
|
3
|
-
[](https://rubygems.org/gems/functional-light-service)
|
|
4
|
+
[](https://github.com/sphynx79/functional-light-service/actions/workflows/project-build.yml)
|
|
5
|
+
[](https://app.codecov.io/gh/sphynx79/functional-light-service)
|
|
6
|
+
[](http://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://rubygems.org/gems/functional-light-service)
|
|
8
|
+
|
|
9
|
+
## Table of Content
|
|
10
|
+
|
|
11
|
+
* [Requirements](#requirements)
|
|
12
|
+
* [Installation](#installation)
|
|
13
|
+
* [Why FunctionalLightService?](#why-functionallightservice?)
|
|
14
|
+
* [Stopping the Series of Actions](#stopping-the-series-of-actions)
|
|
15
|
+
* [Failing the Context](#failing-the-context)
|
|
16
|
+
* [Skipping the Rest of the Actions](#skipping-the-rest-of-the-actions)
|
|
17
|
+
* [Benchmarking Actions with Around Advice](#benchmarking-actions-with-around-advice)
|
|
18
|
+
* [Before and After Action Hooks](#before-and-after-action-hooks)
|
|
19
|
+
* [Key Aliases](#key-aliases)
|
|
20
|
+
* [Logging](#logging)
|
|
21
|
+
* [Error Codes](#error-codes)
|
|
22
|
+
* [Action Rollback](#action-rollback)
|
|
23
|
+
* [Localizing Messages](#localizing-messages)
|
|
24
|
+
* [Logic in Organizers](#logic-in-organizers)
|
|
25
|
+
* [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
|
|
26
|
+
* [Functional programming](#functional-programming)
|
|
27
|
+
* [Pattern](#pattern)
|
|
28
|
+
* [Usage](#functional-usage)
|
|
29
|
+
* [Result: Success & Failure](#functional-usage-success-failure)
|
|
30
|
+
* [Result Chaining](#functional-usage-chaining)
|
|
31
|
+
* [Complex Example in a Builder Action](#functional-usage-complex-action)
|
|
32
|
+
* [Pattern matching](#functional-usage-pattern-matching)
|
|
33
|
+
* [Option](#functional-usage-option)
|
|
34
|
+
* [Coercion](#functional-usage-coercion)
|
|
35
|
+
* [Enum](#functional-usage-enum)
|
|
36
|
+
* [Maybe](#functional-usage-maybe)
|
|
37
|
+
* [Usage](#usage)
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
This gem requires ruby >= 2.5.0
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Add this line to your application's Gemfile:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gem 'functional-light-service'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
And then execute:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
$ bundle
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install it yourself as:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ gem install functional-light-service
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Why FunctionalLightService?
|
|
64
|
+
|
|
65
|
+
While studying functional programming in Ruby, I discovered the fantastic gem **Deterministic**, which made it much easier to write Ruby code in a functional style.
|
|
66
|
+
By leveraging its `in_sequence` method, I can chain a series of actions:
|
|
67
|
+
|
|
68
|
+
- If every step completes without raising an exception, the call returns a `Success()` monad.
|
|
69
|
+
- If any step fails, the remaining actions are skipped and a `Failure()` monad is returned.
|
|
70
|
+
|
|
71
|
+
I writing this code:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class Foo
|
|
75
|
+
include Deterministic::Prelude
|
|
76
|
+
|
|
77
|
+
def call(input)
|
|
78
|
+
result = in_sequence do
|
|
79
|
+
get(:sanitized_input) { sanitize(input) }
|
|
80
|
+
and_then { validate(sanitized_input) }
|
|
81
|
+
and_then { connect_db }
|
|
82
|
+
get(:user) { get_user(sanitized_input) }
|
|
83
|
+
and_yield { print_response(user) }
|
|
84
|
+
end
|
|
85
|
+
logger.warn(result.value) if result.failure?
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
logger.fatal(e.message)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def sanitize(input)
|
|
91
|
+
sanitized_input = {}
|
|
92
|
+
sanitized_input[:name] = input[:name].downcase
|
|
93
|
+
sanitized_input[:password] = input[:password].downcase
|
|
94
|
+
Success(sanitized_input)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate(sanitized_input)
|
|
98
|
+
try! do
|
|
99
|
+
raise "Not allow empty name" if sanitized_input[:name].empty?
|
|
100
|
+
raise "Not allow empty password" if sanitized_input[:password].empty?
|
|
101
|
+
end.map_err { |n| Failure(n.message) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def connect_db
|
|
105
|
+
try! do
|
|
106
|
+
raise "Error connection to db" if rand(0..1) == 1
|
|
107
|
+
end.map_err { |n| Failure(n.message) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_user(sanitized_input)
|
|
111
|
+
user = FAKEDB.find do |_k, v|
|
|
112
|
+
sanitized_input[:name] == v[:name] && sanitized_input[:password] == v[:password]
|
|
113
|
+
end
|
|
114
|
+
user.nil? ? Failure("Name or password error") : Success(user)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def print_response(user)
|
|
118
|
+
Success(logger.info("Login successful id: #{user[0]} name: #{user[1][:name]}"))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Foo.new.call(:name => "foo", :password => "bar")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
While refactoring my codebase, I needed each action to live in a well‑defined context.
|
|
126
|
+
That’s when I discovered the excellent gem **LightService**. It gives me exactly what I was looking for:
|
|
127
|
+
|
|
128
|
+
- a clean separation between business concerns and orchestration logic
|
|
129
|
+
- a simple way to arrange actions in a pipeline
|
|
130
|
+
- the freedom to place every action in its own class, each with its own contextual data
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
class Foo
|
|
134
|
+
extend LightService::Organizer
|
|
135
|
+
|
|
136
|
+
def self.call(name: "", password: "")
|
|
137
|
+
result = with(:name => name, :password => password).reduce(actions)
|
|
138
|
+
logger.warn(result.message) if result.failure?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.actions
|
|
142
|
+
[
|
|
143
|
+
Sanitize,
|
|
144
|
+
Validate,
|
|
145
|
+
ConnectDb,
|
|
146
|
+
GetUser,
|
|
147
|
+
PrintResponse
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class Sanitize
|
|
153
|
+
extend LightService::Action
|
|
154
|
+
expects :name, :password
|
|
155
|
+
promises :sanitized_input
|
|
156
|
+
|
|
157
|
+
executed do |ctx|
|
|
158
|
+
sanitized_input = {}
|
|
159
|
+
sanitized_input[:name] = ctx.name.downcase
|
|
160
|
+
sanitized_input[:password] = ctx.password.downcase
|
|
161
|
+
ctx.sanitized_input = sanitized_input
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class Validate
|
|
166
|
+
extend LightService::Action
|
|
167
|
+
expects :sanitized_input
|
|
168
|
+
|
|
169
|
+
executed do |ctx|
|
|
170
|
+
ctx.fail_and_return!("Not allow empty name") if ctx.sanitized_input[:name].empty?
|
|
171
|
+
ctx.fail_and_return!("Not allow empty password") if ctx.sanitized_input[:password].empty?
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class ConnectDb
|
|
176
|
+
extend LightService::Action
|
|
177
|
+
|
|
178
|
+
executed do |ctx|
|
|
179
|
+
raise "Error connection to db"
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
ctx.fail!(e.message) if rand(0..1) == 1
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# private_class_method :..
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class GetUser
|
|
188
|
+
extend LightService::Action
|
|
189
|
+
expects :sanitized_input
|
|
190
|
+
promises :user
|
|
191
|
+
|
|
192
|
+
executed do |ctx|
|
|
193
|
+
user = FAKEDB.find do |_k, v|
|
|
194
|
+
ctx.sanitized_input[:name] == v[:name] && ctx.sanitized_input[:password] == v[:password]
|
|
195
|
+
end
|
|
196
|
+
ctx.fail_and_return!("Name or password error") if user.nil?
|
|
197
|
+
ctx.user = user
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
class PrintResponse
|
|
202
|
+
extend LightService::Action
|
|
203
|
+
expects :user
|
|
204
|
+
|
|
205
|
+
executed do |ctx|
|
|
206
|
+
logger.info("Login successful id: #{ctx.user[0]} name: #{ctx.user[1][:name]}")
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
Foo.call(:name => "foo", :password => "bar")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The switch to **LightService** came at a price: I missed the functional‑programming super‑powers that **Deterministic** had given me.
|
|
214
|
+
So I asked myself, *why not enjoy the best of both worlds?*
|
|
215
|
+
That question led me to create **this gem**. Now I can keep all the conveniences LightService offers—action pipelines, clear contexts—while still coding in a fully functional style with expressive monads.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
class Foo
|
|
219
|
+
extend FunctionalLightService::Organizer
|
|
220
|
+
|
|
221
|
+
def self.call(name: "", password: "")
|
|
222
|
+
result = with(:name => name, :password => password).reduce(actions)
|
|
223
|
+
logger.warn(result.message) if result.failure?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def self.actions
|
|
227
|
+
[
|
|
228
|
+
Sanitize,
|
|
229
|
+
Validate,
|
|
230
|
+
ConnectDb,
|
|
231
|
+
GetUser,
|
|
232
|
+
PrintResponse
|
|
233
|
+
]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class Sanitize
|
|
238
|
+
extend FunctionalLightService::Action
|
|
239
|
+
expects :name, :password
|
|
240
|
+
promises :sanitized_input
|
|
241
|
+
|
|
242
|
+
executed do |ctx|
|
|
243
|
+
name = ctx.name
|
|
244
|
+
password = ctx.password
|
|
245
|
+
ctx.sanitized_input = downcase(name, password).value
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def self.downcase(name, password)
|
|
249
|
+
ctx.try! do
|
|
250
|
+
{
|
|
251
|
+
:name => name.downcase,
|
|
252
|
+
:password => password.downcase
|
|
253
|
+
}
|
|
254
|
+
end.map_err { ctx.fail!("Error nel method downcase") }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private_class_method :downcase
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
class Validate
|
|
261
|
+
extend FunctionalLightService::Action
|
|
262
|
+
expects :sanitized_input
|
|
263
|
+
|
|
264
|
+
executed do |ctx|
|
|
265
|
+
validate_params(ctx.sanitized_input).match do
|
|
266
|
+
None() { ctx.Success(0) }
|
|
267
|
+
Some() { |errors| ctx.fail_and_return!(errors) }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def self.validate_params(params)
|
|
272
|
+
return ctx.Some("Not allow empty name") if ctx.Option.any?(params[:name]).none?
|
|
273
|
+
return ctx.Some("Not allow empty password") if ctx.Option.any?(params[:password]).none?
|
|
274
|
+
|
|
275
|
+
ctx.None
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private_class_method :validate_params
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
class ConnectDb
|
|
282
|
+
extend FunctionalLightService::Action
|
|
283
|
+
|
|
284
|
+
executed do |ctx|
|
|
285
|
+
ctx.try! do
|
|
286
|
+
raise "Error connection to db" if rand(0..1) == 1
|
|
287
|
+
end.map_err { |n| ctx.fail!(n.message) }
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
class GetUser
|
|
292
|
+
extend FunctionalLightService::Action
|
|
293
|
+
expects :sanitized_input
|
|
294
|
+
promises :user
|
|
295
|
+
|
|
296
|
+
executed do |ctx|
|
|
297
|
+
user = Success(ctx.sanitized_input[:name]) >> method(:fetch_name) >> method(:check_password)
|
|
298
|
+
ctx.user = user.value
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def self.fetch_name(name)
|
|
302
|
+
records = FAKEDB.select { |_k, v| name == v[:name] }
|
|
303
|
+
ctx.fail_and_return!("Name not found in DB") if records.empty?
|
|
304
|
+
|
|
305
|
+
Success(records)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.check_password(records)
|
|
309
|
+
record = records.select { |_k, v| ctx.sanitized_input[:password] == v[:password] }
|
|
310
|
+
return ctx.fail_and_return!("Password is not correct") if record.empty?
|
|
311
|
+
|
|
312
|
+
Success(record)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private_class_method :fetch_name, :check_password
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
class PrintResponse
|
|
319
|
+
extend FunctionalLightService::Action
|
|
320
|
+
expects :user
|
|
321
|
+
|
|
322
|
+
executed do |ctx|
|
|
323
|
+
id = ctx.user.keys[0]
|
|
324
|
+
name = ctx.user.values[0][:name]
|
|
325
|
+
logger.info("Login successful id: #{id} name: #{name}")
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
Foo.call(:name => "foo", :password => "bar")
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Stopping the Series of Actions
|
|
333
|
+
|
|
334
|
+
When everything goes smoothly, the organizer returns a **successful** context.
|
|
335
|
+
You can check it like this:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
class SomeController < ApplicationController
|
|
339
|
+
def index
|
|
340
|
+
result_context = SomeOrganizer.call(current_user.id)
|
|
341
|
+
|
|
342
|
+
if result_context.success?
|
|
343
|
+
redirect_to foo_path, :notice => "Everything went OK! Thanks!"
|
|
344
|
+
else
|
|
345
|
+
flash[:error] = result_context.message
|
|
346
|
+
render :action => "new"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Sometimes, though, things don’t go as planned — an external API is down or a business rule fails.
|
|
353
|
+
In those cases, you can short‑circuit the pipeline in two ways:
|
|
354
|
+
|
|
355
|
+
1. **Fail the context** – aborts execution and returns a `Failure()` monad with an error message.
|
|
356
|
+
2. **Skip the remaining actions** – stops further actions but keeps the context successful, allowing graceful exits without raising an error.
|
|
357
|
+
|
|
358
|
+
### Failing the Context
|
|
359
|
+
|
|
360
|
+
When an action hits an unrecoverable error, call `context.fail!` to mark the context as failed (`context.failure? #=> true`) and abort the pipeline.
|
|
361
|
+
You can pass an optional message to describe what went wrong:
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
context.fail!("Validation failed")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
If you also need to leave the executed block immediately, you have two options:
|
|
368
|
+
|
|
369
|
+
- next context – after fail!, simply return the context.
|
|
370
|
+
- context.fail_and_return!(msg) – a one‑liner that sets the failure state and exits the block.
|
|
371
|
+
|
|
372
|
+
Here is an example:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
class SubmitsOrderAction
|
|
376
|
+
extend FunctionalLightService::Action
|
|
377
|
+
expects :order, :mailer
|
|
378
|
+
|
|
379
|
+
executed do |context|
|
|
380
|
+
unless context.order.submit_order_successful?
|
|
381
|
+
context.fail_and_return!("Failed to submit the order")
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# This won't be executed
|
|
385
|
+
context.mailer.send_order_notification!
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+

|
|
391
|
+
|
|
392
|
+
In the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd had a failure, that pushed the context into a failure state and the 4th action was skipped.
|
|
393
|
+
|
|
394
|
+
### Skipping the rest of the actions
|
|
395
|
+
|
|
396
|
+
To short‑circuit the pipeline without marking the context as failed, call
|
|
397
|
+
`context.skip_remaining!`. It behaves like `fail!`, but the context
|
|
398
|
+
remains **successful**, so downstream code can still treat the result as OK.
|
|
399
|
+
|
|
400
|
+
Typical use case: you run the first few actions, perform a check, and if everything
|
|
401
|
+
is already fine you can avoid processing the rest.
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
class ChecksOrderStatusAction
|
|
405
|
+
extend FunctionalLightService::Action
|
|
406
|
+
expects :order
|
|
407
|
+
|
|
408
|
+
executed do |context|
|
|
409
|
+
if context.order.send_notification?
|
|
410
|
+
context.skip_remaining!("Everything is good, no need to execute the rest of the actions")
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+

|
|
417
|
+
|
|
418
|
+
In the example above, the organizer invokes four actions.
|
|
419
|
+
The first two run successfully; the third calls skip_remaining!, so the fourth is never executed, yet the overall context stays successful.
|
|
420
|
+
|
|
421
|
+
## Benchmarking Actions with Around Advice
|
|
422
|
+
|
|
423
|
+
When you need to profile a pipeline, adding timing code inside every single
|
|
424
|
+
action clutters your business logic.
|
|
425
|
+
Instead, use the organizer’s `around_each` hook, which wraps each action call
|
|
426
|
+
as it is reduced in order.
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
class LogDuration
|
|
430
|
+
def self.call(context)
|
|
431
|
+
start_time = Time.now
|
|
432
|
+
result = yield # run the wrapped action
|
|
433
|
+
duration = Time.now - start_time
|
|
434
|
+
FunctionalLightService::Configuration.logger.info(
|
|
435
|
+
:action => context.current_action,
|
|
436
|
+
:duration => duration
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
result
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
class CalculatesTax
|
|
444
|
+
extend FunctionalLightService::Organizer
|
|
445
|
+
|
|
446
|
+
def self.call(order)
|
|
447
|
+
with(:order => order).around_each(LogDuration).reduce(
|
|
448
|
+
LooksUpTaxPercentageAction,
|
|
449
|
+
CalculatesOrderTaxAction,
|
|
450
|
+
ProvidesFreeShippingAction
|
|
451
|
+
)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Any object you pass to around_each must implement:
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
def self.call(context, &block)
|
|
460
|
+
# …before logic…
|
|
461
|
+
result = yield # executes the action
|
|
462
|
+
# …after logic…
|
|
463
|
+
result
|
|
464
|
+
end
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
This design lets you measure—or audit—every action without polluting
|
|
468
|
+
the actions themselves.
|
|
469
|
+
|
|
470
|
+
## Before and After Action Hooks
|
|
471
|
+
|
|
472
|
+
Sometimes you need to run code **right before** or **right after** each action.
|
|
473
|
+
FunctionalLightService lets you do that with the `before_actions` and `after_actions` hooks.
|
|
474
|
+
Each hook accepts one (or many) lambdas that will be invoked by the organizer, keeping
|
|
475
|
+
instrumentation neatly separated from business logic.
|
|
476
|
+
|
|
477
|
+
### Example without hooks
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
class SomeOrganizer
|
|
481
|
+
extend FunctionalLightService::Organizer
|
|
482
|
+
|
|
483
|
+
def self.call(ctx)
|
|
484
|
+
with(ctx).reduce(actions)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def self.actions
|
|
488
|
+
[
|
|
489
|
+
OneAction,
|
|
490
|
+
TwoAction,
|
|
491
|
+
ThreeAction
|
|
492
|
+
]
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
class TwoAction
|
|
497
|
+
extend FunctionalLightService::Action
|
|
498
|
+
expects :user, :logger
|
|
499
|
+
|
|
500
|
+
executed do |ctx|
|
|
501
|
+
# Logging information
|
|
502
|
+
if ctx.user.role == 'admin'
|
|
503
|
+
ctx.logger.info('admin is doing something')
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
ctx.user.do_something
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Logging overwhelms the real work in TwoAction.
|
|
512
|
+
Let’s move that concern into hooks.
|
|
513
|
+
|
|
514
|
+
### Option 1 — declare hooks inside the organizer
|
|
515
|
+
|
|
516
|
+
```ruby
|
|
517
|
+
class SomeOrganizer
|
|
518
|
+
extend FunctionalLightService::Organizer
|
|
519
|
+
before_actions (lambda do |ctx|
|
|
520
|
+
if ctx.current_action == TwoAction
|
|
521
|
+
return unless ctx.user.role == 'admin'
|
|
522
|
+
ctx.logger.info('admin is doing something')
|
|
523
|
+
end
|
|
524
|
+
end)
|
|
525
|
+
after_actions (lambda do |ctx|
|
|
526
|
+
if ctx.current_action == TwoAction
|
|
527
|
+
return unless ctx.user.role == 'admin'
|
|
528
|
+
ctx.logger.info('admin is DONE doing something')
|
|
529
|
+
end
|
|
530
|
+
end)
|
|
531
|
+
|
|
532
|
+
def self.call(ctx)
|
|
533
|
+
with(ctx).reduce(actions)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def self.actions
|
|
537
|
+
[
|
|
538
|
+
OneAction,
|
|
539
|
+
TwoAction,
|
|
540
|
+
ThreeAction
|
|
541
|
+
]
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
class TwoAction
|
|
546
|
+
extend FunctionalLightService::Action
|
|
547
|
+
expects :user
|
|
548
|
+
|
|
549
|
+
executed do |ctx|
|
|
550
|
+
ctx.user.do_something
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Now TwoAction is pure business logic.
|
|
556
|
+
Because ctx.current_action holds the class of the action being run, the hooks fire
|
|
557
|
+
only for TwoAction, not OneAction or ThreeAction.
|
|
558
|
+
|
|
559
|
+
### Option 2 — attach hooks from the outside
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
SomeOrganizer.before_actions =
|
|
563
|
+
lambda do |ctx|
|
|
564
|
+
if ctx.current_action == TwoAction
|
|
565
|
+
return unless ctx.user.role == 'admin'
|
|
566
|
+
ctx.logger.info('admin is doing something')
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
These ideas are originally from Aspect Oriented Programming, read more about them [here](https://en.wikipedia.org/wiki/Aspect-oriented_programming).
|
|
572
|
+
|
|
573
|
+
## Expects and Promises
|
|
574
|
+
|
|
575
|
+
Two handy macros define the contract of every action:
|
|
576
|
+
|
|
577
|
+
| Macro | Purpose |
|
|
578
|
+
| ---------- | --------------------------------------------------------------- |
|
|
579
|
+
| `expects` | Declares which keys **must** be present before the action runs. |
|
|
580
|
+
| `promises` | Declares which keys **must** exist after the action finishes. |
|
|
581
|
+
|
|
582
|
+
If either rule is violated, FunctionalLightService raises a dedicated exception.
|
|
583
|
+
|
|
584
|
+
### Basic usage
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
class FooAction
|
|
588
|
+
extend FunctionalLightService::Action
|
|
589
|
+
|
|
590
|
+
expects :baz
|
|
591
|
+
promises :bar
|
|
592
|
+
|
|
593
|
+
executed do |context|
|
|
594
|
+
baz = context.fetch(:baz) # guaranteed to be present
|
|
595
|
+
context[:bar] = baz + 2 # fulfils the promise
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Built‑in readers and writers
|
|
601
|
+
|
|
602
|
+
The macros do more than validation:
|
|
603
|
+
expects adds an accessor reader, so you can reference keys directly.
|
|
604
|
+
promises adds an accessor writer, so you can assign without touching the hash.
|
|
605
|
+
Refactored, the action is cleaner:
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
class FooAction
|
|
609
|
+
extend FunctionalLightService::Action
|
|
610
|
+
|
|
611
|
+
expects :baz
|
|
612
|
+
promises :bar
|
|
613
|
+
|
|
614
|
+
executed do |context|
|
|
615
|
+
context.bar = context.baz + 2
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Want to see it in practice? Check out [this spec](spec/action_expects_and_promises_spec.rb) test file.
|
|
621
|
+
|
|
622
|
+
## Key Aliases
|
|
623
|
+
|
|
624
|
+
Need to wire together actions that use different key names?
|
|
625
|
+
Declare key mappings once in the organizer with the `aliases` macro and every
|
|
626
|
+
action can read or write the value under its preferred name.
|
|
627
|
+
|
|
628
|
+
```ruby
|
|
629
|
+
class AnOrganizer
|
|
630
|
+
extend FunctionalLightService::Organizer
|
|
631
|
+
|
|
632
|
+
aliases :my_key => :key_alias
|
|
633
|
+
|
|
634
|
+
def self.call(order)
|
|
635
|
+
with(:order => order).reduce(
|
|
636
|
+
AnAction,
|
|
637
|
+
AnotherAction,
|
|
638
|
+
)
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
class AnAction
|
|
643
|
+
extend FunctionalLightService::Action
|
|
644
|
+
promises :my_key
|
|
645
|
+
|
|
646
|
+
executed do |context|
|
|
647
|
+
context.my_key = "value"
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
class AnotherAction
|
|
652
|
+
extend FunctionalLightService::Action
|
|
653
|
+
expects :key_alias
|
|
654
|
+
|
|
655
|
+
executed do |context|
|
|
656
|
+
context.key_alias # => "value"
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Logging
|
|
662
|
+
|
|
663
|
+
Turning on logging is the easiest way to see what happens inside a pipeline:
|
|
664
|
+
which organizer is called, which actions run, which keys appear in the context, and when something goes wrong.
|
|
665
|
+
|
|
666
|
+
Logging is **disabled by default**. Enable it in your app’s configuration:
|
|
667
|
+
|
|
668
|
+
```ruby
|
|
669
|
+
FunctionalLightService::Configuration.logger = Logger.new(STDOUT)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
To silence it, point the logger at nil or /dev/null:
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
FunctionalLightService::Configuration.logger = Logger.new('/dev/null')
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Run an organizer and you’ll see output like:
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
I, [DATE] INFO -- : [FunctionalLightService] - calling organizer <TestDoubles::MakesTeaAndCappuccino>
|
|
682
|
+
I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :tea, :milk, :coffee
|
|
683
|
+
I, [DATE] INFO -- : [FunctionalLightService] - executing <TestDoubles::MakesTeaWithMilkAction>
|
|
684
|
+
I, [DATE] INFO -- : [FunctionalLightService] - expects: :tea, :milk
|
|
685
|
+
I, [DATE] INFO -- : [FunctionalLightService] - promises: :milk_tea
|
|
686
|
+
I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :tea, :milk, :coffee, :milk_tea
|
|
687
|
+
I, [DATE] INFO -- : [FunctionalLightService] - executing <TestDoubles::MakesLatteAction>
|
|
688
|
+
I, [DATE] INFO -- : [FunctionalLightService] - expects: :coffee, :milk
|
|
689
|
+
I, [DATE] INFO -- : [FunctionalLightService] - promises: :latte
|
|
690
|
+
I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :tea, :milk, :coffee, :milk_tea, :latte
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
The log provides a blueprint of the series of actions. You can see what organizer is invoked, what actions
|
|
694
|
+
are called in what order, what do the expect and promise and most importantly what keys you have in the context
|
|
695
|
+
after each action is executed.
|
|
696
|
+
|
|
697
|
+
Failures are logged at WARN level:
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
W, [DATE] WARN -- : [FunctionalLightService] - :-((( <TestDoubles::MakesLatteAction> has failed...
|
|
701
|
+
W, [DATE] WARN -- : [FunctionalLightService] - context message: Can't make a latte from a milk that's too hot!
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Skipping the remaining actions is also reported:
|
|
705
|
+
|
|
706
|
+
```bash
|
|
707
|
+
I, [DATE] INFO -- : [FunctionalLightService] - calling organizer <TestDoubles::MakesCappuccinoSkipsAddsTwo>
|
|
708
|
+
I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :milk, :coffee
|
|
709
|
+
I, [DATE] INFO -- : [FunctionalLightService] - ;-) <TestDoubles::MakesLatteAction> has decided to skip the rest of the actions
|
|
710
|
+
I, [DATE] INFO -- : [FunctionalLightService] - context message: Can't make a latte with a fatty milk like that!
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Need different log destinations per organizer? Override the global logger:
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
class FooOrganizer
|
|
717
|
+
extend FunctionalLightService::Organizer
|
|
718
|
+
log_with Logger.new("/my/special.log")
|
|
719
|
+
end
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
## Error Codes
|
|
723
|
+
|
|
724
|
+
Sometimes you need more structure than a free‑text error message.
|
|
725
|
+
fail! and fail_and_return! accept an error_code: keyword so you can branch on well‑defined codes later.
|
|
726
|
+
|
|
727
|
+
```ruby
|
|
728
|
+
class FooAction
|
|
729
|
+
extend FunctionalLightService::Action
|
|
730
|
+
|
|
731
|
+
executed do |context|
|
|
732
|
+
result = external_service.call
|
|
733
|
+
|
|
734
|
+
unless result.success?
|
|
735
|
+
context.fail!(
|
|
736
|
+
"Service call failed",
|
|
737
|
+
error_code: 1001
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
unless entity.save
|
|
742
|
+
context.fail!(
|
|
743
|
+
"Saving the entity failed",
|
|
744
|
+
error_code: 2001
|
|
745
|
+
)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Organizers or downstream actions can then react to specific codes:
|
|
752
|
+
|
|
753
|
+
```ruby
|
|
754
|
+
result = FooOrganizer.call
|
|
755
|
+
|
|
756
|
+
case result.error_code
|
|
757
|
+
when 1001 then retry_later
|
|
758
|
+
when 2001 then alert_ops_team
|
|
759
|
+
end
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Action Rollback
|
|
763
|
+
|
|
764
|
+
Sometimes an action must **undo** its work if a later step fails.
|
|
765
|
+
Example: one action saves records to the database, the next calls an external
|
|
766
|
+
API. If the API call blows up, you want to delete the records you just saved.
|
|
767
|
+
That’s exactly what the `rolled_back` macro is for.
|
|
768
|
+
|
|
769
|
+
```ruby
|
|
770
|
+
class SaveEntities
|
|
771
|
+
extend FunctionalLightService::Action
|
|
772
|
+
expects :user
|
|
773
|
+
|
|
774
|
+
executed do |context|
|
|
775
|
+
context.user.save!
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
rolled_back do |context|
|
|
779
|
+
context.user.destroy
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Trigger a rollback by calling context.fail_with_rollback!.
|
|
785
|
+
Rollback begins with the failing action and walks back through the already
|
|
786
|
+
executed actions in reverse order.
|
|
787
|
+
|
|
788
|
+
```ruby
|
|
789
|
+
class CallExternalApi
|
|
790
|
+
extend FunctionalLightService::Action
|
|
791
|
+
|
|
792
|
+
executed do |context|
|
|
793
|
+
api_call_result = SomeAPI.save_user(context.user)
|
|
794
|
+
|
|
795
|
+
context.fail_with_rollback!("Error when calling external API") if api_call_result.failure?
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Declaring rolled_back is optional. If an action makes no persistent changes,
|
|
801
|
+
there’s nothing to undo—skip it.
|
|
802
|
+
|
|
803
|
+
### Using rollbackable actions standalone
|
|
804
|
+
|
|
805
|
+
When an action is executed outside an organizer via .execute, any
|
|
806
|
+
fail_with_rollback! will raise a FailWithRollbackError (an organizer needs
|
|
807
|
+
the exception to traverse the chain).
|
|
808
|
+
|
|
809
|
+
If you don’t want to wrap the call in begin … rescue, check whether the
|
|
810
|
+
action is running inside an organizer:
|
|
811
|
+
|
|
812
|
+
```ruby
|
|
813
|
+
class FooAction
|
|
814
|
+
extend LightService::Action
|
|
815
|
+
|
|
816
|
+
executed do |context|
|
|
817
|
+
# context.organized_by will be nil if run from an action,
|
|
818
|
+
# or will be the class name if run from an organizer
|
|
819
|
+
if context.organized_by.nil?
|
|
820
|
+
context.fail!
|
|
821
|
+
else
|
|
822
|
+
context.fail_with_rollback!
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
For a full example, see [this acceptance test](spec/acceptance/rollback_spec.rb)
|
|
829
|
+
|
|
830
|
+
## Localizing Messages
|
|
831
|
+
|
|
832
|
+
FunctionalLightService integrates with **I18n** out of the box, so you can translate
|
|
833
|
+
success or failure messages without extra plumbing.
|
|
834
|
+
If your app needs something more advanced, you can swap in a custom localization
|
|
835
|
+
adapter.
|
|
836
|
+
|
|
837
|
+
```ruby
|
|
838
|
+
class FooAction
|
|
839
|
+
extend FunctionalLightService::Action
|
|
840
|
+
|
|
841
|
+
executed do |context|
|
|
842
|
+
unless service_call.success?
|
|
843
|
+
context.fail!(:exceeded_api_limit)
|
|
844
|
+
|
|
845
|
+
# The failure message used here equates to:
|
|
846
|
+
# I18n.t(:exceeded_api_limit, scope: "foo_action.light_service.failures")
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Nested classes
|
|
853
|
+
|
|
854
|
+
Look‑ups follow ActiveSupport’s underscore, just like Rails models inside modules:
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
module PaymentGateway
|
|
858
|
+
class CaptureFunds
|
|
859
|
+
extend FunctionalLightService::Action
|
|
860
|
+
|
|
861
|
+
executed do |context|
|
|
862
|
+
context.fail!(:funds_not_available) if api_service.failed?
|
|
863
|
+
# resolves to:
|
|
864
|
+
# I18n.t(:funds_not_available,
|
|
865
|
+
# scope: "payment_gateway/capture_funds.light_service.failures")
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### Interpolation variables
|
|
872
|
+
|
|
873
|
+
Pass a hash for dynamic values:
|
|
874
|
+
|
|
875
|
+
```ruby
|
|
876
|
+
module PaymentGateway
|
|
877
|
+
class CaptureFunds
|
|
878
|
+
extend FunctionalLightService::Action
|
|
879
|
+
|
|
880
|
+
executed do |context|
|
|
881
|
+
if api_service.failed?
|
|
882
|
+
context.fail!(:funds_not_available, last_four: "1234")
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
```yaml
|
|
890
|
+
# en.yml
|
|
891
|
+
payment_gateway:
|
|
892
|
+
capture_funds:
|
|
893
|
+
light_service:
|
|
894
|
+
failures:
|
|
895
|
+
funds_not_available: "Unable to process your payment for account ending in %{last_four}"
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Custom adapter
|
|
899
|
+
|
|
900
|
+
Need a different lookup scheme? Subclass the built‑in adapter and set it in the
|
|
901
|
+
configuration:
|
|
902
|
+
|
|
903
|
+
```ruby
|
|
904
|
+
# config/initializers/light_service.rb
|
|
905
|
+
FunctionalLightService::Configuration.localization_adapter = MyLocalizer.new
|
|
906
|
+
|
|
907
|
+
# lib/my_localizer.rb
|
|
908
|
+
class MyLocalizer < FunctionalLightService::LocalizationAdapter
|
|
909
|
+
# change default scope to: "light_service.failures.<class_path>"
|
|
910
|
+
def i18n_scope_from_class(action_class, type)
|
|
911
|
+
"light_service.#{type.pluralize}.#{action_class.name.underscore}"
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### Retrieving the message
|
|
917
|
+
|
|
918
|
+
After an action halts with fail! or succeed!, read the translated text via:
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
result = FooAction.execute(baz: 1)
|
|
922
|
+
puts result.message # ⇒ "Exceeded API limit" (or localized equivalent)
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
## Logic in Organizers
|
|
926
|
+
|
|
927
|
+
The Organizer - Action combination works really well for simple use cases. However, as business logic gets more complex, or when FunctionalLightService is used in an ETL workflow, the code that routes the different organizers becomes very complex and imperative. Let's look at a piece of code that does basic data transformations:
|
|
928
|
+
|
|
929
|
+
```ruby
|
|
930
|
+
class ExtractsTransformsLoadsData
|
|
931
|
+
def self.run(connection)
|
|
932
|
+
context = RetrievesConnectionInfo.call(connection)
|
|
933
|
+
context = PullsDataFromRemoteApi.call(context)
|
|
934
|
+
|
|
935
|
+
retrieved_items = context.retrieved_items
|
|
936
|
+
if retrieved_items.empty?
|
|
937
|
+
NotifiesEngineeringTeamAction.execute(context)
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
retrieved_items.each do |item|
|
|
941
|
+
context[:item] = item
|
|
942
|
+
TransformsData.call(context)
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
context = LoadsData.call(context)
|
|
946
|
+
|
|
947
|
+
SendsNotifications.call(context)
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Declarative version
|
|
953
|
+
|
|
954
|
+
```ruby
|
|
955
|
+
class ExtractsTransformsLoadsData
|
|
956
|
+
extend FunctionalLightService::Organizer
|
|
957
|
+
|
|
958
|
+
def self.call(connection)
|
|
959
|
+
with(:connection => connection).reduce(actions)
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def self.actions
|
|
963
|
+
[
|
|
964
|
+
RetrievesConnectionInfo,
|
|
965
|
+
PullsDataFromRemoteApi,
|
|
966
|
+
reduce_if(->(ctx) { ctx.retrieved_items.empty? }, [
|
|
967
|
+
NotifiesEngineeringTeamAction
|
|
968
|
+
]),
|
|
969
|
+
iterate(:retrieved_items, [
|
|
970
|
+
TransformsData
|
|
971
|
+
]),
|
|
972
|
+
LoadsData,
|
|
973
|
+
SendsNotifications
|
|
974
|
+
]
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
The declarative style is shorter, easier to scan, and keeps flow control out of
|
|
980
|
+
your actions.
|
|
981
|
+
|
|
982
|
+
### Organizer constructs
|
|
983
|
+
|
|
984
|
+
| Construct | Declarative “equivalent” | What it does (in one line) |
|
|
985
|
+
| ------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
|
|
986
|
+
| [reduce_until](spec/acceptance/organizer/reduce_until_spec.rb) | `while` loop | Keeps reducing the listed steps **until** the lambda returns `true`. |
|
|
987
|
+
| [reduce_if](spec/acceptance/organizer/reduce_if_spec.rb) | `if/else` | Reduces its sub‑steps **only if** the lambda returns `true`. |
|
|
988
|
+
| [iterate](spec/acceptance/organizer/iterate_spec.rb) | `each` loop | Loops over a collection key; each element is exposed under the **singular** name. |
|
|
989
|
+
| [execute](spec/acceptance/organizer/execute_spec.rb) | one‑off lambda | Runs an inline lambda for quick context tweaks (add keys, transform values, etc.). |
|
|
990
|
+
| [with_callback](spec/acceptance/organizer/with_callback_spec.rb) | streaming callback | Defers execution like a SAX parser—great for huge inputs without loading everything in RAM. |
|
|
991
|
+
| [add_to_context](spec/acceptance/organizer/add_to_context_spec.rb) | N/A (context inject) | Injects key–value pairs into the context just before the following steps run. |
|
|
992
|
+
| [add_aliases](spec/acceptance/organizer/add_aliases_spec.rb) | key aliasing | Creates an alias so actions can read/write the same value under different names. |
|
|
993
|
+
|
|
994
|
+
All seven are covered by acceptance tests in spec/acceptance/organizer/*_spec.rb.
|
|
995
|
+
|
|
996
|
+
**Tip**: When iterating, the collection must already be in the context.
|
|
997
|
+
iterate(:items) expects context[:items]; it then places each element under
|
|
998
|
+
context.item for the inner actions.
|
|
999
|
+
|
|
1000
|
+
```ruby
|
|
1001
|
+
iterate(:items, [ProcessItem])
|
|
1002
|
+
# Inside ProcessItem → context.item
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
Need a quick context mutation? Use execute:
|
|
1006
|
+
|
|
1007
|
+
```ruby
|
|
1008
|
+
execute(->(c) { c[:some_values] = c.some_hash.values })
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
## ContextFactory for Faster Action Testing
|
|
1012
|
+
|
|
1013
|
+
As workflows grow more complex, building a realistic
|
|
1014
|
+
`FunctionalLightService::Context` for unit tests can become painful.
|
|
1015
|
+
Factory objects help, but the data you assemble by hand may still differ
|
|
1016
|
+
from what earlier actions really produce—especially in ETL pipelines where
|
|
1017
|
+
each step mutates the context.
|
|
1018
|
+
|
|
1019
|
+
### Example pipeline:
|
|
1020
|
+
|
|
1021
|
+
```ruby
|
|
1022
|
+
class SomeOrganizer
|
|
1023
|
+
extend FunctionalLightService::Organizer
|
|
1024
|
+
|
|
1025
|
+
def self.call(ctx)
|
|
1026
|
+
with(ctx).reduce(actions)
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def self.actions
|
|
1030
|
+
[
|
|
1031
|
+
ETL::ParsesPayloadAction,
|
|
1032
|
+
ETL::BuildsEnititiesAction,
|
|
1033
|
+
ETL::SetsUpMappingsAction,
|
|
1034
|
+
ETL::SavesEntitiesAction,
|
|
1035
|
+
ETL::SendsNotificationAction
|
|
1036
|
+
]
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
You should test your workflow from the outside, invoking the organizer’s `call` method and verify that the data was properly created or updated in your data store. However, sometimes you need to zoom into one action, and setting up the context to test it is tedious work. This is where `ContextFactory` can be helpful.
|
|
1042
|
+
|
|
1043
|
+
### Enter ContextFactory
|
|
1044
|
+
|
|
1045
|
+
FunctionalLightService::Testing::ContextFactory can generate a
|
|
1046
|
+
pre-populated context that mirrors real runtime data, letting you focus on
|
|
1047
|
+
the behaviour you want to test.
|
|
1048
|
+
|
|
1049
|
+
```ruby
|
|
1050
|
+
require "spec_helper"
|
|
1051
|
+
require "light-service/testing"
|
|
1052
|
+
|
|
1053
|
+
RSpec.describe ETL::SetsUpMappingsAction do
|
|
1054
|
+
let(:context) do
|
|
1055
|
+
FunctionalLightService::Testing::ContextFactory
|
|
1056
|
+
.make_from(SomeOrganizer) # build the full pipeline
|
|
1057
|
+
.for(described_class) # stop right before our action
|
|
1058
|
+
.with(payload: File.read("spec/data/payload.json"))
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
it "sets up mappings correctly" do
|
|
1062
|
+
result = described_class.execute(context)
|
|
1063
|
+
expect(result).to be_success
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
No more 20-line fixture setup—just a realistic context ready to go.
|
|
1069
|
+
|
|
1070
|
+
If your organizer contains additional logic in its own call method,
|
|
1071
|
+
create a test-only organizer inside your specs.
|
|
1072
|
+
See [acceptance test](spec/acceptance/testing/context_factory_spec.rb#L4-L11) for a full example.
|
|
1073
|
+
|
|
1074
|
+
## Functional Programming
|
|
1075
|
+
|
|
1076
|
+
FunctionalLightService lets you write **confident**, side-effect-aware Ruby by
|
|
1077
|
+
offering monads and algebraic data types (ADTs) you can compose and pattern-match
|
|
1078
|
+
without boilerplate.
|
|
1079
|
+
|
|
1080
|
+
### Pattern Overview
|
|
1081
|
+
|
|
1082
|
+
| Monad / ADT | When to use it | Typical flow control |
|
|
1083
|
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
1084
|
+
| **Result** (`Success / Failure`) | An operation can **succeed or fail** and the *value matters* either way. | Short-circuit on the first `Failure`. |
|
|
1085
|
+
| **Option** (`Some / None`) | An operation may return **a value or nothing**, and *why it’s missing doesn’t matter*. Think collections or cache hits. | Run every step, keep only the `Some` results. |
|
|
1086
|
+
| **Maybe** | Wrap any object that *might be `nil`* to avoid endless `nil?` checks. | Chain safe calls; `Null` swallows method calls. |
|
|
1087
|
+
| **Enums** (custom ADTs) | Define your own tagged unions when the built-ins don’t fit. | Full pattern-matching support. |
|
|
1088
|
+
|
|
1089
|
+
### Usage
|
|
1090
|
+
|
|
1091
|
+
### Result – `Success / Failure` <a name="functional-usage-success-failure"></a>
|
|
1092
|
+
|
|
1093
|
+
```ruby
|
|
1094
|
+
Success(1).to_s # => "1"
|
|
1095
|
+
Success(Success(1)) # => Success(1)
|
|
1096
|
+
|
|
1097
|
+
Failure(1).to_s # => "1"
|
|
1098
|
+
Failure(Failure(1)) # => Failure(1)
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
#### Mapping and binding
|
|
1102
|
+
|
|
1103
|
+
```ruby
|
|
1104
|
+
Success(1).fmap { |v| v + 1 } # => Success(2)
|
|
1105
|
+
Failure(1).bind { |v| Success(v - 1) } # => Success(0)
|
|
1106
|
+
|
|
1107
|
+
Success(1).map { |n| Success(n + 1) } # => Success(2)
|
|
1108
|
+
Failure(1).map_err { |n| Success(n + 1) } # => Success(2)
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
#### Flow helpers
|
|
1112
|
+
|
|
1113
|
+
```ruby
|
|
1114
|
+
Success(1).and Success(2) # => Success(2)
|
|
1115
|
+
Success(1).and_then { Success(2) } # => Success(2)
|
|
1116
|
+
|
|
1117
|
+
Failure(1).or Success(99) # => Success(99)
|
|
1118
|
+
Failure(1).or_else { |n| Success(n + 1) } # => Success(2)
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
#### Exception capturing
|
|
1122
|
+
|
|
1123
|
+
```ruby
|
|
1124
|
+
include FunctionalLightService::Prelude::Result
|
|
1125
|
+
|
|
1126
|
+
try! { 1 } # => Success(1)
|
|
1127
|
+
try! { raise "hell" } # => Failure(#<RuntimeError: hell>)
|
|
1128
|
+
try! { risky_call } # => Success(result) or Failure(err)
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
### Result Chaining <a name="functional-usage-chaining"></a>
|
|
1132
|
+
|
|
1133
|
+
You can easily chain the execution of several operations. Here we got some nice function composition.
|
|
1134
|
+
The method must be a unary function, i.e. it always takes one parameter - the context, which is passed from call to call.
|
|
1135
|
+
|
|
1136
|
+
The following aliases are defined
|
|
1137
|
+
|
|
1138
|
+
```ruby
|
|
1139
|
+
alias :>> :map
|
|
1140
|
+
alias :<< :pipe
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
This allows the composition of procs or lambdas and thus allow a clear definiton of a pipeline.
|
|
1144
|
+
|
|
1145
|
+
```ruby
|
|
1146
|
+
Success(params) >>
|
|
1147
|
+
validate >>
|
|
1148
|
+
build_request << log >>
|
|
1149
|
+
send << log >>
|
|
1150
|
+
build_response
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
#### Complex Example in a Builder Action <a name="functional-usage-complex-action"></a>
|
|
1154
|
+
|
|
1155
|
+
```ruby
|
|
1156
|
+
class Foo
|
|
1157
|
+
extend FunctionalLightService::Action
|
|
1158
|
+
expects :params
|
|
1159
|
+
alias :m :method
|
|
1160
|
+
|
|
1161
|
+
executed do |ctx|
|
|
1162
|
+
Success(ctx.params) >> m(:validate) >> m(:send)
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1165
|
+
def self.validate(params)
|
|
1166
|
+
# do stuff
|
|
1167
|
+
Success(validate_and_cleansed_params)
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
def self.send(clean_params)
|
|
1171
|
+
# do stuff
|
|
1172
|
+
Success(result)
|
|
1173
|
+
end
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
class Bar
|
|
1177
|
+
extend FunctionalLightService::Organizer
|
|
1178
|
+
|
|
1179
|
+
def self.call(params)
|
|
1180
|
+
with(:params => params).reduce(Foo)
|
|
1181
|
+
end
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
Bar.call # Success(3)
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
Chaining works with blocks (`#map` is an alias for `#>>`)
|
|
1188
|
+
|
|
1189
|
+
```ruby
|
|
1190
|
+
Success(1).map {|ctx| Success(ctx + 1)}
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
it also works with lambdas
|
|
1194
|
+
|
|
1195
|
+
```ruby
|
|
1196
|
+
Success(1) >> ->(ctx) { Success(ctx + 1) } >> ->(ctx) { Success(ctx + 1) }
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
and it will break the chain of execution, when it encounters a `Failure` on its way
|
|
1200
|
+
|
|
1201
|
+
```ruby
|
|
1202
|
+
def works(ctx)
|
|
1203
|
+
Success(1)
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
def breaks(ctx)
|
|
1207
|
+
Failure(2)
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
def never_executed(ctx)
|
|
1211
|
+
Success(99)
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
Success(0) >> method(:works) >> method(:breaks) >> method(:never_executed) # Failure(2)
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
`#map` aka `#>>` will not catch any exceptions raised. If you want automatic exception handling, the `#try` aka `#>=` will catch an error and wrap it with a failure
|
|
1218
|
+
|
|
1219
|
+
```ruby
|
|
1220
|
+
def error(ctx)
|
|
1221
|
+
raise "error #{ctx}"
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
Success(1) >= method(:error) # Failure(RuntimeError(error 1))
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
### Pattern matching <a name="functional-usage-pattern-matching"></a>
|
|
1228
|
+
|
|
1229
|
+
Now that you have some result, you want to control flow by providing patterns.
|
|
1230
|
+
`#match` can match by
|
|
1231
|
+
|
|
1232
|
+
* success, failure, result or any
|
|
1233
|
+
* values
|
|
1234
|
+
* lambdas
|
|
1235
|
+
* classes
|
|
1236
|
+
|
|
1237
|
+
```ruby
|
|
1238
|
+
Success(1).match do
|
|
1239
|
+
Success() { |s| "success #{s}"}
|
|
1240
|
+
Failure() { |f| "failure #{f}"}
|
|
1241
|
+
end # => "success 1"
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
Note1: the variant's inner value(s) have been unwrapped, and passed to the block.
|
|
1245
|
+
|
|
1246
|
+
Note2: only the __first__ matching pattern block will be executed, so order __can__ be important.
|
|
1247
|
+
|
|
1248
|
+
Note3: you can omit block parameters if you don't use them, or you can use `_` to signify that you don't care about their values. If you specify parameters, their number must match the number of values in the variant.
|
|
1249
|
+
|
|
1250
|
+
The result returned will be the result of the __first__ `#try` or `#let`. As a side note, `#try` is a monad, `#let` is a functor.
|
|
1251
|
+
|
|
1252
|
+
Guards
|
|
1253
|
+
|
|
1254
|
+
```ruby
|
|
1255
|
+
Success(1).match do
|
|
1256
|
+
Success(where { s == 1 }) { |s| "Success #{s}" }
|
|
1257
|
+
end # => "Success 1"
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
Note1: the guard has access to variable names defined by the block arguments.
|
|
1261
|
+
|
|
1262
|
+
Note2: the guard is not evaluated using the enclosing context's `self`; if you need to call methods on the enclosing scope, you must specify a receiver.
|
|
1263
|
+
|
|
1264
|
+
Also you can match the result class
|
|
1265
|
+
|
|
1266
|
+
```ruby
|
|
1267
|
+
Success([1, 2, 3]).match do
|
|
1268
|
+
Success(where { s.is_a?(Array) }) { |s| s.first }
|
|
1269
|
+
end # => 1
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
If no match was found a `NoMatchError` is raised, so make sure you always cover all possible outcomes.
|
|
1273
|
+
|
|
1274
|
+
```ruby
|
|
1275
|
+
Success(1).match do
|
|
1276
|
+
Failure() { |f| "you'll never get me" }
|
|
1277
|
+
end # => NoMatchError
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
Matches must be exhaustive, otherwise an error will be raised, showing the variants which have not been covered.
|
|
1281
|
+
|
|
1282
|
+
### Option <a name="functional-usage-option"></a>
|
|
1283
|
+
|
|
1284
|
+
```ruby
|
|
1285
|
+
Some(1).some? # #=> true
|
|
1286
|
+
Some(1).none? # #=> false
|
|
1287
|
+
None.some? # #=> false
|
|
1288
|
+
None.none? # #=> true
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
Maps an `Option` with the value `a` to the same `Option` with the value `b`.
|
|
1292
|
+
|
|
1293
|
+
```ruby
|
|
1294
|
+
Some(1).fmap { |n| n + 1 } # => Some(2)
|
|
1295
|
+
None.fmap { |n| n + 1 } # => None
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
Maps a `Result` with the value `a` to another `Result` with the value `b`.
|
|
1299
|
+
|
|
1300
|
+
```ruby
|
|
1301
|
+
Some(1).map { |n| Some(n + 1) } # => Some(2)
|
|
1302
|
+
Some(1).map { |n| None } # => None
|
|
1303
|
+
None.map { |n| Some(n + 1) } # => None
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
Get the inner value or provide a default for a `None`. Calling `#value` on a `None` will raise a `NoMethodError`
|
|
1307
|
+
|
|
1308
|
+
```ruby
|
|
1309
|
+
Some(1).value # => 1
|
|
1310
|
+
Some(1).value_or(2) # => 1
|
|
1311
|
+
None.value # => NoMethodError
|
|
1312
|
+
None.value_or(0) # => 0
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
Add the inner values of option using `+`.
|
|
1316
|
+
|
|
1317
|
+
```ruby
|
|
1318
|
+
Some(1) + Some(1) # => Some(2)
|
|
1319
|
+
Some([1]) + Some(1) # => TypeError: No implicit conversion
|
|
1320
|
+
None + Some(1) # => Some(1)
|
|
1321
|
+
Some(1) + None # => Some(1)
|
|
1322
|
+
Some([1]) + None + Some([2]) # => Some([1, 2])
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Coercion <a name="functional-usage-coercion"></a>
|
|
1326
|
+
|
|
1327
|
+
```ruby
|
|
1328
|
+
Option.any?(nil) # => None
|
|
1329
|
+
Option.any?([]) # => None
|
|
1330
|
+
Option.any?({}) # => None
|
|
1331
|
+
Option.any?(1) # => Some(1)
|
|
1332
|
+
|
|
1333
|
+
Option.some?(nil) # => None
|
|
1334
|
+
Option.some?([]) # => Some([])
|
|
1335
|
+
Option.some?({}) # => Some({})
|
|
1336
|
+
Option.some?(1) # => Some(1)
|
|
1337
|
+
|
|
1338
|
+
Option.try! { 1 } # => Some(1)
|
|
1339
|
+
Option.try! { raise "error"} # => None
|
|
1340
|
+
|
|
1341
|
+
Some(1).match {
|
|
1342
|
+
Some(where { s == 1 }) { |s| s + 1 }
|
|
1343
|
+
Some() { |s| 1 }
|
|
1344
|
+
None() { 0 }
|
|
1345
|
+
} # => 2
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
### Maybe <a name="functional-usage-maybe"></a>
|
|
1349
|
+
|
|
1350
|
+
The simplest NullObject wrapper there can be. It adds `#some?` and `#null?` to `Object` though.
|
|
1351
|
+
|
|
1352
|
+
```ruby
|
|
1353
|
+
require 'functional-light-service/functional/maybe' # you need to do this explicitly
|
|
1354
|
+
Maybe(nil).foo # => Null
|
|
1355
|
+
Maybe(nil).foo.bar # => Null
|
|
1356
|
+
Maybe({a: 1})[:a] # => 1
|
|
1357
|
+
|
|
1358
|
+
Maybe(nil).null? # => true
|
|
1359
|
+
Maybe({}).null? # => false
|
|
1360
|
+
|
|
1361
|
+
Maybe(nil).some? # => false
|
|
1362
|
+
Maybe({}).some? # => true
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
### Enums (custom ADTs) <a name="functional-usage-enum"></a>
|
|
1366
|
+
|
|
1367
|
+
All the above are implemented using enums, see their definition, for more details.
|
|
1368
|
+
|
|
1369
|
+
```ruby
|
|
1370
|
+
Threenum = FunctionalLightService::enum {
|
|
1371
|
+
Nullary()
|
|
1372
|
+
Unary(:a)
|
|
1373
|
+
Binary(:a, :b)
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
Threenum.variants # => [:Nullary, :Unary, :Binary]
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
Initialize
|
|
1380
|
+
|
|
1381
|
+
```ruby
|
|
1382
|
+
n = Threenum.Nullary # => Threenum::Nullary.new()
|
|
1383
|
+
n.value # => Error
|
|
1384
|
+
|
|
1385
|
+
u = Threenum.Unary(1) # => Threenum::Unary.new(1)
|
|
1386
|
+
u.value # => 1
|
|
1387
|
+
|
|
1388
|
+
b = Threenum::Binary(2, 3) # => Threenum::Binary(2, 3)
|
|
1389
|
+
b.value # => { a:2, b: 3 }
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
Pattern matching
|
|
1393
|
+
|
|
1394
|
+
```ruby
|
|
1395
|
+
Threenum::Unary(5).match {
|
|
1396
|
+
Nullary() { 0 }
|
|
1397
|
+
Unary() { |u| u }
|
|
1398
|
+
Binary() { |a, b| a + b }
|
|
1399
|
+
} # => 5
|
|
1400
|
+
|
|
1401
|
+
# or
|
|
1402
|
+
t = Threenum::Unary(5)
|
|
1403
|
+
Threenum.match(t) {
|
|
1404
|
+
Nullary() { 0 }
|
|
1405
|
+
Unary() { |u| u }
|
|
1406
|
+
Binary() { |a, b| a + b }
|
|
1407
|
+
} # => 5
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
If you want to return the whole matched object, you'll need to pass a reference to the object (second case). Note that `self` refers to the scope enclosing the `match` call.
|
|
1411
|
+
|
|
1412
|
+
```ruby
|
|
1413
|
+
def drop(n)
|
|
1414
|
+
match {
|
|
1415
|
+
Cons(where { n > 0 }) { |h, t| t.drop(n - 1) }
|
|
1416
|
+
Cons() { |_, _| self }
|
|
1417
|
+
Nil() { raise EmptyListError }
|
|
1418
|
+
}
|
|
1419
|
+
end
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
See the linked list implementation in the specs for more examples
|
|
1423
|
+
|
|
1424
|
+
With guard clauses
|
|
1425
|
+
|
|
1426
|
+
```ruby
|
|
1427
|
+
Threenum::Unary(5).match {
|
|
1428
|
+
Nullary() { 0 }
|
|
1429
|
+
Unary() { |u| u }
|
|
1430
|
+
Binary(where { a.is_a?(Fixnum) && b.is_a?(Fixnum) }) { |a, b| a + b }
|
|
1431
|
+
Binary() { |a, b| raise "Expected a, b to be numbers" }
|
|
1432
|
+
} # => 5
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
#### Add methods with impl
|
|
1436
|
+
|
|
1437
|
+
```ruby
|
|
1438
|
+
FunctionalLightService::impl(Threenum) {
|
|
1439
|
+
def sum
|
|
1440
|
+
match {
|
|
1441
|
+
Nullary() { 0 }
|
|
1442
|
+
Unary() { |u| u }
|
|
1443
|
+
Binary() { |a, b| a + b }
|
|
1444
|
+
}
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
def +(other)
|
|
1448
|
+
match {
|
|
1449
|
+
Nullary() { other.sum }
|
|
1450
|
+
Unary() { |a| self.sum + other.sum }
|
|
1451
|
+
Binary() { |a, b| self.sum + other.sum }
|
|
1452
|
+
}
|
|
1453
|
+
end
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
Threenum.Nullary + Threenum.Unary(1) # => Unary(1)
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
All matches must be exhaustive; otherwise NoMatchError is raised.
|
|
1460
|
+
|
|
1461
|
+
## Usage <a name="usage"></a>
|
|
1462
|
+
|
|
1463
|
+
Based on the refactoring example above, just create an organizer object that calls the
|
|
1464
|
+
actions in order and write code for the actions. That's it.
|
|
1465
|
+
|
|
1466
|
+
For further examples, please visit the project's [Wiki](https://github.com/sphynx79/functional-light-service/wiki).
|
|
1467
|
+
|
|
1468
|
+
## Contributing
|
|
1469
|
+
|
|
1470
|
+
1. Fork it
|
|
1471
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
1472
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
1473
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
1474
|
+
5. Create new Pull Request
|
|
1475
|
+
|
|
1476
|
+
Huge thanks to the [contributors](https://github.com/sphynx79/functional-light-service/graphs/contributors)!
|
|
1477
|
+
|
|
1478
|
+
## Changelog
|
|
1479
|
+
|
|
1480
|
+
Follow the changelog in this [document](https://github.com/sphynx79/functional-light-service/blob/master/CHANGELOG.md).
|
|
1481
|
+
|
|
1482
|
+
## Thank You
|
|
1483
|
+
|
|
1484
|
+
A very special thank you to [Attila Domokos](https://github.com/adomokos) for
|
|
1485
|
+
his fantastic work on [LightService](https://github.com/adomokos/light-service).
|
|
1486
|
+
A very special thank you to [Piotr Zolnierek](https://github.com/pzol) for
|
|
1487
|
+
his fantastic work on [Deterministic](https://github.com/pzol/deterministic).
|
|
1488
|
+
FunctionalLightService is inspired heavily by the concepts put to code by Attila and add some functionality taken from the excellent work of mario Piotr.
|
|
1489
|
+
|
|
1490
|
+
## License
|
|
1491
|
+
|
|
1492
|
+
FunctionalLightService is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|