functional-light-service 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.rubocop.yml +72 -0
- data/.travis.yml +22 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +1424 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/functional-light-service.gemspec +26 -0
- data/gemfiles/activesupport_5.gemfile +8 -0
- data/gemfiles/activesupport_5.gemfile.lock +82 -0
- data/lib/functional-light-service/action.rb +102 -0
- data/lib/functional-light-service/configuration.rb +24 -0
- data/lib/functional-light-service/context/key_verifier.rb +118 -0
- data/lib/functional-light-service/context.rb +165 -0
- data/lib/functional-light-service/errors.rb +6 -0
- data/lib/functional-light-service/functional/enum.rb +254 -0
- data/lib/functional-light-service/functional/maybe.rb +14 -0
- data/lib/functional-light-service/functional/monad.rb +66 -0
- data/lib/functional-light-service/functional/null.rb +74 -0
- data/lib/functional-light-service/functional/option.rb +99 -0
- data/lib/functional-light-service/functional/result.rb +122 -0
- data/lib/functional-light-service/localization_adapter.rb +44 -0
- data/lib/functional-light-service/organizer/execute.rb +14 -0
- data/lib/functional-light-service/organizer/iterate.rb +22 -0
- data/lib/functional-light-service/organizer/reduce_if.rb +17 -0
- data/lib/functional-light-service/organizer/reduce_until.rb +20 -0
- data/lib/functional-light-service/organizer/scoped_reducable.rb +13 -0
- data/lib/functional-light-service/organizer/verify_call_method_exists.rb +28 -0
- data/lib/functional-light-service/organizer/with_callback.rb +26 -0
- data/lib/functional-light-service/organizer/with_reducer.rb +71 -0
- data/lib/functional-light-service/organizer/with_reducer_factory.rb +18 -0
- data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +105 -0
- data/lib/functional-light-service/organizer.rb +105 -0
- data/lib/functional-light-service/testing/context_factory.rb +40 -0
- data/lib/functional-light-service/testing.rb +1 -0
- data/lib/functional-light-service/version.rb +3 -0
- data/lib/functional-light-service.rb +29 -0
- data/resources/fail_actions.png +0 -0
- data/resources/light-service.png +0 -0
- data/resources/organizer_and_actions.png +0 -0
- data/resources/skip_actions.png +0 -0
- data/spec/acceptance/add_numbers_spec.rb +11 -0
- data/spec/acceptance/after_actions_spec.rb +71 -0
- data/spec/acceptance/around_each_spec.rb +19 -0
- data/spec/acceptance/before_actions_spec.rb +98 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
- data/spec/acceptance/fail_spec.rb +24 -0
- data/spec/acceptance/include_warning_spec.rb +29 -0
- data/spec/acceptance/log_from_organizer_spec.rb +154 -0
- data/spec/acceptance/message_localization_spec.rb +118 -0
- data/spec/acceptance/not_having_call_method_warning_spec.rb +39 -0
- data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +65 -0
- data/spec/acceptance/organizer/execute_spec.rb +46 -0
- data/spec/acceptance/organizer/iterate_spec.rb +37 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +51 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +43 -0
- data/spec/acceptance/organizer/with_callback_spec.rb +110 -0
- data/spec/acceptance/rollback_spec.rb +132 -0
- data/spec/acceptance/skip_all_warning_spec.rb +20 -0
- data/spec/acceptance/testing/context_factory_spec.rb +54 -0
- data/spec/action_expected_keys_spec.rb +63 -0
- data/spec/action_expects_and_promises_spec.rb +93 -0
- data/spec/action_promised_keys_spec.rb +122 -0
- data/spec/action_spec.rb +89 -0
- data/spec/context/inspect_spec.rb +57 -0
- data/spec/context_spec.rb +197 -0
- data/spec/examples/amount_spec.rb +77 -0
- data/spec/examples/controller_spec.rb +63 -0
- data/spec/examples/validate_address_spec.rb +37 -0
- data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
- data/spec/lib/deterministic/currify_spec.rb +88 -0
- data/spec/lib/deterministic/monad_axioms.rb +44 -0
- data/spec/lib/deterministic/monad_spec.rb +45 -0
- data/spec/lib/deterministic/null_spec.rb +58 -0
- data/spec/lib/deterministic/option_spec.rb +133 -0
- data/spec/lib/deterministic/result/failure_spec.rb +65 -0
- data/spec/lib/deterministic/result/result_map_spec.rb +154 -0
- data/spec/lib/deterministic/result/result_shared.rb +24 -0
- data/spec/lib/deterministic/result/success_spec.rb +41 -0
- data/spec/lib/deterministic/result_spec.rb +63 -0
- data/spec/lib/enum_spec.rb +112 -0
- data/spec/localization_adapter_spec.rb +83 -0
- data/spec/organizer/with_reducer_spec.rb +56 -0
- data/spec/organizer_key_aliases_spec.rb +29 -0
- data/spec/organizer_spec.rb +93 -0
- data/spec/readme_spec.rb +47 -0
- data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
- data/spec/sample/calculates_tax_spec.rb +30 -0
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +53 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
- data/spec/sample/tax/calculates_order_tax_action.rb +9 -0
- data/spec/sample/tax/calculates_tax.rb +11 -0
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +27 -0
- data/spec/sample/tax/provides_free_shipping_action.rb +10 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support.rb +1 -0
- data/spec/test_doubles.rb +552 -0
- data/spec/testing/context_factory/iterate_spec.rb +39 -0
- data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
- data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
- data/spec/testing/context_factory/with_callback_spec.rb +38 -0
- data/spec/testing/context_factory_spec.rb +55 -0
- 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).
|