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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +47 -0
  3. data/.rubocop.yml +103 -15
  4. data/.solargraph.yml +11 -0
  5. data/Appraisals +6 -2
  6. data/CHANGELOG.md +128 -0
  7. data/Gemfile +1 -3
  8. data/README.md +1492 -1424
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/functional-light-service.gemspec +19 -8
  12. data/gemfiles/dry_inflector_0_2_1.gemfile +5 -0
  13. data/gemfiles/i18n_1_8_11.gemfile +5 -0
  14. data/lib/functional-light-service/context/key_verifier.rb +2 -2
  15. data/lib/functional-light-service/context.rb +152 -154
  16. data/lib/functional-light-service/functional/enum.rb +2 -6
  17. data/lib/functional-light-service/functional/maybe.rb +1 -0
  18. data/lib/functional-light-service/functional/null.rb +1 -1
  19. data/lib/functional-light-service/functional/option.rb +0 -2
  20. data/lib/functional-light-service/functional/result.rb +2 -2
  21. data/lib/functional-light-service/organizer/with_reducer.rb +6 -0
  22. data/lib/functional-light-service/organizer/with_reducer_factory.rb +1 -1
  23. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +5 -2
  24. data/lib/functional-light-service/organizer.rb +10 -0
  25. data/lib/functional-light-service/testing/context_factory.rb +2 -0
  26. data/lib/functional-light-service/version.rb +1 -1
  27. data/spec/acceptance/fail_spec.rb +42 -16
  28. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  29. data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
  30. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  31. data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
  32. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  33. data/spec/action_spec.rb +8 -0
  34. data/spec/context/inspect_spec.rb +6 -21
  35. data/spec/context_spec.rb +1 -1
  36. data/spec/lib/deterministic/monad_axioms.rb +2 -0
  37. data/spec/lib/deterministic/monad_spec.rb +2 -0
  38. data/spec/lib/deterministic/null_spec.rb +2 -0
  39. data/spec/lib/deterministic/option_spec.rb +18 -14
  40. data/spec/lib/enum_spec.rb +3 -1
  41. data/spec/organizer_spec.rb +21 -0
  42. data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
  43. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  44. data/spec/spec_helper.rb +8 -5
  45. data/spec/test_doubles.rb +56 -9
  46. metadata +156 -26
  47. data/.travis.yml +0 -24
  48. data/gemfiles/dry_inflector_0_2.gemfile +0 -8
  49. data/gemfiles/i18n_1_8.gemfile +0 -8
data/README.md CHANGED
@@ -1,1424 +1,1492 @@
1
- # FunctionalLightService
2
- [![Gem Version](https://img.shields.io/gem/v/functional-light-service.svg)](https://rubygems.org/gems/functional-light-service)
3
- [![Build Status](https://travis-ci.org/sphynx79/functional-light-service.svg?branch=master)](https://travis-ci.org/sphynx79/functional-light-service)
4
- [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT)
5
-
6
- ## Table of Content
7
- * [Requirements](#requirements)
8
- * [Installation](#installation)
9
- * [Why FunctionalLightService?](#why-functionallightservice?)
10
- * [Stopping the Series of Actions](#stopping-the-series-of-actions)
11
- * [Failing the Context](#failing-the-context)
12
- * [Skipping the Rest of the Actions](#skipping-the-rest-of-the-actions)
13
- * [Benchmarking Actions with Around Advice](#benchmarking-actions-with-around-advice)
14
- * [Before and After Action Hooks](#before-and-after-action-hooks)
15
- * [Key Aliases](#key-aliases)
16
- * [Logging](#logging)
17
- * [Error Codes](#error-codes)
18
- * [Action Rollback](#action-rollback)
19
- * [Localizing Messages](#localizing-messages)
20
- * [Logic in Organizers](#logic-in-organizers)
21
- * [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
22
- * [Functional programming](#functional-programming)
23
- * [Pattern](#pattern)
24
- * [Usage](#functional-usage)
25
- * [Result: Success & Failure](#functional-usage-success-failure)
26
- * [Result Chaining](#functional-usage-chaining)
27
- * [Complex Example in a Builder Action](#functional-usage-complex-action)
28
- * [Pattern matching](#functional-usage-pattern-matching)
29
- * [Option](#functional-usage-option)
30
- * [Coercion](#functional-usage-coercion)
31
- * [Enum](#functional-usage-enum)
32
- * [Maybe](#functional-usage-maybe)
33
- * [Usage](#usage)
34
-
35
-
36
- ## Requirements
37
-
38
- This gem requires ruby >= 2.5.0
39
-
40
- ## Installation
41
- Add this line to your application's Gemfile:
42
-
43
- ```bash
44
- gem 'functional-light-service'
45
- ```
46
-
47
- And then execute:
48
- ```bash
49
- $ bundle
50
- ```
51
-
52
- Or install it yourself as:
53
- ```bash
54
- $ gem install functional-light-service
55
- ```
56
-
57
- ## Why FunctionalLightService?
58
-
59
- While i was studying the functional programming in Ruby, i came across this fantastic gem Deterministic, that it simplified my the writing of my Ruby code with a functional approach.
60
- I used deterministic making extensive use of the in_sequence method, that allowed me to concatenate a series of actions in sequence, if all method that i call work nice without exception, it returned me a modad with the status Success (), in case of failure the rest of the actions was not executed, and return a monad with the status Failure ().
61
-
62
- I writing this code:
63
-
64
- ```ruby
65
- class Foo
66
- include Deterministic::Prelude
67
-
68
- def call(input)
69
- result = in_sequence do
70
- get(:sanitized_input) { sanitize(input) }
71
- and_then { validate(sanitized_input) }
72
- and_then { connect_db }
73
- get(:user) { get_user(sanitized_input) }
74
- and_yield { print_response(user) }
75
- end
76
- logger.warn(result.value) if result.failure?
77
- rescue StandardError => e
78
- logger.fatal(e.message)
79
- end
80
-
81
- def sanitize(input)
82
- sanitized_input = {}
83
- sanitized_input[:name] = input[:name].downcase
84
- sanitized_input[:password] = input[:password].downcase
85
- Success(sanitized_input)
86
- end
87
-
88
- def validate(sanitized_input)
89
- try! do
90
- raise "Not allow empty name" if sanitized_input[:name].empty?
91
- raise "Not allow empty password" if sanitized_input[:password].empty?
92
- end.map_err { |n| Failure(n.message) }
93
- end
94
-
95
- def connect_db
96
- try! do
97
- raise "Error connection to db" if rand(0..1) == 1
98
- end.map_err { |n| Failure(n.message) }
99
- end
100
-
101
- def get_user(sanitized_input)
102
- user = FAKEDB.find do |_k, v|
103
- sanitized_input[:name] == v[:name] && sanitized_input[:password] == v[:password]
104
- end
105
- user.nil? ? Failure("Name or password error") : Success(user)
106
- end
107
-
108
- def print_response(user)
109
- Success(logger.info("Login successful id: #{user[0]} name: #{user[1][:name]}"))
110
- end
111
- end
112
-
113
- Foo.new.call(:name => "foo", :password => "bar")
114
- ```
115
-
116
- At a certain point I felt the need to better structure my code and every action had its context.
117
- accidentally I came across in this fantastic gem light-service, that did just what I wanted, it allows me to separate the business and logic, organize the actions in sequence, and write my actions in separate classes with each its context
118
-
119
-
120
- ```ruby
121
- class Foo
122
- extend LightService::Organizer
123
-
124
- def self.call(name: "", password: "")
125
- result = with(:name => name, :password => password).reduce(actions)
126
- logger.warn(result.message) if result.failure?
127
- end
128
-
129
- def self.actions
130
- [
131
- Sanitize,
132
- Validate,
133
- ConnectDb,
134
- GetUser,
135
- PrintResponse
136
- ]
137
- end
138
- end
139
-
140
- class Sanitize
141
- extend LightService::Action
142
- expects :name, :password
143
- promises :sanitized_input
144
-
145
- executed do |ctx|
146
- sanitized_input = {}
147
- sanitized_input[:name] = ctx.name.downcase
148
- sanitized_input[:password] = ctx.password.downcase
149
- ctx.sanitized_input = sanitized_input
150
- end
151
- end
152
-
153
- class Validate
154
- extend LightService::Action
155
- expects :sanitized_input
156
-
157
- executed do |ctx|
158
- ctx.fail_and_return!("Not allow empty name") if ctx.sanitized_input[:name].empty?
159
- ctx.fail_and_return!("Not allow empty password") if ctx.sanitized_input[:password].empty?
160
- end
161
- end
162
-
163
- class ConnectDb
164
- extend LightService::Action
165
-
166
- executed do |ctx|
167
- raise "Error connection to db"
168
- rescue StandardError => e
169
- ctx.fail!(e.message) if rand(0..1) == 1
170
- end
171
-
172
- # private_class_method :..
173
- end
174
-
175
- class GetUser
176
- extend LightService::Action
177
- expects :sanitized_input
178
- promises :user
179
-
180
- executed do |ctx|
181
- user = FAKEDB.find do |_k, v|
182
- ctx.sanitized_input[:name] == v[:name] && ctx.sanitized_input[:password] == v[:password]
183
- end
184
- ctx.fail_and_return!("Name or password error") if user.nil?
185
- ctx.user = user
186
- end
187
- end
188
-
189
- class PrintResponse
190
- extend LightService::Action
191
- expects :user
192
-
193
- executed do |ctx|
194
- logger.info("Login successful id: #{ctx.user[0]} name: #{ctx.user[1][:name]}")
195
- end
196
- end
197
-
198
- Foo.call(:name => "foo", :password => "bar")
199
- ```
200
- But in this case I lost the power of functional programming that deterministic gave me, why not take the best of two world, this is the reason that brought me make this gem. Now I can use same same feature that light-service give me with the power functional programming.
201
-
202
- ```ruby
203
- class Foo
204
- extend FunctionalLightService::Organizer
205
-
206
- def self.call(name: "", password: "")
207
- result = with(:name => name, :password => password).reduce(actions)
208
- logger.warn(result.message) if result.failure?
209
- end
210
-
211
- def self.actions
212
- [
213
- Sanitize,
214
- Validate,
215
- ConnectDb,
216
- GetUser,
217
- PrintResponse
218
- ]
219
- end
220
- end
221
-
222
- class Sanitize
223
- extend FunctionalLightService::Action
224
- expects :name, :password
225
- promises :sanitized_input
226
-
227
- executed do |ctx|
228
- name = ctx.name
229
- password = ctx.password
230
- ctx.sanitized_input = downcase(name, password).value
231
- end
232
-
233
- def self.downcase(name, password)
234
- ctx.try! do
235
- {
236
- :name => name.downcase,
237
- :password => password.downcase
238
- }
239
- end.map_err { ctx.fail!("Error nel method downcase") }
240
- end
241
-
242
- private_class_method :downcase
243
- end
244
-
245
- class Validate
246
- extend FunctionalLightService::Action
247
- expects :sanitized_input
248
-
249
- executed do |ctx|
250
- validate_params(ctx.sanitized_input).match do
251
- None() { ctx.Success(0) }
252
- Some() { |errors| ctx.fail_and_return!(errors) }
253
- end
254
- end
255
-
256
- def self.validate_params(params)
257
- return ctx.Some("Not allow empty name") if ctx.Option.any?(params[:name]).none?
258
- return ctx.Some("Not allow empty password") if ctx.Option.any?(params[:password]).none?
259
-
260
- ctx.None
261
- end
262
-
263
- private_class_method :validate_params
264
- end
265
-
266
- class ConnectDb
267
- extend FunctionalLightService::Action
268
-
269
- executed do |ctx|
270
- ctx.try! do
271
- raise "Error connection to db" if rand(0..1) == 1
272
- end.map_err { |n| ctx.fail!(n.message) }
273
- end
274
- end
275
-
276
- class GetUser
277
- extend FunctionalLightService::Action
278
- expects :sanitized_input
279
- promises :user
280
-
281
- executed do |ctx|
282
- user = Success(ctx.sanitized_input[:name]) >> method(:fetch_name) >> method(:check_password)
283
- ctx.user = user.value
284
- end
285
-
286
- def self.fetch_name(name)
287
- records = FAKEDB.select { |_k, v| name == v[:name] }
288
- ctx.fail_and_return!("Name not found in DB") if records.empty?
289
-
290
- Success(records)
291
- end
292
-
293
- def self.check_password(records)
294
- record = records.select { |_k, v| ctx.sanitized_input[:password] == v[:password] }
295
- return ctx.fail_and_return!("Password is not correct") if record.empty?
296
-
297
- Success(record)
298
- end
299
-
300
- private_class_method :fetch_name, :check_password
301
- end
302
-
303
- class PrintResponse
304
- extend FunctionalLightService::Action
305
- expects :user
306
-
307
- executed do |ctx|
308
- id = ctx.user.keys[0]
309
- name = ctx.user.values[0][:name]
310
- logger.info("Login successful id: #{id} name: #{name}")
311
- end
312
- end
313
-
314
- Foo.call(:name => "foo", :password => "bar")
315
-
316
- ```
317
-
318
- ## Stopping the Series of Actions
319
- When nothing unexpected happens during the organizer's call, the returned `context` will be successful. Here is how you can check for this:
320
- ```ruby
321
- class SomeController < ApplicationController
322
- def index
323
- result_context = SomeOrganizer.call(current_user.id)
324
-
325
- if result_context.success?
326
- redirect_to foo_path, :notice => "Everything went OK! Thanks!"
327
- else
328
- flash[:error] = result_context.message
329
- render :action => "new"
330
- end
331
- end
332
- end
333
- ```
334
- However, sometimes not everything will play out as you expect it. An external API call might not be available or some complex business logic will need to stop the processing of the Series of Actions.
335
- You have two options to stop the call chain:
336
-
337
- 1. Failing the context
338
- 2. Skipping the rest of the actions
339
-
340
- ### Failing the Context
341
- When something goes wrong in an action and you want to halt the chain, you need to call `fail!` on the context object. This will push the context in a failure state (`context.failure? # will evalute to true`).
342
- The context's `fail!` method can take an optional message argument, this message might help describing what went wrong.
343
- In case you need to return immediately from the point of failure, you have to do that by calling `next context`.
344
-
345
- In case you want to fail the context and stop the execution of the executed block, use the `fail_and_return!('something went wrong')` method.
346
- This will immediately leave the block, you don't need to call `next context` to return from the block.
347
-
348
- Here is an example:
349
- ```ruby
350
- class SubmitsOrderAction
351
- extend FunctionalLightService::Action
352
- expects :order, :mailer
353
-
354
- executed do |context|
355
- unless context.order.submit_order_successful?
356
- context.fail_and_return!("Failed to submit the order")
357
- end
358
-
359
- # This won't be executed
360
- context.mailer.send_order_notification!
361
- end
362
- end
363
- ```
364
- ![fail-actions](https://raw.githubusercontent.com/sphynx79/functional-light-service/master/resources/fail_actions.png)
365
-
366
- 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.
367
-
368
- ### Skipping the rest of the actions
369
- You can skip the rest of the actions by calling `context.skip_remaining!`. This behaves very similarly to the above-mentioned `fail!` mechanism, except this will not push the context into a failure state.
370
- A good use case for this is executing the first couple of action and based on a check you might not need to execute the rest.
371
- Here is an example of how you do it:
372
- ```ruby
373
- class ChecksOrderStatusAction
374
- extend FunctionalLightService::Action
375
- expects :order
376
-
377
- executed do |context|
378
- if context.order.send_notification?
379
- context.skip_remaining!("Everything is good, no need to execute the rest of the actions")
380
- end
381
- end
382
- end
383
- ```
384
- ![skip-actions](https://raw.githubusercontent.com/sphynx79/functional-light-service/master/resources/skip_actions.png)
385
-
386
- In the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd decided to skip the rest, the 4th action was not invoked. The context was successful.
387
-
388
-
389
- ## Benchmarking Actions with Around Advice
390
- Benchmarking your action is needed when you profile the series of actions. You could add benchmarking logic to each and every action, however, that would blur the business logic you have in your actions.
391
-
392
- Take advantage of the organizer's `around_each` method, which wraps the action calls as its reducing them in order.
393
-
394
- Check out this example:
395
-
396
- ```ruby
397
- class LogDuration
398
- def self.call(context)
399
- start_time = Time.now
400
- result = yield
401
- duration = Time.now - start_time
402
- FunctionalLightService::Configuration.logger.info(
403
- :action => context.current_action,
404
- :duration => duration
405
- )
406
-
407
- result
408
- end
409
- end
410
-
411
- class CalculatesTax
412
- extend FunctionalLightService::Organizer
413
-
414
- def self.call(order)
415
- with(:order => order).around_each(LogDuration).reduce(
416
- LooksUpTaxPercentageAction,
417
- CalculatesOrderTaxAction,
418
- ProvidesFreeShippingAction
419
- )
420
- end
421
- end
422
- ```
423
-
424
- Any object passed into `around_each` must respond to #call with two arguments: the action name and the context it will execute with. It is also passed a block, where FunctionalLightService's action execution will be done in, so the result must be returned. While this is a little work, it also gives you before and after state access to the data for any auditing and/or checks you may need to accomplish.
425
-
426
- ## Before and After Action Hooks
427
-
428
- In case you need to inject code right before and after the actions are executed, you can use the `before_actions` and `after_actions` hooks. It accepts one or multiple lambdas that the Action implementation will invoke. This addition to FunctionalLightService is a great way to decouple instrumentation from business logic.
429
-
430
- Consider this code:
431
-
432
- ```ruby
433
- class SomeOrganizer
434
- extend FunctionalLightService::Organizer
435
-
436
- def self.call(ctx)
437
- with(ctx).reduce(actions)
438
- end
439
-
440
- def self.actions
441
- [
442
- OneAction,
443
- TwoAction,
444
- ThreeAction
445
- ]
446
- end
447
- end
448
-
449
- class TwoAction
450
- extend FunctionalLightService::Action
451
- expects :user, :logger
452
-
453
- executed do |ctx|
454
- # Logging information
455
- if ctx.user.role == 'admin'
456
- ctx.logger.info('admin is doing something')
457
- end
458
-
459
- ctx.user.do_something
460
- end
461
- end
462
- ```
463
-
464
- The logging logic makes `TwoAction` more complex, there is more code for logging than for business logic.
465
-
466
- You have two options to decouple instrumentation from real logic with `before_actions` and `after_actions` hooks:
467
-
468
- 1. Declare your hooks in the Organizer
469
- 2. Attach hooks to the Organizer from the outside
470
-
471
- This is how you can declaratively add before and after hooks to the Organizer:
472
-
473
- ```ruby
474
- class SomeOrganizer
475
- extend FunctionalLightService::Organizer
476
- before_actions (lambda do |ctx|
477
- if ctx.current_action == TwoAction
478
- return unless ctx.user.role == 'admin'
479
- ctx.logger.info('admin is doing something')
480
- end
481
- end)
482
- after_actions (lambda do |ctx|
483
- if ctx.current_action == TwoAction
484
- return unless ctx.user.role == 'admin'
485
- ctx.logger.info('admin is DONE doing something')
486
- end
487
- end)
488
-
489
- def self.call(ctx)
490
- with(ctx).reduce(actions)
491
- end
492
-
493
- def self.actions
494
- [
495
- OneAction,
496
- TwoAction,
497
- ThreeAction
498
- ]
499
- end
500
- end
501
-
502
- class TwoAction
503
- extend FunctionalLightService::Action
504
- expects :user
505
-
506
- executed do |ctx|
507
- ctx.user.do_something
508
- end
509
- end
510
- ```
511
-
512
- Note how the action has no logging logic after this change. Also, you can target before and after action logic for specific actions, as the `ctx.current_action` will have the class name of the currently processed action. In the example above, logging will occur only for `TwoAction` and not for `OneAction` or `ThreeAction`.
513
-
514
- Here is how you can declaratively add `before_hooks` or `after_hooks` to your Organizer from the outside:
515
-
516
- ```ruby
517
- SomeOrganizer.before_actions =
518
- lambda do |ctx|
519
- if ctx.current_action == TwoAction
520
- return unless ctx.user.role == 'admin'
521
- ctx.logger.info('admin is doing something')
522
- end
523
- end
524
- ```
525
-
526
- These ideas are originally from Aspect Oriented Programming, read more about them [here](https://en.wikipedia.org/wiki/Aspect-oriented_programming).
527
-
528
- ## Expects and Promises
529
- The `expects` and `promises` macros are rules for the inputs/outputs of an action.
530
- `expects` describes what keys it needs to execute, and `promises` makes sure the keys are in the context after the
531
- action is reduced. If either of them are violated, a custom exception is thrown.
532
-
533
- This is how it's used:
534
- ```ruby
535
- class FooAction
536
- extend FunctionalLightService::Action
537
- expects :baz
538
- promises :bar
539
-
540
- executed do |context|
541
- baz = context.fetch :baz
542
-
543
- bar = baz + 2
544
- context[:bar] = bar
545
- end
546
- end
547
- ```
548
-
549
- The `expects` macro does a bit more for you: it pulls the value with the expected key from the context, and
550
- makes it available to you through a reader. You can refactor the action like this:
551
-
552
- ```ruby
553
- class FooAction
554
- extend FunctionalLightService::Action
555
- expects :baz
556
- promises :bar
557
-
558
- executed do |context|
559
- bar = context.baz + 2
560
- context[:bar] = bar
561
- end
562
- end
563
- ```
564
-
565
- The `promises` macro will not only check if the context has the promised keys, it also sets it for you in the context if
566
- you use the accessor with the same name. The code above can be further simplified:
567
-
568
- ```ruby
569
- class FooAction
570
- extend FunctionalLightService::Action
571
- expects :baz
572
- promises :bar
573
-
574
- executed do |context|
575
- context.bar = context.baz + 2
576
- end
577
- end
578
- ```
579
-
580
- Take a look at [this spec](spec/action_expects_and_promises_spec.rb) to see the refactoring in action.
581
-
582
- ## Key Aliases
583
- The `aliases` macro sets up pairs of keys and aliases in an organizer. Actions can access the context using the aliases.
584
-
585
- This allows you to put together existing actions from different sources and have them work together without having to modify their code. Aliases will work with or without action `expects`.
586
-
587
- Say for example you have actions `AnAction` and `AnotherAction` that you've used in previous projects. `AnAction` provides `:my_key` but `AnotherAction` needs to use that value but expects `:key_alias`. You can use them together in an organizer like so:
588
-
589
- ```ruby
590
- class AnOrganizer
591
- extend FunctionalLightService::Organizer
592
-
593
- aliases :my_key => :key_alias
594
-
595
- def self.call(order)
596
- with(:order => order).reduce(
597
- AnAction,
598
- AnotherAction,
599
- )
600
- end
601
- end
602
-
603
- class AnAction
604
- extend FunctionalLightService::Action
605
- promises :my_key
606
-
607
- executed do |context|
608
- context.my_key = "value"
609
- end
610
- end
611
-
612
- class AnotherAction
613
- extend FunctionalLightService::Action
614
- expects :key_alias
615
-
616
- executed do |context|
617
- context.key_alias # => "value"
618
- end
619
- end
620
- ```
621
-
622
- ## Logging
623
- Enable FunctionalLightService's logging to better understand what goes on within the series of actions,
624
- what's in the context or when an action fails.
625
-
626
- Logging in FunctionalLightService is turned off by default. However, turning it on is simple. Add this line to your
627
- project's config file:
628
-
629
- ```ruby
630
- FunctionalLightService::Configuration.logger = Logger.new(STDOUT)
631
- ```
632
-
633
- You can turn off the logger by setting it to nil or `/dev/null`.
634
-
635
- ```ruby
636
- FunctionalLightService::Configuration.logger = Logger.new('/dev/null')
637
- ```
638
-
639
- Watch the console while you are executing the workflow through the organizer. You should see something like this:
640
-
641
- ```bash
642
- I, [DATE] INFO -- : [FunctionalLightService] - calling organizer <TestDoubles::MakesTeaAndCappuccino>
643
- I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :tea, :milk, :coffee
644
- I, [DATE] INFO -- : [FunctionalLightService] - executing <TestDoubles::MakesTeaWithMilkAction>
645
- I, [DATE] INFO -- : [FunctionalLightService] - expects: :tea, :milk
646
- I, [DATE] INFO -- : [FunctionalLightService] - promises: :milk_tea
647
- I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :tea, :milk, :coffee, :milk_tea
648
- I, [DATE] INFO -- : [FunctionalLightService] - executing <TestDoubles::MakesLatteAction>
649
- I, [DATE] INFO -- : [FunctionalLightService] - expects: :coffee, :milk
650
- I, [DATE] INFO -- : [FunctionalLightService] - promises: :latte
651
- I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :tea, :milk, :coffee, :milk_tea, :latte
652
- ```
653
-
654
- The log provides a blueprint of the series of actions. You can see what organizer is invoked, what actions
655
- are called in what order, what do the expect and promise and most importantly what keys you have in the context
656
- after each action is executed.
657
-
658
- The logger logs its messages with "INFO" level. The exception to this is the event when an action fails the context.
659
- That message is logged with "WARN" level:
660
-
661
- ```bash
662
- I, [DATE] INFO -- : [FunctionalLightService] - calling organizer <TestDoubles::MakesCappuccinoAddsTwoAndFails>
663
- I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :milk, :coffee
664
- W, [DATE] WARN -- : [FunctionalLightService] - :-((( <TestDoubles::MakesLatteAction> has failed...
665
- W, [DATE] WARN -- : [FunctionalLightService] - context message: Can't make a latte from a milk that's too hot!
666
- ```
667
-
668
- The log message will show you what message was added to the context when the action pushed the
669
- context into a failure state.
670
-
671
- The event of skipping the rest of the actions is also captured by its logs:
672
-
673
- ```bash
674
- I, [DATE] INFO -- : [FunctionalLightService] - calling organizer <TestDoubles::MakesCappuccinoSkipsAddsTwo>
675
- I, [DATE] INFO -- : [FunctionalLightService] - keys in context: :milk, :coffee
676
- I, [DATE] INFO -- : [FunctionalLightService] - ;-) <TestDoubles::MakesLatteAction> has decided to skip the rest of the actions
677
- I, [DATE] INFO -- : [FunctionalLightService] - context message: Can't make a latte with a fatty milk like that!
678
- ```
679
-
680
- You can specify the logger on the organizer level, so the organizer does not use the global logger.
681
-
682
- ```ruby
683
- class FooOrganizer
684
- extend FunctionalLightService::Organizer
685
- log_with Logger.new("/my/special.log")
686
- end
687
- ```
688
-
689
- ## Error Codes
690
- You can add some more structure to your error handling by taking advantage of error codes in the context.
691
- Normally, when something goes wrong in your actions, you fail the process by setting the context to failure:
692
-
693
- ```ruby
694
- class FooAction
695
- extend FunctionalLightService::Action
696
-
697
- executed do |context|
698
- context.fail!("I don't like what happened here.")
699
- end
700
- end
701
- ```
702
-
703
- However, you might need to handle the errors coming from your action pipeline differently.
704
- Using an error code can help you check what type of expected error occurred in the organizer
705
- or in the actions.
706
-
707
- ```ruby
708
- class FooAction
709
- extend FunctionalLightService::Action
710
-
711
- executed do |context|
712
- unless (service_call.success?)
713
- context.fail!("Service call failed", error_code: 1001)
714
- end
715
-
716
- # Do something else
717
-
718
- unless (entity.save)
719
- context.fail!("Saving the entity failed", error_code: 2001)
720
- end
721
- end
722
- end
723
- ```
724
-
725
- ## Action Rollback
726
- Sometimes your action has to undo what it did when an error occurs. Think about a chain of actions where you need
727
- to persist records in your data store in one action and you have to call an external service in the next. What happens if there
728
- is an error when you call the external service? You want to remove the records you previously saved. You can do it now with
729
- the `rolled_back` macro.
730
-
731
- ```ruby
732
- class SaveEntities
733
- extend FunctionalLightService::Action
734
- expects :user
735
-
736
- executed do |context|
737
- context.user.save!
738
- end
739
-
740
- rolled_back do |context|
741
- context.user.destroy
742
- end
743
- end
744
- ```
745
-
746
- You need to call the `fail_with_rollback!` method to initiate a rollback for actions starting with the action where the failure
747
- was triggered.
748
-
749
- ```ruby
750
- class CallExternalApi
751
- extend FunctionalLightService::Action
752
-
753
- executed do |context|
754
- api_call_result = SomeAPI.save_user(context.user)
755
-
756
- context.fail_with_rollback!("Error when calling external API") if api_call_result.failure?
757
- end
758
- end
759
- ```
760
-
761
- Using the `rolled_back` macro is optional for the actions in the chain. You shouldn't care about undoing non-persisted changes.
762
-
763
- The actions are rolled back in reversed order from the point of failure starting with the action that triggered it.
764
-
765
- See [this](spec/acceptance/rollback_spec.rb) acceptance test to learn more about this functionality.
766
-
767
- ## Localizing Messages
768
- By default FunctionalLightService provides a mechanism for easily translating your error or success messages via I18n. You can also provide your own custom localization adapter if your application's logic is more complex than what is shown here.
769
-
770
- ```ruby
771
- class FooAction
772
- extend FunctionalLightService::Action
773
-
774
- executed do |context|
775
- unless service_call.success?
776
- context.fail!(:exceeded_api_limit)
777
-
778
- # The failure message used here equates to:
779
- # I18n.t(:exceeded_api_limit, scope: "foo_action.light_service.failures")
780
- end
781
- end
782
- end
783
- ```
784
-
785
- This also works with nested classes via the ActiveSupport `#underscore` method, just as ActiveRecord performs localization lookups on models placed inside a module.
786
-
787
- ```ruby
788
- module PaymentGateway
789
- class CaptureFunds
790
- extend FunctionalLightService::Action
791
-
792
- executed do |context|
793
- if api_service.failed?
794
- context.fail!(:funds_not_available)
795
- end
796
-
797
- # this failure message equates to:
798
- # I18n.t(:funds_not_available, scope: "payment_gateway/capture_funds.light_service.failures")
799
- end
800
- end
801
- end
802
- ```
803
-
804
- If you need to provide custom variables for interpolation during localization, pass that along in a hash.
805
-
806
- ```ruby
807
- module PaymentGateway
808
- class CaptureFunds
809
- extend FunctionalLightService::Action
810
-
811
- executed do |context|
812
- if api_service.failed?
813
- context.fail!(:funds_not_available, last_four: "1234")
814
- end
815
-
816
- # this failure message equates to:
817
- # I18n.t(:funds_not_available, last_four: "1234", scope: "payment_gateway/capture_funds.light_service.failures")
818
-
819
- # the translation string itself being:
820
- # => "Unable to process your payment for account ending in %{last_four}"
821
- end
822
- end
823
- end
824
- ```
825
-
826
- To provide your own custom adapter, use the configuration setting and subclass the default adapter FunctionalLightService provides.
827
-
828
- ```ruby
829
- FunctionalLightService::Configuration.localization_adapter = MyLocalizer.new
830
-
831
- # lib/my_localizer.rb
832
- class MyLocalizer < FunctionalLightService::LocalizationAdapter
833
-
834
- # I just want to change the default lookup path
835
- # => "light_service.failures.payment_gateway/capture_funds"
836
- def i18n_scope_from_class(action_class, type)
837
- "light_service.#{type.pluralize}.#{action_class.name.underscore}"
838
- end
839
- end
840
- ```
841
-
842
- To get the value of a `fail!` or `succeed!` message, simply call `#message` on the returned context.
843
-
844
- ## Logic in Organizers
845
-
846
- 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:
847
-
848
- ```ruby
849
- class ExtractsTransformsLoadsData
850
- def self.run(connection)
851
- context = RetrievesConnectionInfo.call(connection)
852
- context = PullsDataFromRemoteApi.call(context)
853
-
854
- retrieved_items = context.retrieved_items
855
- if retrieved_items.empty?
856
- NotifiesEngineeringTeamAction.execute(context)
857
- end
858
-
859
- retrieved_items.each do |item|
860
- context[:item] = item
861
- TransformsData.call(context)
862
- end
863
-
864
- context = LoadsData.call(context)
865
-
866
- SendsNotifications.call(context)
867
- end
868
- end
869
- ```
870
-
871
- The `FunctionalLightService::Context` is initialized with the first action, that context is passed around among organizers and actions. This code is still simpler than many out there, but it feels very imperative: it has conditionals, iterators in it. Let's see how we could make it a bit more simpler with a declarative style:
872
-
873
- ```ruby
874
- class ExtractsTransformsLoadsData
875
- extend FunctionalLightService::Organizer
876
-
877
- def self.call(connection)
878
- with(:connection => connection).reduce(actions)
879
- end
880
-
881
- def self.actions
882
- [
883
- RetrievesConnectionInfo,
884
- PullsDataFromRemoteApi,
885
- reduce_if(->(ctx) { ctx.retrieved_items.empty? }, [
886
- NotifiesEngineeringTeamAction
887
- ]),
888
- iterate(:retrieved_items, [
889
- TransformsData
890
- ]),
891
- LoadsData,
892
- SendsNotifications
893
- ]
894
- end
895
- end
896
- ```
897
-
898
- This code is much easier to reason about, it's less noisy and it captures the goal of FunctionalLightService well: simple, declarative code that's easy to understand.
899
-
900
- The 5 different constructs an organizer can have:
901
-
902
- 1. `reduce_until`
903
- 2. `reduce_if`
904
- 3. `iterate`
905
- 4. `execute`
906
- 5. `with_callback`
907
-
908
- `reduce_until` behaves like a while loop in imperative languages, it iterates until the provided predicate in the lambda evaluates to true. Take a look at [this acceptance test](spec/acceptance/organizer/reduce_until_spec.rb) to see how it's used.
909
-
910
- `reduce_if` will reduce the included organizers and/or actions if the predicate in the lambda evaluates to true. [This acceptance test](spec/acceptance/organizer/reduce_if_spec.rb) describes this functionality.
911
-
912
- `iterate` gives your iteration logic, the symbol you define there has to be in the context as a key. For example, to iterate over items you will use `iterate(:items)` in your steps, the context needs to have `items` as a key, otherwise it will fail. The organizer will singularize the collection name and will put the actual item into the context under that name. Remaining with the example above, each element will be accessible by the name `item` for the actions in the `iterate` steps. [This acceptance test](spec/acceptance/organizer/iterate_spec.rb) should provide you with an example.
913
-
914
- To take advantage of another organizer or action, you might need to tweak the context a bit. Let's say you have a hash, and you need to iterate over its values in a series of action. To alter the context and have the values assigned into a variable, you need to create a new action with 1 line of code in it. That seems a lot of ceremony for a simple change. You can do that in a `execute` method like this `execute(->(ctx) { ctx[:some_values] = ctx.some_hash.values })`. [This test](spec/acceptance/organizer/execute_spec.rb) describes how you can use it.
915
-
916
- Use `with_callback` when you want to execute actions with a deferred and controlled callback. It works similar to a Sax parser, I've used it for processing large files. The advantage of it is not having to keep large amount of data in memory. See [this acceptance test](spec/acceptance/organizer/with_callback_spec.rb) as a working example.
917
-
918
- ## ContextFactory for Faster Action Testing
919
-
920
- As the complexity of your workflow increases, you will find yourself spending more and more time creating a context (FunctionalLightService::Context it is) for your action tests. Some of this code can be reused by clever factories, but still, you are using a context that is artificial, and can be different from what the previous actions produced. This is especially true, when you use FunctionalLightService in ETLs, where you start out with initial data and your actions are mutating its state.
921
-
922
- Here is an example:
923
-
924
- ```ruby
925
- class SomeOrganizer
926
- extend FunctionalLightService::Organizer
927
-
928
- def self.call(ctx)
929
- with(ctx).reduce(actions)
930
- end
931
-
932
- def self.actions
933
- [
934
- ETL::ParsesPayloadAction,
935
- ETL::BuildsEnititiesAction,
936
- ETL::SetsUpMappingsAction,
937
- ETL::SavesEntitiesAction,
938
- ETL::SendsNotificationAction
939
- ]
940
- end
941
- end
942
- ```
943
-
944
- 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.
945
-
946
- In order to test the third action `ETL::SetsUpMappingAction`, you have to have several entities in the context. Depending on the logic you need to write code for, this could be a lot of work. However, by using the `ContextFactory` in your spec, you could easily have a prepared context that’s ready for testing:
947
-
948
- ```ruby
949
- require 'spec_helper'
950
- require 'light-service/testing'
951
-
952
- RSpec.describe ETL::SetsUpMappingsAction do
953
- let(:context) do
954
- FunctionalLightService::Testing::ContextFactory
955
- .make_from(SomeOrganizer)
956
- .for(described_class)
957
- .with(:payload => File.read(‘spec/data/payload.json’)
958
- end
959
-
960
- it ‘works like it should’ do
961
- result = described_class.execute(context)
962
- expect(result).to be_success
963
- end
964
- end
965
- ```
966
-
967
- This context then can be passed to the action under test, freeing you up from the 20 lines of factory or fixture calls to create a context for your specs.
968
-
969
- In case your organizer has more logic in its `call` method, you could create your own test organizer in your specs like you can see it in this [acceptance test](spec/acceptance/testing/context_factory_spec.rb#L4-L11). This is reusable in all your action tests.
970
-
971
- ## Functional programming
972
- FunctionalLightService is to help your code to be more confident, by utilizing functional programming patterns.
973
-
974
- ## Patterns
975
- FunctionalLightService provides different monads, here is a short guide, when to use which
976
-
977
- #### Result: Success & Failure
978
- - an operation which can succeed or fail
979
- - the result (content) of of the success or failure is important
980
- - you are building one thing
981
- - chaining: if one fails (Failure), don't execute the rest
982
-
983
- #### Option: Some & None
984
- - an operation which returns either some result or nothing
985
- - in case it returns nothing it is not important to know why
986
- - you are working rather with a collection of things
987
- - chaining: execute all and then select the successful ones (Some)
988
-
989
-
990
- #### Maybe
991
- - an object may be nil, you want to avoid endless nil? checks
992
-
993
- #### Enums (Algebraic Data Types)
994
- - roll your own pattern
995
-
996
- ## Usage <a name="functional-usage"></a>
997
- ### Result: Success & Failure <a name="functional-usage-success-failure"></a>
998
-
999
- ```ruby
1000
- Success(1).to_s # => "1"
1001
- Success(Success(1)) # => Success(1)
1002
-
1003
- Failure(1).to_s # => "1"
1004
- Failure(Failure(1)) # => Failure(1)
1005
- ```
1006
-
1007
- Maps a `Result` with the value `a` to the same `Result` with the value `b`.
1008
-
1009
- ```ruby
1010
- Success(1).fmap { |v| v + 1} # => Success(2)
1011
- Failure(1).fmap { |v| v - 1} # => Failure(0)
1012
- ```
1013
-
1014
- Maps a `Result` with the value `a` to another `Result` with the value `b`.
1015
-
1016
- ```ruby
1017
- Success(1).bind { |v| Failure(v + 1) } # => Failure(2)
1018
- Failure(1).bind { |v| Success(v - 1) } # => Success(0)
1019
- ```
1020
-
1021
- Maps a `Success` with the value `a` to another `Result` with the value `b`. It works like `#bind` but only on `Success`.
1022
-
1023
- ```ruby
1024
- Success(1).map { |n| Success(n + 1) } # => Success(2)
1025
- Failure(0).map { |n| Success(n + 1) } # => Failure(0)
1026
- ```
1027
- Maps a `Failure` with the value `a` to another `Result` with the value `b`. It works like `#bind` but only on `Failure`.
1028
-
1029
- ```ruby
1030
- Failure(1).map_err { |n| Success(n + 1) } # => Success(2)
1031
- Success(0).map_err { |n| Success(n + 1) } # => Success(0)
1032
- ```
1033
-
1034
- ```ruby
1035
- Success(0).try { |n| raise "Error" } # => Failure(Error)
1036
- ```
1037
-
1038
- Replaces `Success a` with `Result b`. If a `Failure` is passed as argument, it is ignored.
1039
-
1040
- ```ruby
1041
- Success(1).and Success(2) # => Success(2)
1042
- Failure(1).and Success(2) # => Failure(1)
1043
- ```
1044
-
1045
- Replaces `Success a` with the result of the block. If a `Failure` is passed as argument, it is ignored.
1046
-
1047
- ```ruby
1048
- Success(1).and_then { Success(2) } # => Success(2)
1049
- Failure(1).and_then { Success(2) } # => Failure(1)
1050
- ```
1051
-
1052
- Replaces `Failure a` with `Result`. If a `Failure` is passed as argument, it is ignored.
1053
-
1054
- ```ruby
1055
- Success(1).or Success(2) # => Success(1)
1056
- Failure(1).or Success(1) # => Success(1)
1057
- ```
1058
-
1059
- Replaces `Failure a` with the result of the block. If a `Success` is passed as argument, it is ignored.
1060
-
1061
- ```ruby
1062
- Success(1).or_else { Success(2) } # => Success(1)
1063
- Failure(1).or_else { |n| Success(n)} # => Success(1)
1064
- ```
1065
-
1066
- Executes the block passed, but completely ignores its result. If an error is raised within the block it will **NOT** be catched.
1067
-
1068
- Try failable operations to return `Success` or `Failure`
1069
-
1070
- ```ruby
1071
- include FunctionalLightService::Prelude::Result
1072
-
1073
- try! { 1 } # => Success(1)
1074
- try! { raise "hell" } # => Failure(#<RuntimeError: hell>)
1075
- ```
1076
-
1077
- ### Result Chaining <a name="functional-usage-chaining"></a>
1078
- You can easily chain the execution of several operations. Here we got some nice function composition.
1079
- The method must be a unary function, i.e. it always takes one parameter - the context, which is passed from call to call.
1080
-
1081
- The following aliases are defined
1082
-
1083
- ```ruby
1084
- alias :>> :map
1085
- alias :<< :pipe
1086
- ```
1087
-
1088
- This allows the composition of procs or lambdas and thus allow a clear definiton of a pipeline.
1089
-
1090
- ```ruby
1091
- Success(params) >>
1092
- validate >>
1093
- build_request << log >>
1094
- send << log >>
1095
- build_response
1096
- ```
1097
-
1098
- #### Complex Example in a Builder Action <a name="functional-usage-complex-action"></a>
1099
-
1100
- ```ruby
1101
- class Foo
1102
- extend FunctionalLightService::Action
1103
- expects :params
1104
- alias :m :method
1105
-
1106
- executed do |ctx|
1107
- Success(ctx.params) >> m(:validate) >> m(:send)
1108
- end
1109
-
1110
- def self.validate(params)
1111
- # do stuff
1112
- Success(validate_and_cleansed_params)
1113
- end
1114
-
1115
- def self.send(clean_params)
1116
- # do stuff
1117
- Success(result)
1118
- end
1119
- end
1120
-
1121
- class Bar
1122
- extend FunctionalLightService::Organizer
1123
-
1124
- def self.call(params)
1125
- with(:params => params).reduce(Foo)
1126
- end
1127
- end
1128
-
1129
- Bar.call # Success(3)
1130
- ```
1131
-
1132
- Chaining works with blocks (`#map` is an alias for `#>>`)
1133
-
1134
- ```ruby
1135
- Success(1).map {|ctx| Success(ctx + 1)}
1136
- ```
1137
-
1138
- it also works with lambdas
1139
- ```ruby
1140
- Success(1) >> ->(ctx) { Success(ctx + 1) } >> ->(ctx) { Success(ctx + 1) }
1141
- ```
1142
-
1143
- and it will break the chain of execution, when it encounters a `Failure` on its way
1144
-
1145
- ```ruby
1146
- def works(ctx)
1147
- Success(1)
1148
- end
1149
-
1150
- def breaks(ctx)
1151
- Failure(2)
1152
- end
1153
-
1154
- def never_executed(ctx)
1155
- Success(99)
1156
- end
1157
-
1158
- Success(0) >> method(:works) >> method(:breaks) >> method(:never_executed) # Failure(2)
1159
- ```
1160
-
1161
- `#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
1162
-
1163
- ```ruby
1164
- def error(ctx)
1165
- raise "error #{ctx}"
1166
- end
1167
-
1168
- Success(1) >= method(:error) # Failure(RuntimeError(error 1))
1169
- ```
1170
- ### Pattern matching <a name="functional-usage-pattern-matching"></a>
1171
- Now that you have some result, you want to control flow by providing patterns.
1172
- `#match` can match by
1173
-
1174
- * success, failure, result or any
1175
- * values
1176
- * lambdas
1177
- * classes
1178
-
1179
- ```ruby
1180
- Success(1).match do
1181
- Success() { |s| "success #{s}"}
1182
- Failure() { |f| "failure #{f}"}
1183
- end # => "success 1"
1184
- ```
1185
- Note1: the variant's inner value(s) have been unwrapped, and passed to the block.
1186
-
1187
- Note2: only the __first__ matching pattern block will be executed, so order __can__ be important.
1188
-
1189
- 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.
1190
-
1191
- 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.
1192
-
1193
- Guards
1194
-
1195
- ```ruby
1196
- Success(1).match do
1197
- Success(where { s == 1 }) { |s| "Success #{s}" }
1198
- end # => "Success 1"
1199
- ```
1200
-
1201
- Note1: the guard has access to variable names defined by the block arguments.
1202
-
1203
- 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.
1204
-
1205
- Also you can match the result class
1206
-
1207
- ```ruby
1208
- Success([1, 2, 3]).match do
1209
- Success(where { s.is_a?(Array) }) { |s| s.first }
1210
- end # => 1
1211
- ```
1212
-
1213
- If no match was found a `NoMatchError` is raised, so make sure you always cover all possible outcomes.
1214
-
1215
- ```ruby
1216
- Success(1).match do
1217
- Failure() { |f| "you'll never get me" }
1218
- end # => NoMatchError
1219
- ```
1220
-
1221
- Matches must be exhaustive, otherwise an error will be raised, showing the variants which have not been covered.
1222
-
1223
- ### Option <a name="functional-usage-option"></a>
1224
-
1225
- ```ruby
1226
- Some(1).some? # #=> true
1227
- Some(1).none? # #=> false
1228
- None.some? # #=> false
1229
- None.none? # #=> true
1230
- ```
1231
-
1232
- Maps an `Option` with the value `a` to the same `Option` with the value `b`.
1233
-
1234
- ```ruby
1235
- Some(1).fmap { |n| n + 1 } # => Some(2)
1236
- None.fmap { |n| n + 1 } # => None
1237
- ```
1238
-
1239
- Maps a `Result` with the value `a` to another `Result` with the value `b`.
1240
-
1241
- ```ruby
1242
- Some(1).map { |n| Some(n + 1) } # => Some(2)
1243
- Some(1).map { |n| None } # => None
1244
- None.map { |n| Some(n + 1) } # => None
1245
- ```
1246
-
1247
- Get the inner value or provide a default for a `None`. Calling `#value` on a `None` will raise a `NoMethodError`
1248
-
1249
- ```ruby
1250
- Some(1).value # => 1
1251
- Some(1).value_or(2) # => 1
1252
- None.value # => NoMethodError
1253
- None.value_or(0) # => 0
1254
- ```
1255
-
1256
- Add the inner values of option using `+`.
1257
-
1258
- ```ruby
1259
- Some(1) + Some(1) # => Some(2)
1260
- Some([1]) + Some(1) # => TypeError: No implicit conversion
1261
- None + Some(1) # => Some(1)
1262
- Some(1) + None # => Some(1)
1263
- Some([1]) + None + Some([2]) # => Some([1, 2])
1264
- ```
1265
-
1266
- ### Coercion <a name="functional-usage-coercion"></a>
1267
- ```ruby
1268
- Option.any?(nil) # => None
1269
- Option.any?([]) # => None
1270
- Option.any?({}) # => None
1271
- Option.any?(1) # => Some(1)
1272
-
1273
- Option.some?(nil) # => None
1274
- Option.some?([]) # => Some([])
1275
- Option.some?({}) # => Some({})
1276
- Option.some?(1) # => Some(1)
1277
-
1278
- Option.try! { 1 } # => Some(1)
1279
- Option.try! { raise "error"} # => None
1280
-
1281
- Some(1).match {
1282
- Some(where { s == 1 }) { |s| s + 1 }
1283
- Some() { |s| 1 }
1284
- None() { 0 }
1285
- } # => 2
1286
- ```
1287
-
1288
- ### Enums <a name="functional-usage-enum"></a>
1289
- All the above are implemented using enums, see their definition, for more details.
1290
-
1291
- ```ruby
1292
- Threenum = FunctionalLightService::enum {
1293
- Nullary()
1294
- Unary(:a)
1295
- Binary(:a, :b)
1296
- }
1297
-
1298
- Threenum.variants # => [:Nullary, :Unary, :Binary]
1299
- ```
1300
- Initialize
1301
-
1302
- ```ruby
1303
- n = Threenum.Nullary # => Threenum::Nullary.new()
1304
- n.value # => Error
1305
-
1306
- u = Threenum.Unary(1) # => Threenum::Unary.new(1)
1307
- u.value # => 1
1308
-
1309
- b = Threenum::Binary(2, 3) # => Threenum::Binary(2, 3)
1310
- b.value # => { a:2, b: 3 }
1311
- ```
1312
- Pattern matching
1313
-
1314
- ```ruby
1315
- Threenum::Unary(5).match {
1316
- Nullary() { 0 }
1317
- Unary() { |u| u }
1318
- Binary() { |a, b| a + b }
1319
- } # => 5
1320
-
1321
- # or
1322
- t = Threenum::Unary(5)
1323
- Threenum.match(t) {
1324
- Nullary() { 0 }
1325
- Unary() { |u| u }
1326
- Binary() { |a, b| a + b }
1327
- } # => 5
1328
- ```
1329
-
1330
- 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.
1331
-
1332
- ```ruby
1333
- def drop(n)
1334
- match {
1335
- Cons(where { n > 0 }) { |h, t| t.drop(n - 1) }
1336
- Cons() { |_, _| self }
1337
- Nil() { raise EmptyListError }
1338
- }
1339
- end
1340
- ```
1341
-
1342
- See the linked list implementation in the specs for more examples
1343
-
1344
- With guard clauses
1345
-
1346
- ```ruby
1347
- Threenum::Unary(5).match {
1348
- Nullary() { 0 }
1349
- Unary() { |u| u }
1350
- Binary(where { a.is_a?(Fixnum) && b.is_a?(Fixnum) }) { |a, b| a + b }
1351
- Binary() { |a, b| raise "Expected a, b to be numbers" }
1352
- } # => 5
1353
- ```
1354
-
1355
- Implementing methods for enums
1356
-
1357
- ```ruby
1358
- FunctionalLightService::impl(Threenum) {
1359
- def sum
1360
- match {
1361
- Nullary() { 0 }
1362
- Unary() { |u| u }
1363
- Binary() { |a, b| a + b }
1364
- }
1365
- end
1366
-
1367
- def +(other)
1368
- match {
1369
- Nullary() { other.sum }
1370
- Unary() { |a| self.sum + other.sum }
1371
- Binary() { |a, b| self.sum + other.sum }
1372
- }
1373
- end
1374
- }
1375
-
1376
- Threenum.Nullary + Threenum.Unary(1) # => Unary(1)
1377
- ```
1378
-
1379
- All matches must be exhaustive, i.e. cover all variants
1380
-
1381
- ### Maybe <a name="functional-usage-maybe"></a>
1382
- The simplest NullObject wrapper there can be. It adds `#some?` and `#null?` to `Object` though.
1383
-
1384
- ```ruby
1385
- require 'functional-light-service/functional/maybe' # you need to do this explicitly
1386
- Maybe(nil).foo # => Null
1387
- Maybe(nil).foo.bar # => Null
1388
- Maybe({a: 1})[:a] # => 1
1389
-
1390
- Maybe(nil).null? # => true
1391
- Maybe({}).null? # => false
1392
-
1393
- Maybe(nil).some? # => false
1394
- Maybe({}).some? # => true
1395
- ```
1396
-
1397
- ## Usage <a name="usage"></a>
1398
- Based on the refactoring example above, just create an organizer object that calls the
1399
- actions in order and write code for the actions. That's it.
1400
-
1401
- For further examples, please visit the project's [Wiki](https://github.com/sphynx79/functional-light-service/wiki).
1402
-
1403
- ## Contributing
1404
- 1. Fork it
1405
- 2. Create your feature branch (`git checkout -b my-new-feature`)
1406
- 3. Commit your changes (`git commit -am 'Added some feature'`)
1407
- 4. Push to the branch (`git push origin my-new-feature`)
1408
- 5. Create new Pull Request
1409
-
1410
- Huge thanks to the [contributors](https://github.com/sphynx79/functional-light-service/graphs/contributors)!
1411
-
1412
- ## Changelog
1413
- Follow the changelog in this [document](https://github.com/sphynx79/functional-light-service/blob/master/CHANGELOG.md).
1414
-
1415
- ## Thank You
1416
-
1417
- A very special thank you to [Attila Domokos](https://github.com/adomokos) for
1418
- his fantastic work on [LightService](https://github.com/adomokos/light-service).
1419
- A very special thank you to [Piotr Zolnierek](https://github.com/pzol) for
1420
- his fantastic work on [Deterministic](https://github.com/pzol/deterministic).
1421
- FunctionalLightService is inspired heavily by the concepts put to code by Attila and add some functionality taken from the excellent work of mario Piotr.
1422
-
1423
- ## License
1424
- FunctionalLightService is released under the [MIT License](http://www.opensource.org/licenses/MIT).
1
+ # FunctionalLightService
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/functional-light-service.svg)](https://rubygems.org/gems/functional-light-service)
4
+ [![CI Tests](https://github.com/sphynx79/functional-light-service/actions/workflows/project-build.yml/badge.svg)](https://github.com/sphynx79/functional-light-service/actions/workflows/project-build.yml)
5
+ [![Codecov](https://codecov.io/gh/sphynx79/functional-light-service/branch/master/graph/badge.svg)](https://app.codecov.io/gh/sphynx79/functional-light-service)
6
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT)
7
+ [![Download Count](https://img.shields.io/gem/dt/functional-light-service)](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
+ ![fail-actions](https://raw.githubusercontent.com/sphynx79/functional-light-service/master/resources/fail_actions.png)
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
+ ![skip-actions](https://raw.githubusercontent.com/sphynx79/functional-light-service/master/resources/skip_actions.png)
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).