functional-light-service 6.0.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +26 -0
- data/README.md +130 -11
- data/functional-light-service.gemspec +3 -1
- data/lib/functional-light-service/action.rb +48 -0
- data/lib/functional-light-service/configuration.rb +12 -2
- data/lib/functional-light-service/context/key_verifier.rb +1 -1
- data/lib/functional-light-service/context.rb +15 -2
- data/lib/functional-light-service/errors.rb +1 -0
- data/lib/functional-light-service/functional/sequencer.rb +144 -0
- data/lib/functional-light-service/i18n/localization_adapter.rb +50 -0
- data/lib/functional-light-service/localization_adapter.rb +19 -28
- data/lib/functional-light-service/localization_map.rb +9 -0
- data/lib/functional-light-service/organizer/reduce_case.rb +50 -0
- data/lib/functional-light-service/organizer/reduce_if_else.rb +23 -0
- data/lib/functional-light-service/organizer/reduce_until.rb +1 -1
- data/lib/functional-light-service/organizer/reduce_while.rb +31 -0
- data/lib/functional-light-service/organizer/scoped_reducable.rb +2 -2
- data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +2 -1
- data/lib/functional-light-service/organizer.rb +18 -3
- data/lib/functional-light-service/version.rb +1 -1
- data/lib/functional-light-service.rb +6 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +24 -0
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +22 -0
- data/spec/acceptance/organizer/execute_spec.rb +21 -0
- data/spec/acceptance/organizer/reduce_case_spec.rb +65 -0
- data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
- data/spec/acceptance/organizer/reduce_while_spec.rb +96 -0
- data/spec/acceptance/skip_all_remaining_spec.rb +139 -0
- data/spec/action_optional_expected_keys_spec.rb +107 -0
- data/spec/context/inspect_spec.rb +13 -3
- data/spec/context_spec.rb +12 -0
- data/spec/i18n_localization_adapter_spec.rb +83 -0
- data/spec/lib/deterministic/sequencer_spec.rb +506 -0
- data/spec/localization_adapter_spec.rb +66 -83
- data/spec/spec_helper.rb +3 -0
- data/spec/test_doubles.rb +28 -0
- metadata +27 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53e332561acc819e0d2d6488a38adeb3600aadcf31c100e474e1d1b0a6397157
|
|
4
|
+
data.tar.gz: 6500b3cdbf6895ce0e165eb36cc3dc7634ed4fc341cd42ceea3a5553eaf6033d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b64b265d6d63225baca32509169796242f4003c4000e429f4ab9758a310ae4b1a4668553512b40bc9d070a61cd2566ef0eb1efc8cdad3e06cda7b5b478a1854
|
|
7
|
+
data.tar.gz: 959de9564732c9acd2108c746b36be9da3445ea969a7940943c5c88543044d25fd5e3c5df5f6f6b749aa54f950a9a9a511320d992d0536d901eff116f70d2431
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
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
|
+
|
|
20
|
+
## 6.1.0 (2026-07-03)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Sequencer: do-notation per Result con in_sequence/get/let/and_then/observe/and_yield, portato dalla gem deterministic (MIT) e incluso nel Prelude ( 2026-07-03 ) [ sphynx79]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
1
27
|
## 6.0.0 (2026-07-03)
|
|
2
28
|
|
|
3
29
|
Release maggiore guidata da un audit tecnico completo (vedi `AUDIT-functional-light-service.md`
|
data/README.md
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* [Usage](#functional-usage)
|
|
29
29
|
* [Result: Success & Failure](#functional-usage-success-failure)
|
|
30
30
|
* [Result Chaining](#functional-usage-chaining)
|
|
31
|
+
* [Sequencing (do-notation)](#functional-usage-sequencing)
|
|
31
32
|
* [Complex Example in a Builder Action](#functional-usage-complex-action)
|
|
32
33
|
* [Pattern matching](#functional-usage-pattern-matching)
|
|
33
34
|
* [Option](#functional-usage-option)
|
|
@@ -418,6 +419,28 @@ end
|
|
|
418
419
|
In the example above, the organizer invokes four actions.
|
|
419
420
|
The first two run successfully; the third calls skip_remaining!, so the fourth is never executed, yet the overall context stays successful.
|
|
420
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
|
+
|
|
421
444
|
## Benchmarking Actions with Around Advice
|
|
422
445
|
|
|
423
446
|
When you need to profile a pipeline, adding timing code inside every single
|
|
@@ -619,6 +642,33 @@ end
|
|
|
619
642
|
|
|
620
643
|
Want to see it in practice? Check out [this spec](spec/action_expects_and_promises_spec.rb) test file.
|
|
621
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
|
+
|
|
622
672
|
## Key Aliases
|
|
623
673
|
|
|
624
674
|
Need to wire together actions that use different key names?
|
|
@@ -829,10 +879,30 @@ For a full example, see [this acceptance test](spec/acceptance/rollback_spec.rb)
|
|
|
829
879
|
|
|
830
880
|
## Localizing Messages
|
|
831
881
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
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.
|
|
836
906
|
|
|
837
907
|
```ruby
|
|
838
908
|
class FooAction
|
|
@@ -905,7 +975,7 @@ configuration:
|
|
|
905
975
|
FunctionalLightService::Configuration.localization_adapter = MyLocalizer.new
|
|
906
976
|
|
|
907
977
|
# lib/my_localizer.rb
|
|
908
|
-
class MyLocalizer < FunctionalLightService::LocalizationAdapter
|
|
978
|
+
class MyLocalizer < FunctionalLightService::I18n::LocalizationAdapter
|
|
909
979
|
# change default scope to: "light_service.failures.<class_path>"
|
|
910
980
|
def i18n_scope_from_class(action_class, type)
|
|
911
981
|
"light_service.#{type.pluralize}.#{action_class.name.underscore}"
|
|
@@ -983,15 +1053,18 @@ your actions.
|
|
|
983
1053
|
|
|
984
1054
|
| Construct | Declarative “equivalent” | What it does (in one line) |
|
|
985
1055
|
| ------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
|
|
986
|
-
| [reduce_until](spec/acceptance/organizer/reduce_until_spec.rb) | `
|
|
987
|
-
| [
|
|
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). |
|
|
988
1061
|
| [iterate](spec/acceptance/organizer/iterate_spec.rb) | `each` loop | Loops over a collection key; each element is exposed under the **singular** name. |
|
|
989
|
-
| [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.). |
|
|
990
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. |
|
|
991
|
-
| [add_to_context](spec/acceptance/organizer/add_to_context_spec.rb) | N/A (context inject) | Injects key–value pairs into the context
|
|
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. |
|
|
992
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. |
|
|
993
1066
|
|
|
994
|
-
All
|
|
1067
|
+
All ten are covered by acceptance tests in spec/acceptance/organizer/*_spec.rb.
|
|
995
1068
|
|
|
996
1069
|
**Tip**: When iterating, the collection must already be in the context.
|
|
997
1070
|
iterate(:items) expects context[:items]; it then places each element under
|
|
@@ -1002,10 +1075,23 @@ iterate(:items, [ProcessItem])
|
|
|
1002
1075
|
# Inside ProcessItem → context.item
|
|
1003
1076
|
```
|
|
1004
1077
|
|
|
1005
|
-
Need a quick context mutation?
|
|
1078
|
+
Need a quick context mutation? Use execute, with a lambda or a block:
|
|
1006
1079
|
|
|
1007
1080
|
```ruby
|
|
1008
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]
|
|
1009
1095
|
```
|
|
1010
1096
|
|
|
1011
1097
|
## ContextFactory for Faster Action Testing
|
|
@@ -1150,6 +1236,39 @@ Success(params) >>
|
|
|
1150
1236
|
build_response
|
|
1151
1237
|
```
|
|
1152
1238
|
|
|
1239
|
+
### Sequencing (do-notation) – `in_sequence` <a name="functional-usage-sequencing"></a>
|
|
1240
|
+
|
|
1241
|
+
When a pipeline needs the intermediate values of earlier steps, chaining alone
|
|
1242
|
+
gets awkward. `in_sequence` (ported from the [deterministic](https://github.com/pzol/deterministic)
|
|
1243
|
+
gem, MIT License) gives you a do-notation style block: each step returns a
|
|
1244
|
+
`Result`, the sequence short-circuits on the first `Failure`, and values bound
|
|
1245
|
+
with `get`/`let` are available to all subsequent steps by name.
|
|
1246
|
+
|
|
1247
|
+
```ruby
|
|
1248
|
+
class DownloadRemit
|
|
1249
|
+
include FunctionalLightService::Prelude
|
|
1250
|
+
|
|
1251
|
+
def call(row)
|
|
1252
|
+
in_sequence do
|
|
1253
|
+
get(:url) { extract_url(row) } # binds the Success value to :url
|
|
1254
|
+
get(:file) { fetch(url) } # :url is available here
|
|
1255
|
+
let(:name) { File.basename(url) } # binds a plain (non-Result) value
|
|
1256
|
+
and_then { validate(file) } # step without binding
|
|
1257
|
+
observe { logger.info("got #{name}") } # side effect, return value ignored
|
|
1258
|
+
and_yield { Success(name) } # final result of the sequence
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
end
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
* `get(:name) { ... }` – runs a step returning a `Result`; on `Success` binds the
|
|
1265
|
+
unwrapped value to `name`, on `Failure` stops the sequence and returns it.
|
|
1266
|
+
* `let(:name) { ... }` – binds the block's plain return value (no `Result` involved).
|
|
1267
|
+
* `and_then { ... }` – runs a step returning a `Result` without binding its value.
|
|
1268
|
+
* `observe { ... }` – runs a side effect; its return value is ignored.
|
|
1269
|
+
* `and_yield { ... }` – mandatory final step; its `Result` is the value of the
|
|
1270
|
+
whole `in_sequence` block.
|
|
1271
|
+
|
|
1153
1272
|
#### Complex Example in a Builder Action <a name="functional-usage-complex-action"></a>
|
|
1154
1273
|
|
|
1155
1274
|
```ruby
|
|
@@ -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
|
-
|
|
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},
|
|
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
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module FunctionalLightService
|
|
6
|
+
# Do-notation for Result: chains steps that return Success/Failure,
|
|
7
|
+
# short-circuiting on the first Failure. Ported from the deterministic
|
|
8
|
+
# gem (MIT License, Copyright (c) Piotr Zolnierek and contributors,
|
|
9
|
+
# https://github.com/pzol/deterministic).
|
|
10
|
+
#
|
|
11
|
+
# in_sequence do
|
|
12
|
+
# get(:user) { fetch_user(id) } # binds the Success value to :user
|
|
13
|
+
# let(:name) { user.fetch(:name) } # binds a plain (non-Result) value
|
|
14
|
+
# and_then { validate(user) } # step without binding
|
|
15
|
+
# observe { log(name) } # side effect, return value ignored
|
|
16
|
+
# and_yield { Success(name) } # final result of the sequence
|
|
17
|
+
# end
|
|
18
|
+
module Sequencer
|
|
19
|
+
class InvalidSequenceError < StandardError; end
|
|
20
|
+
|
|
21
|
+
module Operation
|
|
22
|
+
Get = Struct.new(:block, :name)
|
|
23
|
+
Let = Struct.new(:block, :name)
|
|
24
|
+
AndThen = Struct.new(:block)
|
|
25
|
+
Observe = Struct.new(:block)
|
|
26
|
+
AndYield = Struct.new(:block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def in_sequence(&)
|
|
30
|
+
sequencer = Sequencer.new(self)
|
|
31
|
+
sequencer.instance_eval(&)
|
|
32
|
+
sequencer.yield
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Sequencer
|
|
36
|
+
def initialize(instance)
|
|
37
|
+
@operations = []
|
|
38
|
+
@operation_wrapper = OperationWrapper.new(instance)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get(name, &block)
|
|
42
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
43
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
44
|
+
|
|
45
|
+
@operations << Operation::Get.new(block, name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def let(name, &block)
|
|
49
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
50
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
51
|
+
|
|
52
|
+
@operations << Operation::Let.new(block, name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def and_then(&block)
|
|
56
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
57
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
58
|
+
|
|
59
|
+
@operations << Operation::AndThen.new(block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def observe(&block)
|
|
63
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
64
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
65
|
+
|
|
66
|
+
@operations << Operation::Observe.new(block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def and_yield(&block)
|
|
70
|
+
raise ArgumentError, 'no block given' unless block_given?
|
|
71
|
+
raise InvalidSequenceError, 'and_yield already called' if @sequenced_operations
|
|
72
|
+
|
|
73
|
+
@operations << Operation::AndYield.new(block)
|
|
74
|
+
|
|
75
|
+
prepare_sequenced_operations
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def yield
|
|
79
|
+
raise InvalidSequenceError, 'and_yield not called' unless @sequenced_operations
|
|
80
|
+
|
|
81
|
+
@operation_wrapper.instance_eval(&@sequenced_operations)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
87
|
+
def prepare_sequenced_operations
|
|
88
|
+
operations = @operations
|
|
89
|
+
|
|
90
|
+
@sequenced_operations = ->(_) do
|
|
91
|
+
operations.reduce(Result::Success.new(nil)) do |last_result, operation|
|
|
92
|
+
last_result.map do
|
|
93
|
+
case operation
|
|
94
|
+
when Operation::Get
|
|
95
|
+
result = instance_eval(&operation.block)
|
|
96
|
+
result.map do |output|
|
|
97
|
+
# Runs in the context of the OperationWrapper, so the
|
|
98
|
+
# bound value is stored within the wrapper itself.
|
|
99
|
+
@gotten_results[operation.name] = output
|
|
100
|
+
result
|
|
101
|
+
end
|
|
102
|
+
when Operation::Let
|
|
103
|
+
@gotten_results[operation.name] = instance_eval(&operation.block)
|
|
104
|
+
last_result
|
|
105
|
+
when Operation::Observe
|
|
106
|
+
instance_eval(&operation.block)
|
|
107
|
+
last_result
|
|
108
|
+
when Operation::AndThen, Operation::AndYield
|
|
109
|
+
instance_eval(&operation.block)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# OperationWrapper proxies all method calls to the wrapped instance, but
|
|
119
|
+
# first checks if the name of the called method matches a value stored
|
|
120
|
+
# within @gotten_results and returns the value if it does.
|
|
121
|
+
class OperationWrapper < SimpleDelegator
|
|
122
|
+
def initialize(*args)
|
|
123
|
+
super
|
|
124
|
+
@gotten_results = {}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def method_missing(name, *args, **kwargs, &)
|
|
128
|
+
if @gotten_results.key?(name)
|
|
129
|
+
@gotten_results[name]
|
|
130
|
+
else
|
|
131
|
+
super
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def respond_to_missing?(name, include_private = false)
|
|
136
|
+
@gotten_results.key?(name) || super
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
module Prelude
|
|
142
|
+
include Sequencer
|
|
143
|
+
end
|
|
144
|
+
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
|