light-service 0.13.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +28 -0
  3. data/.travis.yml +3 -9
  4. data/Appraisals +0 -4
  5. data/Gemfile +0 -2
  6. data/README.md +287 -51
  7. data/RELEASES.md +21 -2
  8. data/gemfiles/activesupport_5.gemfile +0 -1
  9. data/gemfiles/activesupport_6.gemfile +0 -1
  10. data/lib/generators/light_service/action_generator.rb +90 -0
  11. data/lib/generators/light_service/generator_utils.rb +45 -0
  12. data/lib/generators/light_service/organizer_generator.rb +66 -0
  13. data/lib/generators/light_service/templates/action_spec_template.erb +31 -0
  14. data/lib/generators/light_service/templates/action_template.erb +30 -0
  15. data/lib/generators/light_service/templates/organizer_spec_template.erb +20 -0
  16. data/lib/generators/light_service/templates/organizer_template.erb +22 -0
  17. data/lib/light-service/action.rb +61 -4
  18. data/lib/light-service/context/key_verifier.rb +18 -1
  19. data/lib/light-service/context.rb +5 -3
  20. data/lib/light-service/errors.rb +1 -0
  21. data/lib/light-service/organizer/reduce_if_else.rb +21 -0
  22. data/lib/light-service/organizer/with_reducer.rb +12 -7
  23. data/lib/light-service/organizer/with_reducer_factory.rb +1 -1
  24. data/lib/light-service/organizer/with_reducer_log_decorator.rb +3 -0
  25. data/lib/light-service/organizer.rb +16 -3
  26. data/lib/light-service/version.rb +1 -1
  27. data/lib/light-service.rb +1 -0
  28. data/light-service.gemspec +6 -1
  29. data/spec/acceptance/after_actions_spec.rb +17 -0
  30. data/spec/acceptance/around_each_spec.rb +15 -0
  31. data/spec/acceptance/log_from_organizer_spec.rb +1 -1
  32. data/spec/acceptance/organizer/add_to_context_spec.rb +27 -0
  33. data/spec/acceptance/organizer/execute_with_add_to_context_spec.rb +28 -0
  34. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  35. data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
  36. data/spec/acceptance/organizer/reduce_if_spec.rb +6 -0
  37. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  38. data/spec/action_optional_expected_keys_spec.rb +82 -0
  39. data/spec/action_spec.rb +8 -0
  40. data/spec/lib/generators/action_generator_advanced_spec.rb +43 -0
  41. data/spec/lib/generators/action_generator_simple_spec.rb +37 -0
  42. data/spec/lib/generators/full_generator_test_blobs.rb +193 -0
  43. data/spec/lib/generators/organizer_generator_advanced_spec.rb +37 -0
  44. data/spec/lib/generators/organizer_generator_simple_spec.rb +37 -0
  45. data/spec/organizer_spec.rb +5 -0
  46. data/spec/spec_helper.rb +5 -1
  47. data/spec/test_doubles.rb +37 -0
  48. metadata +87 -9
  49. data/gemfiles/activesupport_3.gemfile +0 -8
  50. data/gemfiles/activesupport_4.gemfile +0 -8
  51. data/resources/orchestrators_deprecated.svg +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e9dfb5cf61024afa28719b84e160c9580fa39c99eda19b73cdb0cced9017da2
4
- data.tar.gz: 7f389c208b3b06000c84859d595d9b49acf8dfe8d370736ee564a1ae8ef8b5d1
3
+ metadata.gz: b37602f04d4539ba8d8622f9e226c916071760ee37e21c7add4e1972f9c5175f
4
+ data.tar.gz: f55175d1a477c78896522f4bdca07697ceb80c7b62fca80e4c6803ff06bb2ec7
5
5
  SHA512:
6
- metadata.gz: b1b7792d344efe1aab4297f8774649590b8e0336a44082da31a167a62b75a039b7f89759923beb9a463f6c017b0387297efc815f357deae31270b54e2c1c1bcc
7
- data.tar.gz: 0d1d339a23d2d7b39bd5403d81fe1e1519a89eec22fd64f00931d9dc49a81c5476f67e6dd28591e0e88c55b1378d1155ed0d1700100569aa9b43a1922c860f3e
6
+ metadata.gz: d087df4f8d16b2e4f2611ac9e026906d2c7bb40cbe19b31052a71ed87e7ed5d70016bfc6ccfff363a4a04df7e9ff676c495d5246a7f69a142897cede456e99f4
7
+ data.tar.gz: 2317cff4254ae4bb6a20f481f042e2e63785fa015f130e4d01acb799f65e1e1db5f1f8fe1ebe9fdc973617925c456f8e4d024fe20ba0af98204a65a0c0f81b78
@@ -0,0 +1,28 @@
1
+ name: CI Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu, macos]
16
+ ruby: [2.5.3, 2.6.6, 2.7.2]
17
+ gemfile: [activesupport_5, activesupport_6]
18
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
19
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
20
+ BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
21
+ steps:
22
+ - uses: actions/checkout@v2
23
+ - uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ - run: bundle install
27
+ - run: bundle exec rspec spec
28
+ - run: bundle exec rubocop
data/.travis.yml CHANGED
@@ -4,10 +4,9 @@ env:
4
4
  - RUN_COVERAGE_REPORT=true
5
5
 
6
6
  rvm:
7
- - 2.4.2
8
7
  - 2.5.3
9
- - 2.6.0
10
- - 2.7.0
8
+ - 2.6.6
9
+ - 2.7.2
11
10
 
12
11
  before_install:
13
12
  - 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc'
@@ -20,16 +19,11 @@ script:
20
19
  - bundle exec rubocop
21
20
 
22
21
  gemfile:
23
- - gemfiles/activesupport_3.gemfile
24
22
  - gemfiles/activesupport_4.gemfile
25
23
  - gemfiles/activesupport_5.gemfile
26
24
  - gemfiles/activesupport_6.gemfile
27
25
 
28
26
  matrix:
29
27
  exclude:
30
- - rvm: 2.4.2
31
- gemfile: gemfiles/activesupport_6.gemfile
32
- - rvm: 2.7.0
33
- gemfile: gemfiles/activesupport_3.gemfile
34
- - rvm: 2.7.0
28
+ - rvm: 2.7.2
35
29
  gemfile: gemfiles/activesupport_4.gemfile
data/Appraisals CHANGED
@@ -1,7 +1,3 @@
1
- appraise "activesupport-3" do
2
- gem "activesupport", "~> 3.0"
3
- end
4
-
5
1
  appraise "activesupport-4" do
6
2
  gem "activesupport", "~> 4.0"
7
3
  end
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in light_service.gemspec
4
4
  gemspec
5
-
6
- gem 'appraisal', '~> 2.0'
data/README.md CHANGED
@@ -1,33 +1,38 @@
1
1
  ![LightService](https://raw.githubusercontent.com/adomokos/light-service/master/resources/light-service.png)
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/light-service.svg)](https://rubygems.org/gems/light-service)
4
- [![Build Status](https://secure.travis-ci.org/adomokos/light-service.svg)](http://travis-ci.org/adomokos/light-service)
4
+ [![CI Tests](https://github.com/adomokos/light-service/actions/workflows/project-build.yml/badge.svg)](https://github.com/adomokos/light-service/actions/workflows/project-build.yml)
5
+ [![codecov](https://codecov.io/gh/adomokos/light-service/branch/master/graph/badge.svg)](https://codecov.io/gh/adomokos/light-service)
5
6
  [![Code Climate](https://codeclimate.com/github/adomokos/light-service.svg)](https://codeclimate.com/github/adomokos/light-service)
6
7
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT)
7
8
  [![Download Count](https://ruby-gem-downloads-badge.herokuapp.com/light-service?type=total)](https://rubygems.org/gems/light-service)
8
9
 
9
- <br><br>
10
+ LightService is a powerful and flexible service skeleton framework with an emphasis on simplicity
10
11
 
11
- ![Orchestrators-Deprecated](resources/orchestrators_deprecated.svg)
12
- <br>Version 0.9.0 deprecates Orchestrators and moves all their functionalities into Organizers. Please check out [this PR](https://github.com/adomokos/light-service/pull/132) to see the changes.
13
-
14
- <br>
15
-
16
- ## Table of Content
12
+ ## Table of Contents
17
13
  * [Why LightService?](#why-lightservice)
14
+ * [Getting Started](#getting-started)
15
+ * [Requirements](#requirements)
16
+ * [Installation](#installation)
17
+ * [Your first action](#your-first-action)
18
+ * [Your first organizer](#your-first-organizer)
18
19
  * [Stopping the Series of Actions](#stopping-the-series-of-actions)
19
20
  * [Failing the Context](#failing-the-context)
20
21
  * [Skipping the Rest of the Actions](#skipping-the-rest-of-the-actions)
21
22
  * [Benchmarking Actions with Around Advice](#benchmarking-actions-with-around-advice)
22
23
  * [Before and After Action Hooks](#before-and-after-action-hooks)
24
+ * [Expects and Promises](#expects-and-promises)
25
+ * [Default values for optional Expected keys](#default-values-for-optional-expected-keys)
23
26
  * [Key Aliases](#key-aliases)
24
27
  * [Logging](#logging)
25
28
  * [Error Codes](#error-codes)
26
29
  * [Action Rollback](#action-rollback)
27
30
  * [Localizing Messages](#localizing-messages)
28
- * [Orchestrator Logic in Organizers](#orchestrator-logic-in-organizers)
31
+ * [Orchestrating Logic in Organizers](#orchestrating-logic-in-organizers)
29
32
  * [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
30
-
33
+ * [Rails support](#rails-support)
34
+ * [Implementations in other languages](#other-implementations)
35
+ * [Contributing](#contributing)
31
36
 
32
37
  ## Why LightService?
33
38
 
@@ -175,7 +180,143 @@ end
175
180
  I gave a [talk at RailsConf 2013](http://www.adomokos.com/2013/06/simple-and-elegant-rails-code-with.html) on
176
181
  simple and elegant Rails code where I told the story of how LightService was extracted from the projects I had worked on.
177
182
 
183
+ ## Getting started
184
+
185
+ ### Requirements
186
+
187
+ This gem requires ruby 2.x. Use of [generators](#rails-support) requires Rails 5+ (tested on Rails 5.x & 6.x only. Will probably work on
188
+ Rails versions as old as 3.2)
189
+
190
+ ### Installation
191
+
192
+ In your Gemfile:
193
+
194
+ ```ruby
195
+ gem 'light-service'
196
+ ```
197
+
198
+ And then
199
+
200
+ ```shell
201
+ bundle install
202
+ ```
203
+
204
+ Or install it yourself as:
205
+
206
+ ```shell
207
+ gem install light-service
208
+ ```
209
+
210
+ ### Your first action
211
+
212
+ LightService's building blocks are actions that are normally composed within an organizer, but can be run independently.
213
+ Let's make a simple greeter action. Each action can take an optional list of expected inputs and promised outputs. If
214
+ these are specified and missing at action start and stop respectively, an exception will be thrown.
215
+
216
+ ```ruby
217
+ class GreetsPerson
218
+ extend ::LightService::Action
219
+
220
+ expects :name
221
+ promises :greeting
222
+
223
+ executed do |context|
224
+ context.greeting = "Hey there, #{name}. You enjoying LightService so far?"
225
+ end
226
+ end
227
+ ```
228
+
229
+ When an action is run, you have access to its returned context, and the status of the action. You can invoke an
230
+ action by calling `.execute` on its class with `key: value` arguments, and inspect its status and context like so:
231
+
232
+ ```ruby
233
+ outcome = GreetsPerson.execute(name: "Han")
234
+
235
+ if outcome.success?
236
+ puts outcome.greeting # which was a promised context value
237
+ elsif outcome.failure?
238
+ puts "Rats... I can't say hello to you"
239
+ end
240
+ ```
241
+
242
+ You will notice that actions are set up to promote simplicity, i.e. they either succeed or fail, and they have
243
+ very clear inputs and outputs. Ideally, they should do [exactly one thing](https://en.wikipedia.org/wiki/Single-responsibility_principle). This makes them as easy to test as unit tests.
244
+
245
+ ### Your first organizer
246
+
247
+ LightService provides a facility to compose actions using organizers. This is great when you have a business process
248
+ to execute that has multiple steps. By composing actions that do exactly one thing, you can sequence simple
249
+ actions together to perform complex multi-step business processes in a clear manner that is very easy
250
+ to reason about.
251
+
252
+ There are advanced ways to sequence actions that can be found later in the README, but we'll keep this simple for now.
253
+ First, let's add a second action that we can sequence to run after the `GreetsPerson` action from above:
254
+
255
+ ```ruby
256
+ class RandomlyAwardsPrize
257
+ extend ::LightService::Action
258
+
259
+ expects :name, :greeting
260
+ promises :did_i_win
261
+
262
+ executed do |context|
263
+ prize_num = "#{context.name}__#{context.greeting}".length
264
+ prizes = ["jelly beans", "ice cream", "pie"]
265
+ did_i_win = rand((1..prize_num)) % 7 == 0
266
+ did_i_lose = rand((1..prize_num)) % 13 == 0
267
+
268
+ if did_i_lose
269
+ # When failing, send a message as an argument, readable from the return context
270
+ context.fail!("you are exceptionally unlucky")
271
+ else
272
+ # You can specify 'optional' context items by treating context like a hash.
273
+ # Useful for when you may or may not be returning extra data. Ideally, selecting
274
+ # a prize should be a separate action that is only run if you win.
275
+ context[:prize] = "lifetime supply of #{prizes.sample}" if did_i_win
276
+ context.did_i_win = did_i_win
277
+ end
278
+ end
279
+ end
280
+ ```
281
+
282
+ And here's the organizer that ties the two together. You implement a `call` class method that takes some arguments and
283
+ from there sends them to `with` in `key: value` format which forms the initial state of the context. From there, chain
284
+ `reduce` to `with` and send it a list of action class names in sequence. The organizer will call each action, one
285
+ after the other, and build up the context as it goes along.
286
+
287
+ ```ruby
288
+ class WelcomeAPotentiallyLuckyPerson
289
+ extend LightService::Organizer
290
+
291
+ def self.call(name)
292
+ with(:name => name).reduce(GreetsPerson, RandomlyAwardsPrize)
293
+ end
294
+ end
295
+ ```
296
+
297
+ When an organizer is run, you have access to the context as it passed through all actions, and the overall status
298
+ of the organized execution. You can invoke an organizer by calling `.call` on the class with the expected arguments,
299
+ and inspect its status and context just like you would an action:
178
300
 
301
+ ```ruby
302
+ outcome = WelcomeAPotentiallyLuckyPerson.call("Han")
303
+
304
+ if outcome.success?
305
+ puts outcome.greeting # which was a promised context value
306
+
307
+ if outcome.did_i_win
308
+ puts "And you've won a prize! Lucky you. Please see the front desk for your #{outcome.prize}."
309
+ end
310
+ else # outcome.failure? is true, and we can pull the failure message out of the context for feedback to the user.
311
+ puts "Rats... I can't say hello to you, because #{outcome.message}."
312
+ end
313
+ ```
314
+
315
+ Because organizers generally run through complex business logic, and every action has the potential to cause a failure,
316
+ testing an organizer is functionally equivalent to an integration test.
317
+
318
+ For further examples, please visit the project's [Wiki](https://github.com/adomokos/light-service/wiki) and review
319
+ the ["Why LightService" section](#why-lightservice) above.
179
320
 
180
321
  ## Stopping the Series of Actions
181
322
  When nothing unexpected happens during the organizer's call, the returned `context` will be successful. Here is how you can check for this:
@@ -390,9 +531,11 @@ These ideas are originally from Aspect Oriented Programming, read more about the
390
531
  ## Expects and Promises
391
532
  The `expects` and `promises` macros are rules for the inputs/outputs of an action.
392
533
  `expects` describes what keys it needs to execute, and `promises` makes sure the keys are in the context after the
393
- action is reduced. If either of them are violated, a custom exception is thrown.
534
+ action is reduced. If either of them are violated, a `LightService::ExpectedKeysNotInContextError` or
535
+ `LightService::PromisedKeysNotInContextError` exception respectively will be thrown.
394
536
 
395
537
  This is how it's used:
538
+
396
539
  ```ruby
397
540
  class FooAction
398
541
  extend LightService::Action
@@ -400,46 +543,78 @@ class FooAction
400
543
  promises :bar
401
544
 
402
545
  executed do |context|
403
- baz = context.fetch :baz
404
-
405
- bar = baz + 2
406
- context[:bar] = bar
546
+ context.bar = context.baz + 2
407
547
  end
408
548
  end
409
549
  ```
410
550
 
411
- The `expects` macro does a bit more for you: it pulls the value with the expected key from the context, and
412
- makes it available to you through a reader. You can refactor the action like this:
551
+ The `expects` macro will pull the value with the expected key from the context, and
552
+ makes it available to you through a reader.
553
+
554
+ The `promises` macro will not only check if the context has the promised keys, it
555
+ also sets them for you in the context if you use the accessor with the same name,
556
+ much the same way as the expects macro works.
557
+
558
+ The context object is essentially a smarter-than-normal Hash. Take a look at [this spec](spec/action_expects_and_promises_spec.rb)
559
+ to see expects and promises used with and without accessors.
560
+
561
+ ### Default values for optional Expected keys
562
+
563
+ When you have an expected key that has a sensible default which should be used everywhere and
564
+ only overridden on an as-needed basis, you can specify a default value. An example use-case
565
+ is a flag that allows a failure from a service under most circumstances to avoid failing an
566
+ entire workflow because of a non-critical action.
567
+
568
+ LightService provides two mechanisms for specifying default values:
569
+
570
+ 1. A static value that is used as-is
571
+ 2. A callable that takes the current context as a param
572
+
573
+ Using the above use case, consider an action that sends a text message. In most cases,
574
+ if there is a problem sending the text message, it might be OK for it to fail. We will
575
+ `expect` an `allow_failure` key, but set it with a default, like so:
413
576
 
414
577
  ```ruby
415
- class FooAction
578
+ class SendSMS
416
579
  extend LightService::Action
417
- expects :baz
418
- promises :bar
580
+ expects :message, :user
581
+ expects :allow_failure, default: true
419
582
 
420
583
  executed do |context|
421
- bar = context.baz + 2
422
- context[:bar] = bar
584
+ sms_api = SMSService.new(key: ENV["SMS_API_KEY"])
585
+ status = sms_api.send(ctx.user.mobile_number, ctx.message)
586
+
587
+ if !status.sent_ok?
588
+ ctx.fail!(status.err_msg) unless ctx.allow_failure
589
+ end
423
590
  end
424
591
  end
425
592
  ```
426
593
 
427
- The `promises` macro will not only check if the context has the promised keys, it also sets it for you in the context if
428
- you use the accessor with the same name. The code above can be further simplified:
594
+ Default values can also be processed dynamically by providing a callable. Any values already
595
+ specified in the context are available to it via Hash key lookup syntax. e.g.
429
596
 
430
597
  ```ruby
431
- class FooAction
598
+ class SendSMS
432
599
  extend LightService::Action
433
- expects :baz
434
- promises :bar
600
+ expects :message, :user
601
+ expects :allow_failure, default: ->(ctx) { !ctx[:user].admin? } # Admins must always get SMS'
435
602
 
436
603
  executed do |context|
437
- context.bar = context.baz + 2
604
+ sms_api = SMSService.new(key: ENV["SMS_API_KEY"])
605
+ status = sms_api.send(ctx.user.mobile_number, ctx.message)
606
+
607
+ if !status.sent_ok?
608
+ ctx.fail!(status.err_msg) unless ctx.allow_failure
609
+ end
438
610
  end
439
611
  end
440
612
  ```
441
613
 
442
- Take a look at [this spec](spec/action_expects_and_promises_spec.rb) to see the refactoring in action.
614
+ **Note** that default values must be specified one at a time on their own line.
615
+
616
+ You can then call an action or organizer that uses an action with defaults without specifying
617
+ the expected key that has a default.
443
618
 
444
619
  ## Key Aliases
445
620
  The `aliases` macro sets up pairs of keys and aliases in an organizer. Actions can access the context using the aliases.
@@ -624,7 +799,28 @@ Using the `rolled_back` macro is optional for the actions in the chain. You shou
624
799
 
625
800
  The actions are rolled back in reversed order from the point of failure starting with the action that triggered it.
626
801
 
627
- See [this](spec/acceptance/rollback_spec.rb) acceptance test to learn more about this functionality.
802
+ See [this acceptance test](spec/acceptance/rollback_spec.rb) to learn more about this functionality.
803
+
804
+ You may find yourself directly using an action that can roll back by calling `.execute` instead of using it from within an Organizer.
805
+ If this action fails and attempts a rollback, a `FailWithRollbackError` exception will be raised. This is so that the organizer can
806
+ rollback the actions one by one. If you don't want to wrap your call to the action with a `begin, rescue FailWithRollbackError`
807
+ block, you can introspect the context like so, and keep your usage of the action clean:
808
+
809
+ ```ruby
810
+ class FooAction
811
+ extend LightService::Action
812
+
813
+ executed do |context|
814
+ # context.organized_by will be nil if run from an action,
815
+ # or will be the class name if run from an organizer
816
+ if context.organized_by.nil?
817
+ context.fail!
818
+ else
819
+ context.fail_with_rollback!
820
+ end
821
+ end
822
+ end
823
+ ```
628
824
 
629
825
  ## Localizing Messages
630
826
  By default LightService 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.
@@ -703,9 +899,13 @@ end
703
899
 
704
900
  To get the value of a `fail!` or `succeed!` message, simply call `#message` on the returned context.
705
901
 
706
- ## Orchestrator Logic in Organizers
902
+ ## Orchestrating Logic in Organizers
903
+
904
+ The Organizer - Action combination works really well for simple use cases. However, as business logic gets more complex, or when LightService is used in an ETL workflow, the code that routes the different organizers becomes very complex and imperative.
905
+
906
+ In the past, this was solved using Orchestrators. As of [Version 0.9.0 Orchestrators have been deprecated](https://github.com/adomokos/light-service/pull/132). All their functionality is now usable directly within Organizers. Read on to understand how to orchestrate workflows from within a single Organizer.
707
907
 
708
- The Organizer - Action combination works really well for simple use cases. However, as business logic gets more complex, or when LightService 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:
908
+ Let's look at a piece of code that does basic data transformations:
709
909
 
710
910
  ```ruby
711
911
  class ExtractsTransformsLoadsData
@@ -763,23 +963,26 @@ The 7 different orchestrator constructs an organizer can have:
763
963
 
764
964
  1. `reduce_until`
765
965
  2. `reduce_if`
766
- 3. `iterate`
767
- 4. `execute`
768
- 5. `with_callback`
769
- 6. `add_to_context`
770
- 7. `add_aliases`
966
+ 3. `reduce_if_else`
967
+ 4. `iterate`
968
+ 5. `execute`
969
+ 6. `with_callback`
970
+ 7. `add_to_context`
971
+ 8. `add_aliases`
771
972
 
772
973
  `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.
773
974
 
774
975
  `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.
775
976
 
977
+ `reduce_if_else` takes three arguments, a condition lambda, a first set of "if true" steps, and a second set of "if false" steps. If the lambda evaluates to true, the "if true" steps are executed, otherwise the "else steps" are executed. [This acceptance test](spec/acceptance/organizer/reduce_if_else_spec.rb) describes this functionality.
978
+
776
979
  `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.
777
980
 
778
981
  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.
779
982
 
780
983
  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.
781
984
 
782
- `add_to_context` can add key-value pairs on the fly to the context. This functionality is useful when you need a value injected into the context under a specific key right before the subsequent actions are executed. [This test](spec/acceptance/organizer/add_to_context_spec.rb) describes its functionality.
985
+ `add_to_context` can add key-value pairs on the fly to the context. This functionality is useful when you need a value injected into the context under a specific key right before the subsequent actions are executed. Keys are also made available as accessors on the context object and can be used just like methods exposed via `expects` and `promises`. [This test](spec/acceptance/organizer/add_to_context_spec.rb) describes its functionality.
783
986
 
784
987
  Your action needs a certain key in the context but it's under a different one? Use the function `add_aliases` to alias an existing key in the context under the desired key. Take a look at [this test](spec/acceptance/organizer/add_aliases_spec.rb) to see an example.
785
988
 
@@ -836,28 +1039,61 @@ This context then can be passed to the action under test, freeing you up from th
836
1039
 
837
1040
  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.
838
1041
 
839
- ## Requirements
1042
+ ## Rails support
1043
+
1044
+ LightService includes Rails generators for creating both Organizers and Actions along with corresponding tests. Currently only RSpec is
1045
+ supported ([PR's for supporting MiniTest are welcome](https://github.com/adomokos/light-service/pulls))
840
1046
 
841
- This gem requires ruby 2.x
1047
+ Note: Generators are namespaced to `light_service` not `light-service` due to Rake name constraints.
842
1048
 
843
- ## Installation
844
- Add this line to your application's Gemfile:
1049
+ ### Organizer generation
845
1050
 
846
- gem 'light-service'
1051
+ ```shell
1052
+ rails generate light_service:organizer My::SuperFancy::Organizer
1053
+ # -- or
1054
+ rails generate light_service:organizer my/super_fancy/organizer
1055
+ ```
847
1056
 
848
- And then execute:
1057
+ Options for this generator are:
849
1058
 
850
- $ bundle
1059
+ * `--dir=<SOME_DIR>`. `<SOME_DIR>` defaults to `organizers`. Will write organizers to `/app/organizers`, and specs to `/spec/organizers`
1060
+ * `--no-tests`. Default is `--tests`. Will generate a test file matching the namespace you've supplied.
851
1061
 
852
- Or install it yourself as:
1062
+ ### Action generation
1063
+
1064
+ ```shell
1065
+ rails generate light_service:action My::SuperFancy::Action
1066
+ # -- or
1067
+ rails generate light_service:action my/super_fancy/action
1068
+ ```
1069
+
1070
+ Options for this generator are:
1071
+
1072
+ * `--dir=<SOME_DIR>`. `<SOME_DIR>` defaults to `actions`. Will write actions to `/app/actions`, and specs to `/spec/actions`
1073
+ * `--no-tests`. Defaults is `--tests`. Will generate a test file matching the namespace you've supplied.
1074
+ * `--no-roll-back`. Default is `--roll-back`. Will generate a `rolled_back` block for you to implement with [roll back functionality](#action-rollback).
1075
+
1076
+ ### Advanced action generation
1077
+
1078
+ You are able to optionally specify `expects` and/or `promises` keys during generation
1079
+
1080
+ ```shell
1081
+ rails generate light_service:action CrankWidget expects:one_fish,two_fish promises:red_fish,blue_fish
1082
+ ```
1083
+
1084
+ When specifying `expects`, convenience variables will be initialized in the `executed` block so that you don't have to call
1085
+ them through the context. A stub context will be created in the test file using these keys too.
1086
+
1087
+ When specifying `promises`, specs will be created testing for their existence after executing the action.
853
1088
 
854
- $ gem install light-service
1089
+ ## Other implementations
855
1090
 
856
- ## Usage
857
- Based on the refactoring example above, just create an organizer object that calls the
858
- actions in order and write code for the actions. That's it.
1091
+ | Language | Repo | Author |
1092
+ | :--------- |:------------------------------------------------------------------------| :------------------------------------------------------|
1093
+ | Python | [pyservice](https://github.com/adomokos/pyservice) | [@adomokos](https://github.com/adomokos) |
1094
+ | PHP | [light-service](https://github.com/douglasgreyling/light-service) | [@douglasgreyling](https://github.com/douglasgreyling) |
1095
+ | JavaScript | [light-service.js](https://github.com/douglasgreyling/light-service.js) | [@douglasgreyling](https://github.com/douglasgreyling) |
859
1096
 
860
- For further examples, please visit the project's [Wiki](https://github.com/adomokos/light-service/wiki).
861
1097
 
862
1098
  ## Contributing
863
1099
  1. Fork it