light-service 0.15.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +28 -0
  3. data/.rubocop.yml +117 -3
  4. data/.travis.yml +3 -8
  5. data/README.md +89 -37
  6. data/RELEASES.md +18 -1
  7. data/lib/generators/light_service/generator_utils.rb +0 -2
  8. data/lib/light-service/action.rb +61 -4
  9. data/lib/light-service/context/key_verifier.rb +22 -3
  10. data/lib/light-service/context.rb +10 -12
  11. data/lib/light-service/errors.rb +5 -0
  12. data/lib/light-service/orchestrator.rb +1 -1
  13. data/lib/light-service/organizer/reduce_case.rb +48 -0
  14. data/lib/light-service/organizer/reduce_if_else.rb +21 -0
  15. data/lib/light-service/organizer/with_reducer.rb +11 -14
  16. data/lib/light-service/organizer/with_reducer_log_decorator.rb +2 -2
  17. data/lib/light-service/organizer.rb +20 -3
  18. data/lib/light-service/testing/context_factory.rb +2 -0
  19. data/lib/light-service/version.rb +1 -1
  20. data/lib/light-service.rb +2 -0
  21. data/light-service.gemspec +3 -2
  22. data/spec/acceptance/after_actions_spec.rb +17 -6
  23. data/spec/acceptance/around_each_spec.rb +15 -0
  24. data/spec/acceptance/before_actions_spec.rb +3 -9
  25. data/spec/acceptance/log_from_organizer_spec.rb +1 -1
  26. data/spec/acceptance/organizer/add_to_context_spec.rb +27 -0
  27. data/spec/acceptance/organizer/execute_with_add_to_context_spec.rb +28 -0
  28. data/spec/acceptance/organizer/reduce_case_spec.rb +53 -0
  29. data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
  30. data/spec/acceptance/organizer/reduce_if_spec.rb +2 -0
  31. data/spec/action_optional_expected_keys_spec.rb +82 -0
  32. data/spec/context/inspect_spec.rb +5 -21
  33. data/spec/context_spec.rb +1 -1
  34. data/spec/lib/generators/action_generator_advanced_spec.rb +1 -1
  35. data/spec/lib/generators/action_generator_simple_spec.rb +1 -1
  36. data/spec/lib/generators/organizer_generator_advanced_spec.rb +1 -1
  37. data/spec/lib/generators/organizer_generator_simple_spec.rb +1 -1
  38. data/spec/sample/calculates_tax_spec.rb +0 -1
  39. data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
  40. data/spec/test_doubles.rb +48 -5
  41. metadata +22 -13
  42. data/gemfiles/activesupport_4.gemfile +0 -7
  43. data/resources/orchestrators_deprecated.svg +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f400829da41766f56e7f0cb9cfe95aa028ef8ab34bf0bc5794bfafcff3cdcfb1
4
- data.tar.gz: 41dbd504ac39a0804eb445b4af086dbcf9986264077994277e17fde98b7bdc3d
3
+ metadata.gz: d713cae791654b29dfc20e3cbffe16094badcc49b8221ccff0765dbd479b40c2
4
+ data.tar.gz: 1e2d61c708b70466a904f8e07aa4ba4aa383bd405342efeb51014286209b2ebb
5
5
  SHA512:
6
- metadata.gz: e9a1148b82b6e1a0813400b0d23b013ff6f06965f3204bb3fa95101adc8a749223dd334887f7543622b4a829fd17f190c21dd5f601df3aecde8d8451459a76af
7
- data.tar.gz: f0aec690e94896dcc13d6caa774003e49ddfa6b74c5acf9272fb7b9d3710d28db4a29601e725e3729deba820013c62b740739d5c6052c4c5ae93fd885cec6fda
6
+ metadata.gz: c1d3f210ecbc0430bae9f573a945d599fcbf36a4acf26f5a49b9997c735dea8eba14f367e7c7a639d5cb8ff458feb5945ecd0078986140f763bb17b967947389
7
+ data.tar.gz: d2715e9fe494e4163efd52b4e2ce32011c3e275e8bdd1ba45e8ef1c2aff8705f2d2c994e02de0b8e20896ea5067ec75fb6a7ca333ac7f9b333544fcd82d2c7ca
@@ -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.7.4, 3.0.2, 3.1.0]
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/.rubocop.yml CHANGED
@@ -1,7 +1,7 @@
1
- require: rubocop-performance
1
+ # require: rubocop-performance
2
2
 
3
3
  AllCops:
4
- TargetRubyVersion: 2.3
4
+ TargetRubyVersion: 2.7
5
5
  Exclude:
6
6
  - 'lib/light-service.rb'
7
7
  - 'vendor/bundle/**/*'
@@ -47,8 +47,122 @@ Metrics/BlockLength:
47
47
  Exclude:
48
48
  - 'spec/**/*.rb'
49
49
 
50
- Layout/TrailingBlankLines:
50
+ Layout/TrailingEmptyLines:
51
51
  Enabled: false
52
52
 
53
53
  Layout/EndOfLine:
54
54
  EnforcedStyle: lf
55
+
56
+ Lint/ConstantDefinitionInBlock:
57
+ Exclude:
58
+ - 'spec/**/*.rb'
59
+
60
+ Lint/EmptyClass:
61
+ Exclude:
62
+ - 'spec/**/*.rb'
63
+
64
+ # Defaults after the Rubocop upgrade
65
+ Gemspec/DateAssignment: # new in 1.10
66
+ Enabled: true
67
+ Layout/LineEndStringConcatenationIndentation: # new in 1.18
68
+ Enabled: true
69
+ Layout/SpaceBeforeBrackets: # new in 1.7
70
+ Enabled: true
71
+ Lint/AmbiguousAssignment: # new in 1.7
72
+ Enabled: true
73
+ Lint/AmbiguousOperatorPrecedence: # new in 1.21
74
+ Enabled: true
75
+ Lint/AmbiguousRange: # new in 1.19
76
+ Enabled: true
77
+ Lint/DeprecatedConstants: # new in 1.8
78
+ Enabled: true
79
+ Lint/DuplicateBranch: # new in 1.3
80
+ Enabled: true
81
+ Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
82
+ Enabled: true
83
+ Lint/EmptyBlock: # new in 1.1
84
+ Enabled: true
85
+ Lint/EmptyClass: # new in 1.3
86
+ Enabled: true
87
+ Lint/EmptyInPattern: # new in 1.16
88
+ Enabled: true
89
+ Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
90
+ Enabled: true
91
+ Lint/LambdaWithoutLiteralBlock: # new in 1.8
92
+ Enabled: true
93
+ Lint/NoReturnInBeginEndBlocks: # new in 1.2
94
+ Enabled: true
95
+ Lint/NumberedParameterAssignment: # new in 1.9
96
+ Enabled: true
97
+ Lint/OrAssignmentToConstant: # new in 1.9
98
+ Enabled: true
99
+ Lint/RedundantDirGlobSort: # new in 1.8
100
+ Enabled: true
101
+ Lint/SymbolConversion: # new in 1.9
102
+ Enabled: true
103
+ Lint/ToEnumArguments: # new in 1.1
104
+ Enabled: true
105
+ Lint/TripleQuotes: # new in 1.9
106
+ Enabled: true
107
+ Lint/UnexpectedBlockArity: # new in 1.5
108
+ Enabled: true
109
+ Lint/UnmodifiedReduceAccumulator: # new in 1.1
110
+ Enabled: true
111
+ Style/ArgumentsForwarding: # new in 1.1
112
+ Enabled: true
113
+ Style/CollectionCompact: # new in 1.2
114
+ Enabled: true
115
+ Style/DocumentDynamicEvalDefinition: # new in 1.1
116
+ Enabled: true
117
+ Style/EndlessMethod: # new in 1.8
118
+ Enabled: true
119
+ Style/HashConversion: # new in 1.10
120
+ Enabled: true
121
+ Style/HashExcept: # new in 1.7
122
+ Enabled: true
123
+ Style/IfWithBooleanLiteralBranches: # new in 1.9
124
+ Enabled: true
125
+ Style/InPatternThen: # new in 1.16
126
+ Enabled: true
127
+ Style/MultilineInPatternThen: # new in 1.16
128
+ Enabled: true
129
+ Style/NegatedIfElseCondition: # new in 1.2
130
+ Enabled: true
131
+ Style/NilLambda: # new in 1.3
132
+ Enabled: true
133
+ Style/QuotedSymbols: # new in 1.16
134
+ Enabled: true
135
+ Style/RedundantArgument: # new in 1.4
136
+ Enabled: true
137
+ Style/RedundantSelfAssignmentBranch: # new in 1.19
138
+ Enabled: true
139
+ Style/StringChars: # new in 1.12
140
+ Enabled: true
141
+ Style/SwapValues: # new in 1.1
142
+ Enabled: true
143
+ Gemspec/RequireMFA: # new in 1.23
144
+ Enabled: true
145
+ Lint/RequireRelativeSelfPath: # new in 1.22
146
+ Enabled: true
147
+ Lint/UselessRuby2Keywords: # new in 1.23
148
+ Enabled: true
149
+ Naming/BlockForwarding: # new in 1.24
150
+ Enabled: true
151
+ Security/IoMethods: # new in 1.22
152
+ Enabled: true
153
+ Style/FileRead: # new in 1.24
154
+ Enabled: true
155
+ Style/FileWrite: # new in 1.24
156
+ Enabled: true
157
+ Style/MapToHash: # new in 1.24
158
+ Enabled: true
159
+ Style/NestedFileDirname: # new in 1.26
160
+ Enabled: true
161
+ Style/NumberedParameters: # new in 1.22
162
+ Enabled: true
163
+ Style/NumberedParametersLimit: # new in 1.22
164
+ Enabled: true
165
+ Style/OpenStructUse: # new in 1.23
166
+ Enabled: true
167
+ Style/SelectByRegexp: # new in 1.22
168
+ Enabled: true
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'
@@ -26,9 +25,5 @@ gemfile:
26
25
 
27
26
  matrix:
28
27
  exclude:
29
- - rvm: 2.4.2
30
- gemfile: gemfiles/activesupport_6.gemfile
31
- - rvm: 2.7.0
32
- gemfile: gemfiles/activesupport_3.gemfile
33
- - rvm: 2.7.0
28
+ - rvm: 2.7.2
34
29
  gemfile: gemfiles/activesupport_4.gemfile
data/README.md CHANGED
@@ -1,20 +1,15 @@
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
5
  [![codecov](https://codecov.io/gh/adomokos/light-service/branch/master/graph/badge.svg)](https://codecov.io/gh/adomokos/light-service)
6
6
  [![Code Climate](https://codeclimate.com/github/adomokos/light-service.svg)](https://codeclimate.com/github/adomokos/light-service)
7
7
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT)
8
8
  [![Download Count](https://ruby-gem-downloads-badge.herokuapp.com/light-service?type=total)](https://rubygems.org/gems/light-service)
9
9
 
10
- <br>
10
+ LightService is a powerful and flexible service skeleton framework with an emphasis on simplicity
11
11
 
12
- ![Orchestrators-Deprecated](resources/orchestrators_deprecated.svg)
13
- <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.
14
-
15
- <br>
16
-
17
- ## Table of Content
12
+ ## Table of Contents
18
13
  * [Why LightService?](#why-lightservice)
19
14
  * [Getting Started](#getting-started)
20
15
  * [Requirements](#requirements)
@@ -26,14 +21,18 @@
26
21
  * [Skipping the Rest of the Actions](#skipping-the-rest-of-the-actions)
27
22
  * [Benchmarking Actions with Around Advice](#benchmarking-actions-with-around-advice)
28
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)
29
26
  * [Key Aliases](#key-aliases)
30
27
  * [Logging](#logging)
31
28
  * [Error Codes](#error-codes)
32
29
  * [Action Rollback](#action-rollback)
33
30
  * [Localizing Messages](#localizing-messages)
34
- * [Orchestrator Logic in Organizers](#orchestrator-logic-in-organizers)
31
+ * [Orchestrating Logic in Organizers](#orchestrating-logic-in-organizers)
35
32
  * [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
36
33
  * [Rails support](#rails-support)
34
+ * [Implementations in other languages](#other-implementations)
35
+ * [Contributing](#contributing)
37
36
 
38
37
  ## Why LightService?
39
38
 
@@ -532,9 +531,11 @@ These ideas are originally from Aspect Oriented Programming, read more about the
532
531
  ## Expects and Promises
533
532
  The `expects` and `promises` macros are rules for the inputs/outputs of an action.
534
533
  `expects` describes what keys it needs to execute, and `promises` makes sure the keys are in the context after the
535
- 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.
536
536
 
537
537
  This is how it's used:
538
+
538
539
  ```ruby
539
540
  class FooAction
540
541
  extend LightService::Action
@@ -542,46 +543,78 @@ class FooAction
542
543
  promises :bar
543
544
 
544
545
  executed do |context|
545
- baz = context.fetch :baz
546
-
547
- bar = baz + 2
548
- context[:bar] = bar
546
+ context.bar = context.baz + 2
549
547
  end
550
548
  end
551
549
  ```
552
550
 
553
- The `expects` macro does a bit more for you: it pulls the value with the expected key from the context, and
554
- 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:
555
576
 
556
577
  ```ruby
557
- class FooAction
578
+ class SendSMS
558
579
  extend LightService::Action
559
- expects :baz
560
- promises :bar
580
+ expects :message, :user
581
+ expects :allow_failure, default: true
561
582
 
562
583
  executed do |context|
563
- bar = context.baz + 2
564
- 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
565
590
  end
566
591
  end
567
592
  ```
568
593
 
569
- The `promises` macro will not only check if the context has the promised keys, it also sets it for you in the context if
570
- 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.
571
596
 
572
597
  ```ruby
573
- class FooAction
598
+ class SendSMS
574
599
  extend LightService::Action
575
- expects :baz
576
- promises :bar
600
+ expects :message, :user
601
+ expects :allow_failure, default: ->(ctx) { !ctx[:user].admin? } # Admins must always get SMS'
577
602
 
578
603
  executed do |context|
579
- 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
580
610
  end
581
611
  end
582
612
  ```
583
613
 
584
- 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.
585
618
 
586
619
  ## Key Aliases
587
620
  The `aliases` macro sets up pairs of keys and aliases in an organizer. Actions can access the context using the aliases.
@@ -866,9 +899,13 @@ end
866
899
 
867
900
  To get the value of a `fail!` or `succeed!` message, simply call `#message` on the returned context.
868
901
 
869
- ## Orchestrator Logic in Organizers
902
+ ## Orchestrating Logic in Organizers
870
903
 
871
- 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:
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.
907
+
908
+ Let's look at a piece of code that does basic data transformations:
872
909
 
873
910
  ```ruby
874
911
  class ExtractsTransformsLoadsData
@@ -922,27 +959,33 @@ end
922
959
 
923
960
  This code is much easier to reason about, it's less noisy and it captures the goal of LightService well: simple, declarative code that's easy to understand.
924
961
 
925
- The 7 different orchestrator constructs an organizer can have:
962
+ The 9 different orchestrator constructs an organizer can have:
926
963
 
927
964
  1. `reduce_until`
928
965
  2. `reduce_if`
929
- 3. `iterate`
930
- 4. `execute`
931
- 5. `with_callback`
932
- 6. `add_to_context`
933
- 7. `add_aliases`
966
+ 3. `reduce_if_else`
967
+ 4. `reduce_case`
968
+ 5. `iterate`
969
+ 6. `execute`
970
+ 7. `with_callback`
971
+ 8. `add_to_context`
972
+ 9. `add_aliases`
934
973
 
935
974
  `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.
936
975
 
937
976
  `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.
938
977
 
978
+ `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.
979
+
980
+ `reduce_case` behaves like a Ruby `case` statement. The first parameter `value` is the key of the value within the context that will be worked with. The second parameter `when` is a hash where the keys are conditional values and the values are steps to take if the condition matches. The final parameter `else` is a set of steps to take if no conditions within the `when` parameter are met. [This acceptance test](spec/acceptance/organizer/reduce_case_spec.rb) describes this functionality.
981
+
939
982
  `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.
940
983
 
941
984
  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.
942
985
 
943
986
  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.
944
987
 
945
- `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.
988
+ `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.
946
989
 
947
990
  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.
948
991
 
@@ -1046,6 +1089,15 @@ them through the context. A stub context will be created in the test file using
1046
1089
 
1047
1090
  When specifying `promises`, specs will be created testing for their existence after executing the action.
1048
1091
 
1092
+ ## Other implementations
1093
+
1094
+ | Language | Repo | Author |
1095
+ | :--------- |:------------------------------------------------------------------------| :------------------------------------------------------|
1096
+ | Python | [pyservice](https://github.com/adomokos/pyservice) | [@adomokos](https://github.com/adomokos) |
1097
+ | PHP | [light-service](https://github.com/douglasgreyling/light-service) | [@douglasgreyling](https://github.com/douglasgreyling) |
1098
+ | JavaScript | [light-service.js](https://github.com/douglasgreyling/light-service.js) | [@douglasgreyling](https://github.com/douglasgreyling) |
1099
+
1100
+
1049
1101
  ## Contributing
1050
1102
  1. Fork it
1051
1103
  2. Create your feature branch (`git checkout -b my-new-feature`)
data/RELEASES.md CHANGED
@@ -1,11 +1,28 @@
1
1
  A brief list of new features and changes introduced with the specified version.
2
2
 
3
+ ### 0.18.0
4
+ * [Remove Ruby 2.6, add 3.1 to build](https://github.com/adomokos/light-service/pull/233)
5
+ * [Add reduce_when](https://github.com/adomokos/light-service/pull/232)
6
+ * [Drop Ruby 2.5 version support, add 3.0 build](https://github.com/adomokos/light-service/pull/225)
7
+ * [Support for named argument in Ruby](https://github.com/adomokos/light-service/pull/224)
8
+
9
+ ### 0.17.0
10
+ * [Fix around_action hook for nested actions](https://github.com/adomokos/light-service/pull/217)
11
+ * [Add ReduceIfElse macro](https://github.com/adomokos/light-service/pull/218)
12
+ * [Implement support for default values for optional expected keys](https://github.com/adomokos/light-service/pull/219)
13
+ * [Add light-service.js implementation to README](https://github.com/adomokos/light-service/pull/222)
14
+
15
+ ### 0.16.0
16
+ * [Drop Ruby 2.4 support](https://github.com/adomokos/light-service/pull/207)
17
+ * [Fix callback current action](https://github.com/adomokos/light-service/pull/209)
18
+ * [Add Context accessors](https://github.com/adomokos/light-service/pull/211)
19
+ * [Switched to GH Actions from Travis CI](https://github.com/adomokos/light-service/pull/212)
20
+
3
21
  ### 0.15.0
4
22
  * [Add Rails Generators](https://github.com/adomokos/light-service/pull/194) - LightService actions and organizers can be generated with generators
5
23
  * [Add CodeCov](https://github.com/adomokos/light-service/pull/195) - Upload code coverage report to codecov.io
6
24
  * [Remove ActiveSupport 3 checks](https://github.com/adomokos/light-service/pull/197) - They are unsupported, no need to tests them any more.
7
25
 
8
-
9
26
  ### 0.14.0
10
27
  * [Add 'organized_by' to context](https://github.com/adomokos/light-service/pull/192) - Context now have an #organized_by attribute
11
28
 
@@ -25,7 +25,6 @@ module LightService
25
25
  options.tests? && test_framework_supported?
26
26
  end
27
27
 
28
- # rubocop:disable Metrics/AbcSize
29
28
  def create_required_gen_vals_from(name)
30
29
  path_parts = name.underscore.split('/')
31
30
 
@@ -39,7 +38,6 @@ module LightService
39
38
  :full_class_name => name.classify
40
39
  }
41
40
  end
42
- # rubocop:enable Metrics/AbcSize
43
41
  end
44
42
  end
45
43
  end
@@ -15,6 +15,12 @@ module LightService
15
15
 
16
16
  module Macros
17
17
  def expects(*args)
18
+ if expect_key_having_default?(args)
19
+ available_defaults[args.first] = args.last[:default]
20
+
21
+ args = [args.first]
22
+ end
23
+
18
24
  expected_keys.concat(args)
19
25
  end
20
26
 
@@ -30,8 +36,8 @@ module LightService
30
36
  @promised_keys ||= []
31
37
  end
32
38
 
33
- def executed
34
- define_singleton_method :execute do |context = {}|
39
+ def executed(*_args, &block)
40
+ define_singleton_method :execute do |context = Context.make|
35
41
  action_context = create_action_context(context)
36
42
  return action_context if action_context.stop_processing?
37
43
 
@@ -43,7 +49,11 @@ module LightService
43
49
 
44
50
  catch(:jump_when_failed) do
45
51
  call_before_action(action_context)
46
- yield(action_context)
52
+
53
+ execute_action(action_context, &block)
54
+
55
+ # Reset the stored action in case it was changed downstream
56
+ action_context.current_action = self
47
57
  call_after_action(action_context)
48
58
  end
49
59
  end
@@ -63,8 +73,34 @@ module LightService
63
73
 
64
74
  private
65
75
 
76
+ def execute_action(context)
77
+ if around_action_context?(context)
78
+ context.around_actions.call(context) do
79
+ yield(context)
80
+ context
81
+ end
82
+ else
83
+ yield(context)
84
+ end
85
+ end
86
+
87
+ def available_defaults
88
+ @available_defaults ||= {}
89
+ end
90
+
91
+ def expect_key_having_default?(key)
92
+ return false unless key.size == 2 && key.last.is_a?(Hash)
93
+ return true if key.last.key?(:default)
94
+
95
+ bad_key = key.last.keys.first
96
+ err_msg = "Specify defaults with a `default` key. You have #{bad_key}."
97
+ raise UnusableExpectKeyDefaultError, err_msg
98
+ end
99
+
66
100
  def create_action_context(context)
67
- return context if context.is_a? LightService::Context
101
+ usable_defaults(context).each do |ctx_key, default|
102
+ context[ctx_key] = extract_default(default, context)
103
+ end
68
104
 
69
105
  LightService::Context.make(context)
70
106
  end
@@ -73,6 +109,22 @@ module LightService
73
109
  expected_keys + promised_keys
74
110
  end
75
111
 
112
+ def missing_expected_keys(context)
113
+ expected_keys - context.keys
114
+ end
115
+
116
+ def usable_defaults(context)
117
+ available_defaults.slice(
118
+ *(missing_expected_keys(context) & available_defaults.keys)
119
+ )
120
+ end
121
+
122
+ def extract_default(default, context)
123
+ return default unless default.respond_to?(:call)
124
+
125
+ default.call(context)
126
+ end
127
+
76
128
  def call_before_action(context)
77
129
  invoke_callbacks(context[:_before_actions], context)
78
130
  end
@@ -90,6 +142,11 @@ module LightService
90
142
 
91
143
  context
92
144
  end
145
+
146
+ def around_action_context?(context)
147
+ context.instance_of?(Context) &&
148
+ context.around_actions.respond_to?(:call)
149
+ end
93
150
  end
94
151
  end
95
152
  end
@@ -24,7 +24,7 @@ module LightService
24
24
 
25
25
  def error_message
26
26
  "#{type_name} #{format_keys(keys_not_found(keys))} " \
27
- "to be in the context during #{action}"
27
+ "to be in the context during #{action}"
28
28
  end
29
29
 
30
30
  def throw_error_predicate(_keys)
@@ -95,7 +95,7 @@ module LightService
95
95
 
96
96
  def error_message
97
97
  "promised or expected keys cannot be a " \
98
- "reserved key: [#{format_keys(violated_keys)}]"
98
+ "reserved key: [#{format_keys(violated_keys)}]"
99
99
  end
100
100
 
101
101
  def keys
@@ -111,7 +111,26 @@ module LightService
111
111
  end
112
112
 
113
113
  def reserved_keys
114
- %i[message error_code current_action].freeze
114
+ %i[message error_code current_action organized_by].freeze
115
+ end
116
+ end
117
+
118
+ class ReservedKeysViaOrganizerVerifier < ReservedKeysVerifier
119
+ # rubocop:disable Lint/MissingSuper
120
+ def initialize(context_data)
121
+ @context = LightService::Context.make(context_data)
122
+ end
123
+ # rubocop:enable Lint/MissingSuper
124
+
125
+ def violated_keys
126
+ context.keys.map(&:to_sym) & reserved_keys
127
+ end
128
+
129
+ def error_message
130
+ <<~ERR
131
+ reserved keys cannot be added to the context
132
+ reserved key: [#{format_keys(violated_keys)}]
133
+ ERR
115
134
  end
116
135
  end
117
136
  end