functional-light-service 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +96 -11
  5. data/functional-light-service.gemspec +3 -1
  6. data/lib/functional-light-service/action.rb +48 -0
  7. data/lib/functional-light-service/configuration.rb +12 -2
  8. data/lib/functional-light-service/context/key_verifier.rb +1 -1
  9. data/lib/functional-light-service/context.rb +15 -2
  10. data/lib/functional-light-service/errors.rb +1 -0
  11. data/lib/functional-light-service/i18n/localization_adapter.rb +50 -0
  12. data/lib/functional-light-service/localization_adapter.rb +19 -28
  13. data/lib/functional-light-service/localization_map.rb +9 -0
  14. data/lib/functional-light-service/organizer/reduce_case.rb +50 -0
  15. data/lib/functional-light-service/organizer/reduce_if_else.rb +23 -0
  16. data/lib/functional-light-service/organizer/reduce_until.rb +1 -1
  17. data/lib/functional-light-service/organizer/reduce_while.rb +31 -0
  18. data/lib/functional-light-service/organizer/scoped_reducable.rb +2 -2
  19. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +2 -1
  20. data/lib/functional-light-service/organizer.rb +18 -3
  21. data/lib/functional-light-service/version.rb +1 -1
  22. data/lib/functional-light-service.rb +5 -0
  23. data/spec/acceptance/organizer/add_to_context_spec.rb +24 -0
  24. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +22 -0
  25. data/spec/acceptance/organizer/execute_spec.rb +21 -0
  26. data/spec/acceptance/organizer/reduce_case_spec.rb +65 -0
  27. data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
  28. data/spec/acceptance/organizer/reduce_while_spec.rb +96 -0
  29. data/spec/acceptance/skip_all_remaining_spec.rb +139 -0
  30. data/spec/action_optional_expected_keys_spec.rb +107 -0
  31. data/spec/context/inspect_spec.rb +13 -3
  32. data/spec/context_spec.rb +12 -0
  33. data/spec/i18n_localization_adapter_spec.rb +83 -0
  34. data/spec/localization_adapter_spec.rb +66 -83
  35. data/spec/spec_helper.rb +3 -0
  36. data/spec/test_doubles.rb +28 -0
  37. metadata +26 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 436934533c3db4aa6ac6e3ce7410b67454fe0e7f3f8b516e9b6ba7674e86fc29
4
- data.tar.gz: c4a36634905f4253a0d32bf9068b70a236f67291e43e20d02f3617679a797ef7
3
+ metadata.gz: 53e332561acc819e0d2d6488a38adeb3600aadcf31c100e474e1d1b0a6397157
4
+ data.tar.gz: 6500b3cdbf6895ce0e165eb36cc3dc7634ed4fc341cd42ceea3a5553eaf6033d
5
5
  SHA512:
6
- metadata.gz: 8ba109d1688a3e2e3c1904682d55e800cd841fa7870364f04916d9f5528ac0e954fe28f650261b1217c0d0819f0df0c1143331fd82f72a1dfc6726549a95cc82
7
- data.tar.gz: 4da5cc5beae3e452b7400ce696a05a7ab5e69c354d27cd3c3e918c86ef47166789a7349a9967215ca53f35cb4d00dac986ac109d673cee00e07abace42b6569f
6
+ metadata.gz: 7b64b265d6d63225baca32509169796242f4003c4000e429f4ab9758a310ae4b1a4668553512b40bc9d070a61cd2566ef0eb1efc8cdad3e06cda7b5b478a1854
7
+ data.tar.gz: 959de9564732c9acd2108c746b36be9da3445ea969a7940943c5c88543044d25fd5e3c5df5f6f6b749aa54f950a9a9a511320d992d0536d901eff116f70d2431
data/.gitignore CHANGED
@@ -19,4 +19,6 @@ vendor/bundle
19
19
  bin
20
20
  gems.tags
21
21
  tags
22
+ .claude/
23
+ .ruby-lsp/
22
24
  !*.gitkeep
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 6.2.0 (2026-07-18)
2
+
3
+ Port selettivo delle feature di light-service upstream 0.14-0.21 (niente Rails
4
+ support; solo aggiunte retro-compatibili).
5
+
6
+ ### Added
7
+ - skip_all_remaining!: salta tutto il resto dell'organizer, scope annidati compresi; non viene resettato da reduce_if/iterate/ecc. (upstream 0.21) ( 2026-07-18 ) [ sphynx79]
8
+ - reduce_while: macro dell'organizer che controlla la condizione prima di ogni step (upstream 0.21) ( 2026-07-18 ) [ sphynx79]
9
+ - reduce_if_else: come reduce_if ma con ramo else (upstream 0.17) ( 2026-07-18 ) [ sphynx79]
10
+ - reduce_case: dispatch sugli step in base a un valore del context, kwargs :value/:when/:else (upstream 0.18) ( 2026-07-18 ) [ sphynx79]
11
+ - expects :key, default: valore-o-lambda per chiavi attese opzionali; kwarg diverso da :default solleva UnusableExpectKeyDefaultError (upstream 0.17) ( 2026-07-18 ) [ sphynx79]
12
+ - execute accetta un blocco oltre alla lambda (upstream master) ( 2026-07-18 ) [ sphynx79]
13
+ - :organized_by tra le chiavi riservate del KeyVerifier: errore piu chiaro in caso di collisione (upstream 0.14) ( 2026-07-18 ) [ sphynx79]
14
+
15
+ ### Changed
16
+ - add_to_context registra anche gli accessor delle chiavi aggiunte (ctx.mia_chiave); chiavi che collidono con metodi del Context ora sollevano ReservedKeysInContextError invece di passare in silenzio (upstream 0.16) ( 2026-07-18 ) [ sphynx79]
17
+ - i18n non e' piu una dipendenza runtime: di default i simboli si risolvono col nuovo adapter built-in (LocalizationMap + Configuration.locale); l'adapter I18n trasloca in FunctionalLightService::I18n::LocalizationAdapter e viene selezionato solo se l'applicazione carica la gem i18n (upstream 0.19/0.20) ( 2026-07-18 ) [ sphynx79]
18
+
19
+
1
20
  ## 6.1.0 (2026-07-03)
2
21
 
3
22
  ### Added
data/README.md CHANGED
@@ -419,6 +419,28 @@ end
419
419
  In the example above, the organizer invokes four actions.
420
420
  The first two run successfully; the third calls skip_remaining!, so the fourth is never executed, yet the overall context stays successful.
421
421
 
422
+ ### Skipping everything, including nested scopes
423
+
424
+ `skip_remaining!` is scoped: constructs like `reduce_if` or `iterate` reset it
425
+ at their boundary, so it only exits the **current** scope. When you need to stop
426
+ the whole organizer from inside a nested construct, use
427
+ `context.skip_all_remaining!` — it is never reset, so every remaining step (in
428
+ the current scope and in the outer ones) is skipped while the context stays
429
+ successful:
430
+
431
+ ```ruby
432
+ class StopsEverythingAction
433
+ extend FunctionalLightService::Action
434
+ expects :item
435
+
436
+ executed do |context|
437
+ if context.item.poison_pill?
438
+ context.skip_all_remaining!("Poison pill found, stopping the pipeline")
439
+ end
440
+ end
441
+ end
442
+ ```
443
+
422
444
  ## Benchmarking Actions with Around Advice
423
445
 
424
446
  When you need to profile a pipeline, adding timing code inside every single
@@ -620,6 +642,33 @@ end
620
642
 
621
643
  Want to see it in practice? Check out [this spec](spec/action_expects_and_promises_spec.rb) test file.
622
644
 
645
+ ### Default values for expected keys
646
+
647
+ An expected key can declare a `default`, used when the key is missing from the
648
+ context (also when the action runs inside an organizer). The default can be a
649
+ static value or a lambda receiving the context:
650
+
651
+ ```ruby
652
+ class GreetsSomeoneAction
653
+ extend FunctionalLightService::Action
654
+
655
+ expects :name
656
+ expects :greeting, :default => "Hello"
657
+ expects :message, :default => ->(ctx) { "#{ctx[:greeting]}, #{ctx[:name]}!" }
658
+
659
+ executed do |context|
660
+ puts context.message
661
+ end
662
+ end
663
+
664
+ GreetsSomeoneAction.execute(:name => "Rick") # ⇒ "Hello, Rick!"
665
+ ```
666
+
667
+ Note that `expects` accepts a single key when a default is given, and any
668
+ keyword other than `default` raises `UnusableExpectKeyDefaultError` at class
669
+ definition time. Keys already reachable through an alias are considered
670
+ present, so their default is not applied.
671
+
623
672
  ## Key Aliases
624
673
 
625
674
  Need to wire together actions that use different key names?
@@ -830,10 +879,30 @@ For a full example, see [this acceptance test](spec/acceptance/rollback_spec.rb)
830
879
 
831
880
  ## Localizing Messages
832
881
 
833
- FunctionalLightService integrates with **I18n** out of the box, so you can translate
834
- success or failure messages without extra plumbing.
835
- If your app needs something more advanced, you can swap in a custom localization
836
- adapter.
882
+ Symbols passed to `fail!`/`succeed!` are looked up through a localization
883
+ adapter. Two adapters ship with the gem:
884
+
885
+ - **Built-in adapter** (default): resolves messages from
886
+ `FunctionalLightService::LocalizationMap.instance`, a plain hash keyed by
887
+ `Configuration.locale` (default `:en`) — no extra dependency needed:
888
+
889
+ ```ruby
890
+ FunctionalLightService::LocalizationMap.instance[:en] = {
891
+ :foo_action => {
892
+ :light_service => {
893
+ :failures => { :exceeded_api_limit => "Exceeded API limit" },
894
+ :successes => { :api_call_ok => "All good" }
895
+ }
896
+ }
897
+ }
898
+ ```
899
+
900
+ - **I18n adapter**: selected automatically when your application loads the
901
+ `i18n` gem (it is no longer a runtime dependency of this gem — add it to
902
+ your own Gemfile if you want I18n-backed lookups).
903
+
904
+ If your app needs something more advanced, you can swap in a custom
905
+ localization adapter.
837
906
 
838
907
  ```ruby
839
908
  class FooAction
@@ -906,7 +975,7 @@ configuration:
906
975
  FunctionalLightService::Configuration.localization_adapter = MyLocalizer.new
907
976
 
908
977
  # lib/my_localizer.rb
909
- class MyLocalizer < FunctionalLightService::LocalizationAdapter
978
+ class MyLocalizer < FunctionalLightService::I18n::LocalizationAdapter
910
979
  # change default scope to: "light_service.failures.<class_path>"
911
980
  def i18n_scope_from_class(action_class, type)
912
981
  "light_service.#{type.pluralize}.#{action_class.name.underscore}"
@@ -984,15 +1053,18 @@ your actions.
984
1053
 
985
1054
  | Construct | Declarative “equivalent” | What it does (in one line) |
986
1055
  | ------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
987
- | [reduce_until](spec/acceptance/organizer/reduce_until_spec.rb) | `while` loop | Keeps reducing the listed steps **until** the lambda returns `true`. |
988
- | [reduce_if](spec/acceptance/organizer/reduce_if_spec.rb) | `if/else` | Reduces its sub‑steps **only if** the lambda returns `true`. |
1056
+ | [reduce_until](spec/acceptance/organizer/reduce_until_spec.rb) | `until` loop | Keeps reducing the listed steps **until** the lambda returns `true`. |
1057
+ | [reduce_while](spec/acceptance/organizer/reduce_while_spec.rb) | `while` guard | Checks the lambda **before each step** and stops as soon as it returns `false`. |
1058
+ | [reduce_if](spec/acceptance/organizer/reduce_if_spec.rb) | `if` | Reduces its sub‑steps **only if** the lambda returns `true`. |
1059
+ | [reduce_if_else](spec/acceptance/organizer/reduce_if_else_spec.rb) | `if/else` | Reduces the first list of steps when the lambda is `true`, the second one otherwise. |
1060
+ | [reduce_case](spec/acceptance/organizer/reduce_case_spec.rb) | `case/when` | Dispatches to the steps matching a context value (`:value`, `:when`, `:else` kwargs). |
989
1061
  | [iterate](spec/acceptance/organizer/iterate_spec.rb) | `each` loop | Loops over a collection key; each element is exposed under the **singular** name. |
990
- | [execute](spec/acceptance/organizer/execute_spec.rb) | one‑off lambda | Runs an inline lambda for quick context tweaks (add keys, transform values, etc.). |
1062
+ | [execute](spec/acceptance/organizer/execute_spec.rb) | one‑off lambda | Runs an inline lambda or block for quick context tweaks (add keys, transform values, etc.). |
991
1063
  | [with_callback](spec/acceptance/organizer/with_callback_spec.rb) | streaming callback | Defers execution like a SAX parser—great for huge inputs without loading everything in RAM. |
992
- | [add_to_context](spec/acceptance/organizer/add_to_context_spec.rb) | N/A (context inject) | Injects key–value pairs into the context just before the following steps run. |
1064
+ | [add_to_context](spec/acceptance/organizer/add_to_context_spec.rb) | N/A (context inject) | Injects key–value pairs into the context (defining accessors) before the next steps run. |
993
1065
  | [add_aliases](spec/acceptance/organizer/add_aliases_spec.rb) | key aliasing | Creates an alias so actions can read/write the same value under different names. |
994
1066
 
995
- All seven are covered by acceptance tests in spec/acceptance/organizer/*_spec.rb.
1067
+ All ten are covered by acceptance tests in spec/acceptance/organizer/*_spec.rb.
996
1068
 
997
1069
  **Tip**: When iterating, the collection must already be in the context.
998
1070
  iterate(:items) expects context[:items]; it then places each element under
@@ -1003,10 +1075,23 @@ iterate(:items, [ProcessItem])
1003
1075
  # Inside ProcessItem → context.item
1004
1076
  ```
1005
1077
 
1006
- Need a quick context mutation?Use execute:
1078
+ Need a quick context mutation? Use execute, with a lambda or a block:
1007
1079
 
1008
1080
  ```ruby
1009
1081
  execute(->(c) { c[:some_values] = c.some_hash.values })
1082
+ # or
1083
+ execute { |c| c[:some_values] = c.some_hash.values }
1084
+ ```
1085
+
1086
+ Need to branch on a context value? Use reduce_case:
1087
+
1088
+ ```ruby
1089
+ reduce_case :value => :status,
1090
+ :when => {
1091
+ :active => [NotifiesUserAction],
1092
+ :archived => [ArchivesRecordAction]
1093
+ },
1094
+ :else => [RaisesUnknownStatusAction]
1010
1095
  ```
1011
1096
 
1012
1097
  ## ContextFactory for Faster Action Testing
@@ -16,11 +16,13 @@ Gem::Specification.new do |gem|
16
16
  gem.required_ruby_version = '>= 3.1.0'
17
17
 
18
18
  gem.add_runtime_dependency("dry-inflector", ">= 0.2.1", "< 2")
19
- gem.add_runtime_dependency("i18n", "~> 1.8", ">= 1.8.11")
20
19
  # logger non e' piu una default gem da Ruby 3.5/4.0: senza questa
21
20
  # dichiarazione `require 'logger'` fallisce sotto Bundler
22
21
  gem.add_runtime_dependency("logger", "~> 1.5")
23
22
 
23
+ # i18n e' opzionale a runtime: l'adapter I18n viene usato solo se
24
+ # l'applicazione ospite carica la gem (vedi Configuration)
25
+ gem.add_development_dependency("i18n", "~> 1.8", ">= 1.8.11")
24
26
  gem.add_development_dependency("rake", "~> 13.0")
25
27
  gem.add_development_dependency("rspec", "~> 3.13")
26
28
  gem.add_development_dependency("simplecov", "~> 0.22")
@@ -17,6 +17,12 @@ module FunctionalLightService
17
17
 
18
18
  module Macros
19
19
  def expects(*args)
20
+ if expect_key_having_default?(args)
21
+ available_defaults[args.first] = args.last[:default]
22
+
23
+ args = [args.first]
24
+ end
25
+
20
26
  expected_keys.concat(args)
21
27
  end
22
28
 
@@ -66,11 +72,53 @@ module FunctionalLightService
66
72
  private
67
73
 
68
74
  def create_action_context(context)
75
+ # I default vanno applicati anche quando l'action gira dentro un
76
+ # organizer (il context è già un Context): prima dell'early return.
77
+ # Il guard sull'ivar evita lavoro (e scritture lazy) nel percorso caldo
78
+ apply_expects_defaults(context) if @available_defaults
79
+
69
80
  return context if context.is_a? FunctionalLightService::Context
70
81
 
71
82
  FunctionalLightService::Context.make(context)
72
83
  end
73
84
 
85
+ def apply_expects_defaults(context)
86
+ usable_defaults(context).each do |ctx_key, default|
87
+ context[ctx_key] = extract_default(default, context)
88
+ end
89
+ end
90
+
91
+ def available_defaults
92
+ @available_defaults ||= {}
93
+ end
94
+
95
+ def expect_key_having_default?(key)
96
+ return false unless key.size == 2 && key.last.is_a?(Hash)
97
+ return true if key.last.key?(:default)
98
+
99
+ bad_key = key.last.keys.first
100
+ err_msg = "Specify defaults with a `default` key. You have #{bad_key}."
101
+ raise UnusableExpectKeyDefaultError, err_msg
102
+ end
103
+
104
+ def missing_expected_keys(context)
105
+ # context.key? risolve anche gli alias: `expected_keys - context.keys`
106
+ # (upstream) darebbe falsi mancanti sulle chiavi aliasate
107
+ expected_keys.reject { |key| context.key?(key) }
108
+ end
109
+
110
+ def usable_defaults(context)
111
+ available_defaults.slice(
112
+ *(missing_expected_keys(context) & available_defaults.keys)
113
+ )
114
+ end
115
+
116
+ def extract_default(default, context)
117
+ return default unless default.respond_to?(:call)
118
+
119
+ default.call(context)
120
+ end
121
+
74
122
  def all_keys
75
123
  expected_keys + promised_keys
76
124
  end
@@ -3,7 +3,7 @@
3
3
  module FunctionalLightService
4
4
  class Configuration
5
5
  class << self
6
- attr_writer :logger, :localization_adapter
6
+ attr_writer :logger, :localization_adapter, :locale
7
7
 
8
8
  def logger
9
9
  @logger = _default_logger unless instance_variable_defined?("@logger")
@@ -11,7 +11,17 @@ module FunctionalLightService
11
11
  end
12
12
 
13
13
  def localization_adapter
14
- @localization_adapter ||= LocalizationAdapter.new
14
+ # La gem i18n non è una dipendenza: l'adapter I18n viene scelto solo
15
+ # se la costante è già stata caricata dall'applicazione ospite
16
+ @localization_adapter ||= if Module.const_defined?('I18n')
17
+ FunctionalLightService::I18n::LocalizationAdapter.new
18
+ else
19
+ LocalizationAdapter.new
20
+ end
21
+ end
22
+
23
+ def locale
24
+ @locale ||= :en
15
25
  end
16
26
 
17
27
  private
@@ -116,7 +116,7 @@ module FunctionalLightService
116
116
  def reserved_keys
117
117
  # _aliases/_before_actions/_after_actions sono chiavi infrastrutturali
118
118
  # scritte da Organizer.with nel context
119
- %i[message error_code current_action
119
+ %i[message error_code current_action organized_by
120
120
  _aliases _before_actions _after_actions].freeze
121
121
  end
122
122
  end
@@ -14,6 +14,7 @@ module FunctionalLightService
14
14
  outcome = Success(:message => '', :error => nil))
15
15
  @outcome = outcome
16
16
  @skip_remaining = false
17
+ @skip_all_remaining = false
17
18
  context.to_hash.each { |k, v| self[k] = v }
18
19
  end
19
20
  # rubocop:enable Lint/MissingSuper
@@ -46,6 +47,10 @@ module FunctionalLightService
46
47
  @skip_remaining
47
48
  end
48
49
 
50
+ def skip_all_remaining?
51
+ @skip_all_remaining
52
+ end
53
+
49
54
  def reset_skip_remaining!
50
55
  # Resetta soltanto il flag: l'esito (e il suo messaggio) non vanno persi
51
56
  @skip_remaining = false
@@ -100,8 +105,15 @@ module FunctionalLightService
100
105
  @skip_remaining = true
101
106
  end
102
107
 
108
+ # Variante non azzerabile di skip_remaining!: salta tutto il resto
109
+ # dell'organizer, scope annidati compresi (scoped_reduce non la resetta)
110
+ def skip_all_remaining!(message = nil)
111
+ @outcome = Success(:message => message)
112
+ @skip_all_remaining = true
113
+ end
114
+
103
115
  def stop_processing?
104
- failure? || skip_remaining?
116
+ failure? || skip_remaining? || skip_all_remaining?
105
117
  end
106
118
 
107
119
  # Registra le chiavi come accessor consentiti: la lettura/scrittura passa
@@ -174,7 +186,8 @@ module FunctionalLightService
174
186
 
175
187
  def inspect
176
188
  "#{self.class}(#{self}, success: #{success?}, message: #{check_nil(message)}, error_code: " \
177
- "#{check_nil(error_code)}, skip_remaining: #{@skip_remaining}, aliases: #{aliases})"
189
+ "#{check_nil(error_code)}, skip_remaining: #{@skip_remaining}, " \
190
+ "skip_all_remaining: #{@skip_all_remaining}, aliases: #{aliases})"
178
191
  end
179
192
 
180
193
  private
@@ -5,4 +5,5 @@ module FunctionalLightService
5
5
  class ExpectedKeysNotInContextError < StandardError; end
6
6
  class PromisedKeysNotInContextError < StandardError; end
7
7
  class ReservedKeysInContextError < StandardError; end
8
+ class UnusableExpectKeyDefaultError < StandardError; end
8
9
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/inflector'
4
+
5
+ module FunctionalLightService
6
+ module I18n
7
+ class LocalizationAdapter
8
+ def failure(message_or_key, action_class, i18n_options = {})
9
+ find_translated_message(message_or_key,
10
+ action_class,
11
+ i18n_options,
12
+ :type => :failure)
13
+ end
14
+
15
+ def success(message_or_key, action_class, i18n_options = {})
16
+ find_translated_message(message_or_key,
17
+ action_class,
18
+ i18n_options,
19
+ :type => :success)
20
+ end
21
+
22
+ private
23
+
24
+ def find_translated_message(message_or_key,
25
+ action_class,
26
+ i18n_options,
27
+ type)
28
+ if message_or_key.is_a?(Symbol)
29
+ translate(message_or_key, action_class, i18n_options.merge(type))
30
+ else
31
+ message_or_key
32
+ end
33
+ end
34
+
35
+ def translate(key, action_class, options = {})
36
+ type = options.delete(:type)
37
+
38
+ scope = i18n_scope_from_class(action_class, type)
39
+ options[:scope] = scope
40
+
41
+ ::I18n.t(key, **options)
42
+ end
43
+
44
+ def i18n_scope_from_class(action_class, type)
45
+ inflector = Dry::Inflector.new
46
+ "#{inflector.underscore(action_class.name)}.light_service.#{inflector.pluralize(type)}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,48 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n'
3
+ require 'dry/inflector'
4
4
 
5
5
  module FunctionalLightService
6
+ # Adapter built-in basato su LocalizationMap: nessuna dipendenza da I18n.
7
+ # Viene selezionato da Configuration quando la costante ::I18n non esiste
6
8
  class LocalizationAdapter
7
- def failure(message_or_key, action_class, i18n_options = {})
9
+ INFLECTOR = Dry::Inflector.new
10
+
11
+ def failure(message_or_key, action_class, options = {})
8
12
  find_translated_message(message_or_key,
9
- action_class,
10
- i18n_options,
11
- :type => :failure)
13
+ INFLECTOR.underscore(action_class.to_s),
14
+ options.merge(:type => :failures))
12
15
  end
13
16
 
14
- def success(message_or_key, action_class, i18n_options = {})
17
+ def success(message_or_key, action_class, options = {})
15
18
  find_translated_message(message_or_key,
16
- action_class,
17
- i18n_options,
18
- :type => :success)
19
+ INFLECTOR.underscore(action_class.to_s),
20
+ options.merge(:type => :successes))
19
21
  end
20
22
 
21
23
  private
22
24
 
23
- def find_translated_message(message_or_key,
24
- action_class,
25
- i18n_options,
26
- type)
25
+ def find_translated_message(message_or_key, action_class, options)
27
26
  if message_or_key.is_a?(Symbol)
28
- translate(message_or_key, action_class, i18n_options.merge(type))
27
+ FunctionalLightService::LocalizationMap.instance.dig(
28
+ FunctionalLightService::Configuration.locale,
29
+ action_class.to_sym,
30
+ :light_service,
31
+ options[:type],
32
+ message_or_key
33
+ )
29
34
  else
30
35
  message_or_key
31
36
  end
32
37
  end
33
-
34
- def translate(key, action_class, options = {})
35
- type = options.delete(:type)
36
-
37
- scope = i18n_scope_from_class(action_class, type)
38
- options[:scope] = scope
39
-
40
- I18n.t(key, **options)
41
- end
42
-
43
- def i18n_scope_from_class(action_class, type)
44
- inflector = Dry::Inflector.new
45
- "#{inflector.underscore(action_class.name)}.light_service.#{inflector.pluralize(type)}"
46
- end
47
38
  end
48
39
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module FunctionalLightService
6
+ class LocalizationMap < Hash
7
+ include ::Singleton
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Organizer
5
+ class ReduceCase
6
+ extend ScopedReducable
7
+
8
+ class Arguments
9
+ attr_reader :value, :when, :else
10
+
11
+ def initialize(**args)
12
+ validate_arguments(**args)
13
+ @value = args[:value]
14
+ @when = args[:when]
15
+ @else = args[:else]
16
+ end
17
+
18
+ private
19
+
20
+ # rubocop:disable Style/MultilineIfModifier
21
+ def validate_arguments(**args)
22
+ raise(
23
+ ArgumentError,
24
+ "Expected keyword arguments: [:value, :when, :else]. Given: #{args.keys}"
25
+ ) unless args.keys.intersection(mandatory_arguments).count == mandatory_arguments.count
26
+ end
27
+ # rubocop:enable Style/MultilineIfModifier
28
+
29
+ def mandatory_arguments
30
+ %i[value when else]
31
+ end
32
+ end
33
+
34
+ def self.run(organizer, **args)
35
+ arguments = Arguments.new(**args)
36
+
37
+ ->(ctx) do
38
+ return ctx if ctx.stop_processing?
39
+
40
+ matched_case = arguments.when.keys.find { |k| k.eql?(ctx[arguments.value]) }
41
+ steps = arguments.when[matched_case] || arguments.else
42
+
43
+ ctx = scoped_reduce(organizer, ctx, steps)
44
+
45
+ ctx
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Organizer
5
+ class ReduceIfElse
6
+ extend ScopedReducable
7
+
8
+ def self.run(organizer, condition_block, if_steps, else_steps)
9
+ ->(ctx) do
10
+ return ctx if ctx.stop_processing?
11
+
12
+ ctx = if condition_block.call(ctx)
13
+ scoped_reduce(organizer, ctx, if_steps)
14
+ else
15
+ scoped_reduce(organizer, ctx, else_steps)
16
+ end
17
+
18
+ ctx
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -11,7 +11,7 @@ module FunctionalLightService
11
11
 
12
12
  loop do
13
13
  ctx = scoped_reduce(organizer, ctx, steps)
14
- break if condition_block.call(ctx) || ctx.failure?
14
+ break if condition_block.call(ctx) || ctx.stop_processing?
15
15
  end
16
16
 
17
17
  ctx
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Organizer
5
+ class ReduceWhile
6
+ def self.run(organizer, condition_block, steps)
7
+ ->(ctx) do
8
+ return ctx if ctx.stop_processing?
9
+
10
+ reset_skip(ctx)
11
+
12
+ Array(steps).each do |step|
13
+ break unless condition_block.call(ctx)
14
+
15
+ ctx = organizer.with(ctx).reduce([step])
16
+ break if ctx.stop_processing?
17
+ end
18
+
19
+ reset_skip(ctx)
20
+
21
+ ctx
22
+ end
23
+ end
24
+
25
+ def self.reset_skip(ctx)
26
+ ctx.reset_skip_remaining! unless ctx.failure? || ctx.skip_all_remaining?
27
+ end
28
+ private_class_method :reset_skip
29
+ end
30
+ end
31
+ end
@@ -4,9 +4,9 @@ module FunctionalLightService
4
4
  module Organizer
5
5
  module ScopedReducable
6
6
  def scoped_reduce(organizer, ctx, steps)
7
- ctx.reset_skip_remaining! unless ctx.failure?
7
+ ctx.reset_skip_remaining! unless ctx.failure? || ctx.skip_all_remaining?
8
8
  ctx = organizer.with(ctx).reduce([steps])
9
- ctx.reset_skip_remaining! unless ctx.failure?
9
+ ctx.reset_skip_remaining! unless ctx.failure? || ctx.skip_all_remaining?
10
10
 
11
11
  ctx
12
12
  end
@@ -93,7 +93,8 @@ module FunctionalLightService
93
93
  end
94
94
 
95
95
  def skip_remaining?(context)
96
- context.respond_to?(:skip_remaining?) && context.skip_remaining?
96
+ (context.respond_to?(:skip_remaining?) && context.skip_remaining?) ||
97
+ (context.respond_to?(:skip_all_remaining?) && context.skip_all_remaining?)
97
98
  end
98
99
 
99
100
  def write_skip_remaining_log(context, action)