functional-light-service 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +72 -0
  5. data/.travis.yml +22 -0
  6. data/Appraisals +3 -0
  7. data/CHANGELOG.md +37 -0
  8. data/CODE_OF_CONDUCT.md +22 -0
  9. data/Gemfile +6 -0
  10. data/LICENSE +22 -0
  11. data/README.md +1424 -0
  12. data/Rakefile +12 -0
  13. data/VERSION +1 -0
  14. data/functional-light-service.gemspec +26 -0
  15. data/gemfiles/activesupport_5.gemfile +8 -0
  16. data/gemfiles/activesupport_5.gemfile.lock +82 -0
  17. data/lib/functional-light-service/action.rb +102 -0
  18. data/lib/functional-light-service/configuration.rb +24 -0
  19. data/lib/functional-light-service/context/key_verifier.rb +118 -0
  20. data/lib/functional-light-service/context.rb +165 -0
  21. data/lib/functional-light-service/errors.rb +6 -0
  22. data/lib/functional-light-service/functional/enum.rb +254 -0
  23. data/lib/functional-light-service/functional/maybe.rb +14 -0
  24. data/lib/functional-light-service/functional/monad.rb +66 -0
  25. data/lib/functional-light-service/functional/null.rb +74 -0
  26. data/lib/functional-light-service/functional/option.rb +99 -0
  27. data/lib/functional-light-service/functional/result.rb +122 -0
  28. data/lib/functional-light-service/localization_adapter.rb +44 -0
  29. data/lib/functional-light-service/organizer/execute.rb +14 -0
  30. data/lib/functional-light-service/organizer/iterate.rb +22 -0
  31. data/lib/functional-light-service/organizer/reduce_if.rb +17 -0
  32. data/lib/functional-light-service/organizer/reduce_until.rb +20 -0
  33. data/lib/functional-light-service/organizer/scoped_reducable.rb +13 -0
  34. data/lib/functional-light-service/organizer/verify_call_method_exists.rb +28 -0
  35. data/lib/functional-light-service/organizer/with_callback.rb +26 -0
  36. data/lib/functional-light-service/organizer/with_reducer.rb +71 -0
  37. data/lib/functional-light-service/organizer/with_reducer_factory.rb +18 -0
  38. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +105 -0
  39. data/lib/functional-light-service/organizer.rb +105 -0
  40. data/lib/functional-light-service/testing/context_factory.rb +40 -0
  41. data/lib/functional-light-service/testing.rb +1 -0
  42. data/lib/functional-light-service/version.rb +3 -0
  43. data/lib/functional-light-service.rb +29 -0
  44. data/resources/fail_actions.png +0 -0
  45. data/resources/light-service.png +0 -0
  46. data/resources/organizer_and_actions.png +0 -0
  47. data/resources/skip_actions.png +0 -0
  48. data/spec/acceptance/add_numbers_spec.rb +11 -0
  49. data/spec/acceptance/after_actions_spec.rb +71 -0
  50. data/spec/acceptance/around_each_spec.rb +19 -0
  51. data/spec/acceptance/before_actions_spec.rb +98 -0
  52. data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
  53. data/spec/acceptance/fail_spec.rb +24 -0
  54. data/spec/acceptance/include_warning_spec.rb +29 -0
  55. data/spec/acceptance/log_from_organizer_spec.rb +154 -0
  56. data/spec/acceptance/message_localization_spec.rb +118 -0
  57. data/spec/acceptance/not_having_call_method_warning_spec.rb +39 -0
  58. data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
  59. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +65 -0
  60. data/spec/acceptance/organizer/execute_spec.rb +46 -0
  61. data/spec/acceptance/organizer/iterate_spec.rb +37 -0
  62. data/spec/acceptance/organizer/reduce_if_spec.rb +51 -0
  63. data/spec/acceptance/organizer/reduce_until_spec.rb +43 -0
  64. data/spec/acceptance/organizer/with_callback_spec.rb +110 -0
  65. data/spec/acceptance/rollback_spec.rb +132 -0
  66. data/spec/acceptance/skip_all_warning_spec.rb +20 -0
  67. data/spec/acceptance/testing/context_factory_spec.rb +54 -0
  68. data/spec/action_expected_keys_spec.rb +63 -0
  69. data/spec/action_expects_and_promises_spec.rb +93 -0
  70. data/spec/action_promised_keys_spec.rb +122 -0
  71. data/spec/action_spec.rb +89 -0
  72. data/spec/context/inspect_spec.rb +57 -0
  73. data/spec/context_spec.rb +197 -0
  74. data/spec/examples/amount_spec.rb +77 -0
  75. data/spec/examples/controller_spec.rb +63 -0
  76. data/spec/examples/validate_address_spec.rb +37 -0
  77. data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
  78. data/spec/lib/deterministic/currify_spec.rb +88 -0
  79. data/spec/lib/deterministic/monad_axioms.rb +44 -0
  80. data/spec/lib/deterministic/monad_spec.rb +45 -0
  81. data/spec/lib/deterministic/null_spec.rb +58 -0
  82. data/spec/lib/deterministic/option_spec.rb +133 -0
  83. data/spec/lib/deterministic/result/failure_spec.rb +65 -0
  84. data/spec/lib/deterministic/result/result_map_spec.rb +154 -0
  85. data/spec/lib/deterministic/result/result_shared.rb +24 -0
  86. data/spec/lib/deterministic/result/success_spec.rb +41 -0
  87. data/spec/lib/deterministic/result_spec.rb +63 -0
  88. data/spec/lib/enum_spec.rb +112 -0
  89. data/spec/localization_adapter_spec.rb +83 -0
  90. data/spec/organizer/with_reducer_spec.rb +56 -0
  91. data/spec/organizer_key_aliases_spec.rb +29 -0
  92. data/spec/organizer_spec.rb +93 -0
  93. data/spec/readme_spec.rb +47 -0
  94. data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
  95. data/spec/sample/calculates_tax_spec.rb +30 -0
  96. data/spec/sample/looks_up_tax_percentage_action_spec.rb +53 -0
  97. data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
  98. data/spec/sample/tax/calculates_order_tax_action.rb +9 -0
  99. data/spec/sample/tax/calculates_tax.rb +11 -0
  100. data/spec/sample/tax/looks_up_tax_percentage_action.rb +27 -0
  101. data/spec/sample/tax/provides_free_shipping_action.rb +10 -0
  102. data/spec/spec_helper.rb +24 -0
  103. data/spec/support.rb +1 -0
  104. data/spec/test_doubles.rb +552 -0
  105. data/spec/testing/context_factory/iterate_spec.rb +39 -0
  106. data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
  107. data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
  108. data/spec/testing/context_factory/with_callback_spec.rb +38 -0
  109. data/spec/testing/context_factory_spec.rb +55 -0
  110. metadata +285 -0
data/README.md ADDED
@@ -0,0 +1,1424 @@
1
+ # FunctionalLightService
2
+ [![Gem Version](https://img.shields.io/gem/v/light-service.svg)](https://rubygems.org/gems/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).