u-case 5.3.1 → 5.5.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/CHANGELOG.md +17 -0
- data/CLAUDE.md +49 -2
- data/README.md +68 -2
- data/README.pt-BR.md +70 -2
- data/lib/micro/case/check.rb +115 -0
- data/lib/micro/case/config.rb +12 -0
- data/lib/micro/case/error.rb +24 -1
- data/lib/micro/case/result/contract.rb +60 -0
- data/lib/micro/case/result.rb +8 -8
- data/lib/micro/case/version.rb +1 -1
- data/lib/micro/case.rb +35 -10
- data/lib/micro/cases/flow.rb +2 -2
- data/lib/micro/cases/map.rb +2 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e57208798d170fbccde7b54f5beb6220cecbfd70c3983fedcb701070a73c7bc
|
|
4
|
+
data.tar.gz: e4a0c472d1fd6edacf6ac3495d902ca851a88f5420360b708bdf2f204818cc1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 154ce36f3efefa81e8e0dcc191cfd25396a7ad0b431f6d2a8190974036ed21f70faa617cc6834569f7fe02855b712abb33dca1da44285913c578ff73cc7cdfcc
|
|
7
|
+
data.tar.gz: 20f5e74caf9b58ae935bb39dc9780fd7bcfe21e68f4adbec162cff797284939067b2e254e790a2eb17ac8de3ce7a30460af5908170ba136730fa94b784f84a87
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
> **Note:** This gem was originally published as `u-service` (versions 0.1.0 – 1.0.0) and renamed to `u-case` starting with `u-case 1.0.0` on 2019-09-15.
|
|
9
9
|
|
|
10
|
+
## [5.5.0] - 2026-05-24
|
|
11
|
+
### Added
|
|
12
|
+
- `Micro::Case.results { |on| ... }` macro to declare a results contract — the allowed `Success`/`Failure` types and the result keys each one requires. `Success(...)` / `Failure(...)` calls that use an undeclared type now raise `Micro::Case::Error::UnexpectedResultType`; calls missing a declared required key raise `Micro::Case::Error::MissingResultKeys`. Use cases without a `results` block keep their previous unrestricted behavior. The check routes through `Micro::Case::Check#results_contract!`, so it is also bypassed when `config.disable_runtime_checks = true` (closes #22). Carve-outs so contracts don't break neighbouring features:
|
|
13
|
+
- Framework-generated `__failure_from_attributes_errors` (the auto-failure produced when `accept:`/`reject:` or ActiveModel validation rejects an input) bypasses the contract — it goes directly to `__set__` rather than through `Failure(...)` — so combining `results` with attribute validation no longer requires declaring `:invalid_attributes`.
|
|
14
|
+
- Rescued exceptions in `Micro::Case::Safe` (which produce `Failure(result: exception)`) bypass the contract.
|
|
15
|
+
- Result hashes with `String` keys are matched against the contract's symbolised required keys — `Success(result: { 'value' => 1 })` satisfies `result: [:value]`, mirroring `Result`'s own tolerance for either key type.
|
|
16
|
+
- Non-`Hash` / non-`Symbol` `result:` arguments fall through to the existing `Micro::Case::Error::InvalidResult` ("must be a Hash") instead of being misreported as missing keys.
|
|
17
|
+
- Non-`Symbol` `type` arguments fall through to `Micro::Case::Error::InvalidResultType` instead of being misreported as undeclared.
|
|
18
|
+
- `Micro::Case.results` raises `ArgumentError` when called on the abstract base class itself, so a stray declaration cannot leak a contract to every subclass in the process.
|
|
19
|
+
|
|
20
|
+
## [5.4.0] - 2026-05-24
|
|
21
|
+
### Added
|
|
22
|
+
- `Micro::Case.config.disable_runtime_checks` config (default `false`) to skip the gem's internal argument/contract checks for better performance in production. All checks are consolidated in `Micro::Case::Check::Enabled` (the default) and `Micro::Case::Check::Disabled` (no-ops with the same signature); the active module is swapped via `Micro::Case.check`. Measured throughput win is JIT-dependent: within noise on stock Ruby (no JIT), ~3–5% on Ruby 3.2 +YJIT, ~4–7% on Ruby 4.0 +PRISM (see `benchmarks/perfomance/runtime_checks/compare.rb`). Closes #45.
|
|
23
|
+
- `benchmarks/perfomance/runtime_checks/` — per-mode subprocess benchmark (`checks_enabled.rb`, `checks_disabled.rb`, `compare.rb`) demonstrating the toggle's perf effect across Ruby versions and JIT modes.
|
|
24
|
+
|
|
10
25
|
## [5.3.1] - 2026-05-23
|
|
11
26
|
### Added
|
|
12
27
|
- This `CHANGELOG.md`, covering the full history of the gem (from `u-service 0.1.0` through `u-case 5.3.1`) following the [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) spec.
|
|
@@ -463,6 +478,8 @@ First release under the `u-case` name (renamed from `u-service`).
|
|
|
463
478
|
- `Micro::Service::Result` with `Success`/`Failure` factories and helper methods for returning typed results from services.
|
|
464
479
|
- Runtime dependency on `u-attributes` for service input declaration.
|
|
465
480
|
|
|
481
|
+
[5.5.0]: https://github.com/serradura/u-case/compare/v5.4.0...v5.5.0
|
|
482
|
+
[5.4.0]: https://github.com/serradura/u-case/compare/v5.3.1...v5.4.0
|
|
466
483
|
[5.3.1]: https://github.com/serradura/u-case/compare/v5.3.0...v5.3.1
|
|
467
484
|
[5.3.0]: https://github.com/serradura/u-case/compare/v5.2.1...v5.3.0
|
|
468
485
|
[5.2.1]: https://github.com/serradura/u-case/compare/v5.2.0...v5.2.1
|
data/CLAUDE.md
CHANGED
|
@@ -90,8 +90,55 @@ Both files are user-facing — keep them in sync with the code:
|
|
|
90
90
|
- **`README.md` and `README.pt-BR.md`**: the **Documentation** table and the
|
|
91
91
|
**Compatibility** table at the top reference the latest released version
|
|
92
92
|
and its dependency bounds. Update both files together — they are
|
|
93
|
-
translations of each other and must stay in lockstep.
|
|
94
|
-
|
|
93
|
+
translations of each other and must stay in lockstep. Any user-visible
|
|
94
|
+
API change requires a README update in the same commit:
|
|
95
|
+
- **New public API** (new macro, new module-level method, new public
|
|
96
|
+
instance method, new error class users can rescue, new config option) —
|
|
97
|
+
add or extend the relevant section in both READMEs with an example.
|
|
98
|
+
- **Changed documented API** — update the existing section in both
|
|
99
|
+
READMEs to match the new behavior.
|
|
100
|
+
- **Removed/deprecated API** — remove or mark the section in both
|
|
101
|
+
READMEs.
|
|
102
|
+
- Pure internal refactors, CI tweaks, and test-only changes don't need
|
|
103
|
+
README updates.
|
|
104
|
+
|
|
105
|
+
## Internal argument checks live in `Micro::Case::Check`
|
|
106
|
+
|
|
107
|
+
Every internal argument/contract check that runs inside the gem (type
|
|
108
|
+
guards, "is this a `Micro::Case`?", "is this a `Symbol`?", "are these
|
|
109
|
+
flow args valid?", etc.) lives in `lib/micro/case/check.rb`, split across
|
|
110
|
+
two modules with **identical method signatures**:
|
|
111
|
+
|
|
112
|
+
- `Micro::Case::Check::Enabled` — the default; raises the curated
|
|
113
|
+
`Micro::Case::Error::*` exceptions.
|
|
114
|
+
- `Micro::Case::Check::Disabled` — no-ops (the matching method just
|
|
115
|
+
`return`s; passthrough methods return their input unchanged).
|
|
116
|
+
|
|
117
|
+
The active one is referenced as `Micro::Case.check`, swapped by
|
|
118
|
+
`config.disable_runtime_checks = true/false` (see PR #145 / issue #45).
|
|
119
|
+
|
|
120
|
+
### When you add a new internal check, you must:
|
|
121
|
+
|
|
122
|
+
1. **Add the method to BOTH modules.** Keep the signature identical.
|
|
123
|
+
The `Enabled` side does the real work; the `Disabled` side is a
|
|
124
|
+
no-op (or passthrough for `hash!`-style coercions).
|
|
125
|
+
2. **Route the call site through `Micro::Case.check.<method>!(...)`.**
|
|
126
|
+
Don't `raise ... unless ...` inline — that bypasses the toggle and
|
|
127
|
+
leaks the check into the disabled-path performance budget.
|
|
128
|
+
3. **Cover both modes in a test.** Mirror the pattern in
|
|
129
|
+
`test/micro/case/disable_runtime_checks_test.rb`: one test that the
|
|
130
|
+
`Enabled` side raises, one that the `Disabled` side does not.
|
|
131
|
+
4. **Avoid extra allocation on the call site.** If the curated
|
|
132
|
+
exception needs dynamic params (a class name, a context string),
|
|
133
|
+
pass the raw strings/values into the check method and construct the
|
|
134
|
+
exception inside `Enabled` (only on the raise path). Don't build the
|
|
135
|
+
exception before calling — that defeats the perf rationale of the
|
|
136
|
+
`Disabled` side.
|
|
137
|
+
|
|
138
|
+
This is the only place where new gem-internal checks belong. Inline
|
|
139
|
+
`raise … unless …` inside the runtime call path is a regression of
|
|
140
|
+
this design — flag it during review and move the check into
|
|
141
|
+
`Micro::Case::Check`.
|
|
95
142
|
|
|
96
143
|
## Bumping the version
|
|
97
144
|
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ The main project goals are:
|
|
|
27
27
|
Version | Documentation
|
|
28
28
|
--------- | -------------
|
|
29
29
|
unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
30
|
-
5.
|
|
30
|
+
5.5.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
|
|
31
31
|
4.5.1 | https://github.com/serradura/u-case/blob/v4.x/README.md
|
|
32
32
|
|
|
33
33
|
> **Note:** Você entende português? 🇧🇷 🇵🇹 Verifique o [README traduzido em pt-BR](https://github.com/serradura/u-case/blob/main/README.pt-BR.md).
|
|
@@ -42,6 +42,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
42
42
|
- [What are the default result types?](#what-are-the-default-result-types)
|
|
43
43
|
- [How to define custom result types?](#how-to-define-custom-result-types)
|
|
44
44
|
- [Is it possible to define a custom type without a result data?](#is-it-possible-to-define-a-custom-type-without-a-result-data)
|
|
45
|
+
- [How to declare a results contract?](#how-to-declare-a-results-contract)
|
|
45
46
|
- [How to use the result hooks?](#how-to-use-the-result-hooks)
|
|
46
47
|
- [Why the hook usage without a defined type exposes the result itself?](#why-the-hook-usage-without-a-defined-type-exposes-the-result-itself)
|
|
47
48
|
- [Using decomposition to access the result data and type](#using-decomposition-to-access-the-result-data-and-type)
|
|
@@ -90,7 +91,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
90
91
|
| u-case | branch | ruby | activemodel | u-attributes |
|
|
91
92
|
| ---------------- | ------ | -------- | -------------- | -------------- |
|
|
92
93
|
| unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
93
|
-
| 5.
|
|
94
|
+
| 5.5.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
94
95
|
| 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
|
|
95
96
|
| 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
|
|
96
97
|
|
|
@@ -335,6 +336,58 @@ result.use_case.attributes # {"a"=>2, "b"=>"2"}
|
|
|
335
336
|
|
|
336
337
|
[⬆️ Back to Top](#table-of-contents-)
|
|
337
338
|
|
|
339
|
+
#### How to declare a results contract?
|
|
340
|
+
|
|
341
|
+
Answer: Use the `results do |on| ... end` macro to declare which result types your use case can return, and which keys each one requires. When a contract is declared, `Success(...)` / `Failure(...)` calls that use an undeclared type raise `Micro::Case::Error::UnexpectedResultType`, and calls that omit a declared required key raise `Micro::Case::Error::MissingResultKeys`.
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
class Divide < Micro::Case
|
|
345
|
+
attributes :a, :b
|
|
346
|
+
|
|
347
|
+
results do |on|
|
|
348
|
+
on.failure(:attributes_must_be_numbers)
|
|
349
|
+
on.failure(:division_by_zero)
|
|
350
|
+
|
|
351
|
+
on.success(result: [:division])
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def call!
|
|
355
|
+
return Failure(:attributes_must_be_numbers) unless Kind.of?(Numeric, a, b)
|
|
356
|
+
return Failure(:division_by_zero) if b == 0
|
|
357
|
+
|
|
358
|
+
Success result: { division: a / b }
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
Divide.call(a: 10, b: 2).data # => { division: 5 }
|
|
363
|
+
Divide.call(a: 10, b: 0).type # => :division_by_zero
|
|
364
|
+
Divide.call(a: 'x', b: 2).type # => :attributes_must_be_numbers
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
A type passed to `on.success` / `on.failure` without a `result:` argument declares the type with no required keys (any payload — including the implicit `{ type => true }` from `Failure(:my_type)` — is accepted). When `result: [:key1, :key2]` is given, those keys must be present in the result hash; extra keys are allowed.
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
class Wrong < Micro::Case
|
|
371
|
+
results do |on|
|
|
372
|
+
on.success(result: [:value])
|
|
373
|
+
on.failure(:known)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def call!
|
|
377
|
+
Success(:other, result: { value: 1 }) # raises Micro::Case::Error::UnexpectedResultType
|
|
378
|
+
# Success(result: { wrong: 1 }) # raises Micro::Case::Error::MissingResultKeys
|
|
379
|
+
# Failure(:other) # raises Micro::Case::Error::UnexpectedResultType
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Notes:
|
|
385
|
+
- Use cases without a `results` block keep their previous unrestricted behavior — the contract is opt-in.
|
|
386
|
+
- Subclasses inherit the parent's contract.
|
|
387
|
+
- Rescued exceptions in `Micro::Case::Safe` (which produce `Failure(result: exception)` automatically) bypass the contract.
|
|
388
|
+
|
|
389
|
+
[⬆️ Back to Top](#table-of-contents-)
|
|
390
|
+
|
|
338
391
|
#### How to use the result hooks?
|
|
339
392
|
|
|
340
393
|
As [mentioned earlier](#microcaseresult---what-is-a-use-case-result), the `Micro::Case::Result` has two methods to improve the application flow control. They are: `#on_success`, `on_failure`.
|
|
@@ -1256,9 +1309,22 @@ Micro::Case.config do |config|
|
|
|
1256
1309
|
# - Calling `Micro::Cases.safe_flow(...)`
|
|
1257
1310
|
# - Calling `Micro::Case::Result#on_exception`
|
|
1258
1311
|
config.disable_safe_features = false
|
|
1312
|
+
|
|
1313
|
+
# Use to skip the gem's internal argument/contract checks (e.g., "is this a
|
|
1314
|
+
# Micro::Case?", "is the result type a Symbol?", "is the use case a kind of
|
|
1315
|
+
# Micro::Case?"). Set to `true` in production for a small performance boost
|
|
1316
|
+
# once your code paths are exercised by your test suite. The trade-off is that
|
|
1317
|
+
# incorrect usage will surface as confusing downstream errors instead of the
|
|
1318
|
+
# gem's curated ones (e.g. `Micro::Case::Error::InvalidUseCase`).
|
|
1319
|
+
config.disable_runtime_checks = false
|
|
1259
1320
|
end
|
|
1260
1321
|
```
|
|
1261
1322
|
|
|
1323
|
+
All checks are consolidated in `Micro::Case::Check::Enabled` (the default).
|
|
1324
|
+
Toggling `disable_runtime_checks = true` swaps `Micro::Case.check` to
|
|
1325
|
+
`Micro::Case::Check::Disabled` — a module with the same signature whose
|
|
1326
|
+
methods are no-ops — so the validations themselves are not run on each call.
|
|
1327
|
+
|
|
1262
1328
|
[⬆️ Back to Top](#table-of-contents-)
|
|
1263
1329
|
|
|
1264
1330
|
## Benchmarks
|
data/README.pt-BR.md
CHANGED
|
@@ -27,7 +27,7 @@ Principais objetivos deste projeto:
|
|
|
27
27
|
Versão | Documentação
|
|
28
28
|
--------- | -------------
|
|
29
29
|
unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
30
|
-
5.
|
|
30
|
+
5.5.0 | https://github.com/serradura/u-case/blob/v5.x/README.md
|
|
31
31
|
4.5.1 | https://github.com/serradura/u-case/blob/v4.x/README.md
|
|
32
32
|
|
|
33
33
|
## Índice <!-- omit in toc -->
|
|
@@ -40,6 +40,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
40
40
|
- [O que são os tipos de resultados?](#o-que-são-os-tipos-de-resultados)
|
|
41
41
|
- [Como definir tipos customizados de resultados?](#como-definir-tipos-customizados-de-resultados)
|
|
42
42
|
- [É possível definir um tipo sem definir os dados do resultado?](#é-possível-definir-um-tipo-sem-definir-os-dados-do-resultado)
|
|
43
|
+
- [Como declarar um contrato de resultados?](#como-declarar-um-contrato-de-resultados)
|
|
43
44
|
- [Como utilizar os hooks dos resultados?](#como-utilizar-os-hooks-dos-resultados)
|
|
44
45
|
- [Por que o hook sem um tipo definido expõe o próprio resultado?](#por-que-o-hook-sem-um-tipo-definido-expõe-o-próprio-resultado)
|
|
45
46
|
- [Usando decomposição para acessar os dados e tipo do resultado](#usando-decomposição-para-acessar-os-dados-e-tipo-do-resultado)
|
|
@@ -88,7 +89,7 @@ unreleased| https://github.com/serradura/u-case/blob/main/README.md
|
|
|
88
89
|
| u-case | branch | ruby | activemodel | u-attributes |
|
|
89
90
|
| ---------------- | ------ | -------- | -------------- | -------------- |
|
|
90
91
|
| unreleased | main | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
91
|
-
| 5.
|
|
92
|
+
| 5.5.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
|
|
92
93
|
| 5.1.0 | v5.x | >= 2.7 | >= 6.0 | >= 2.7, < 4.0 |
|
|
93
94
|
| 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, <= 8.1 | >= 2.7, < 3.0 |
|
|
94
95
|
|
|
@@ -333,6 +334,58 @@ result.use_case.attributes # {"a"=>2, "b"=>"2"}
|
|
|
333
334
|
|
|
334
335
|
[⬆️ Voltar para o índice](#índice-)
|
|
335
336
|
|
|
337
|
+
#### Como declarar um contrato de resultados?
|
|
338
|
+
|
|
339
|
+
Resposta: Utilize a macro `results do |on| ... end` para declarar quais tipos de resultado o caso de uso pode retornar e quais chaves cada um exige. Quando há um contrato declarado, chamadas a `Success(...)` / `Failure(...)` que usem um tipo não declarado levantam `Micro::Case::Error::UnexpectedResultType`, e chamadas que omitam uma chave obrigatória declarada levantam `Micro::Case::Error::MissingResultKeys`.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class Divide < Micro::Case
|
|
343
|
+
attributes :a, :b
|
|
344
|
+
|
|
345
|
+
results do |on|
|
|
346
|
+
on.failure(:attributes_must_be_numbers)
|
|
347
|
+
on.failure(:division_by_zero)
|
|
348
|
+
|
|
349
|
+
on.success(result: [:division])
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def call!
|
|
353
|
+
return Failure(:attributes_must_be_numbers) unless Kind.of?(Numeric, a, b)
|
|
354
|
+
return Failure(:division_by_zero) if b == 0
|
|
355
|
+
|
|
356
|
+
Success result: { division: a / b }
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
Divide.call(a: 10, b: 2).data # => { division: 5 }
|
|
361
|
+
Divide.call(a: 10, b: 0).type # => :division_by_zero
|
|
362
|
+
Divide.call(a: 'x', b: 2).type # => :attributes_must_be_numbers
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Um tipo declarado em `on.success` / `on.failure` sem o argumento `result:` é aceito sem chaves obrigatórias (qualquer payload — inclusive o implícito `{ tipo => true }` de `Failure(:meu_tipo)` — é aceito). Quando `result: [:chave_1, :chave_2]` é informado, essas chaves precisam estar presentes no hash de resultado; chaves extras são permitidas.
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
class Wrong < Micro::Case
|
|
369
|
+
results do |on|
|
|
370
|
+
on.success(result: [:value])
|
|
371
|
+
on.failure(:known)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def call!
|
|
375
|
+
Success(:other, result: { value: 1 }) # levanta Micro::Case::Error::UnexpectedResultType
|
|
376
|
+
# Success(result: { wrong: 1 }) # levanta Micro::Case::Error::MissingResultKeys
|
|
377
|
+
# Failure(:other) # levanta Micro::Case::Error::UnexpectedResultType
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Notas:
|
|
383
|
+
- Casos de uso sem o bloco `results` mantêm o comportamento anterior sem restrições — o contrato é opt-in.
|
|
384
|
+
- Subclasses herdam o contrato declarado na classe pai.
|
|
385
|
+
- Exceções capturadas em `Micro::Case::Safe` (que geram `Failure(result: exception)` automaticamente) são exemptas do contrato.
|
|
386
|
+
|
|
387
|
+
[⬆️ Voltar para o índice](#índice-)
|
|
388
|
+
|
|
336
389
|
#### Como utilizar os hooks dos resultados?
|
|
337
390
|
|
|
338
391
|
Como [mencionando anteriormente](#microcaseresult---o-que-é-o-resultado-de-um-caso-de-uso), o `Micro::Case::Result` tem dois métodos para melhorar o controle do fluxo da aplicação. São eles:
|
|
@@ -1257,9 +1310,24 @@ Micro::Case.config do |config|
|
|
|
1257
1310
|
# - Chamar `Micro::Cases.safe_flow(...)`
|
|
1258
1311
|
# - Chamar `Micro::Case::Result#on_exception`
|
|
1259
1312
|
config.disable_safe_features = false
|
|
1313
|
+
|
|
1314
|
+
# Use para pular as verificações internas de argumento/contrato da gem (por
|
|
1315
|
+
# exemplo, "isto é um Micro::Case?", "o tipo do resultado é um Symbol?",
|
|
1316
|
+
# "o use case é um tipo de Micro::Case?"). Defina `true` em produção para
|
|
1317
|
+
# um pequeno ganho de performance depois que seus caminhos de código já
|
|
1318
|
+
# estiverem cobertos pela sua suíte de testes. O custo é que usos
|
|
1319
|
+
# incorretos vão aparecer como erros confusos mais à frente, em vez dos
|
|
1320
|
+
# erros curados pela gem (ex.: `Micro::Case::Error::InvalidUseCase`).
|
|
1321
|
+
config.disable_runtime_checks = false
|
|
1260
1322
|
end
|
|
1261
1323
|
```
|
|
1262
1324
|
|
|
1325
|
+
Todas as verificações estão consolidadas em `Micro::Case::Check::Enabled` (o
|
|
1326
|
+
padrão). Definir `disable_runtime_checks = true` troca `Micro::Case.check` por
|
|
1327
|
+
`Micro::Case::Check::Disabled` — um módulo com a mesma assinatura cujos
|
|
1328
|
+
métodos não fazem nada — de forma que as validações não são executadas a
|
|
1329
|
+
cada chamada.
|
|
1330
|
+
|
|
1263
1331
|
[⬆️ Voltar para o índice](#índice-)
|
|
1264
1332
|
|
|
1265
1333
|
## Benchmarks
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Micro
|
|
4
|
+
class Case
|
|
5
|
+
module Check
|
|
6
|
+
module Enabled
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def use_case_or_flow!(arg)
|
|
10
|
+
raise Error::InvalidUseCase unless ::Micro.case_or_flow?(arg)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def micro_case_instance!(arg)
|
|
14
|
+
raise Error::InvalidUseCase unless arg.is_a?(::Micro::Case)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def result_instance!(arg)
|
|
18
|
+
raise Error::InvalidResultInstance unless arg.is_a?(::Micro::Case::Result)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def result_not_defined!(is_defined)
|
|
22
|
+
raise Error::ResultIsAlreadyDefined if is_defined
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def result_type!(type)
|
|
26
|
+
raise Error::InvalidResultType unless type.is_a?(Symbol)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def result_data!(data, is_success, type, use_case)
|
|
30
|
+
raise Error::InvalidResult.new(is_success, type, use_case) unless data
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def expected_result!(result, context)
|
|
34
|
+
return if result.is_a?(::Micro::Case::Result)
|
|
35
|
+
|
|
36
|
+
raise Error::UnexpectedResult.new(context)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def expected_self_result!(actual, expected, context)
|
|
40
|
+
return if actual.equal?(expected)
|
|
41
|
+
|
|
42
|
+
raise Error::UnexpectedResult.new(context)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def then_use_case_or_flow!(arg, owner_label)
|
|
46
|
+
return if ::Micro.case_or_flow?(arg)
|
|
47
|
+
|
|
48
|
+
raise Error::InvalidInvocationOfTheThenMethod.new(owner_label)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def flow_use_cases!(use_cases)
|
|
52
|
+
raise Cases::Error::InvalidUseCases if use_cases.none?(&::Micro::Cases::Flow::IsAValidUseCase)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def map_args!(args)
|
|
56
|
+
raise Cases::Error::InvalidUseCases unless ::Micro::Cases::Map.const_get(:HasValidArgs, false)[args]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def hash!(arg)
|
|
60
|
+
Kind::Hash[arg]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def results_contract!(use_case_class, kind, type, value)
|
|
64
|
+
contract = use_case_class.__results_contract__
|
|
65
|
+
return unless contract
|
|
66
|
+
return unless type.is_a?(Symbol)
|
|
67
|
+
return if value.is_a?(Exception)
|
|
68
|
+
|
|
69
|
+
if kind == :success
|
|
70
|
+
declared = contract.success_declared?(type)
|
|
71
|
+
declared_types = contract.successes.keys
|
|
72
|
+
required = contract.success_keys(type) if declared
|
|
73
|
+
else
|
|
74
|
+
declared = contract.failure_declared?(type)
|
|
75
|
+
declared_types = contract.failures.keys
|
|
76
|
+
required = contract.failure_keys(type) if declared
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise Error::UnexpectedResultType.new(use_case_class, kind, type, declared_types) unless declared
|
|
80
|
+
return if required.nil? || required.empty?
|
|
81
|
+
|
|
82
|
+
if value.is_a?(Hash)
|
|
83
|
+
data_keys = value.keys.map { |k| k.is_a?(String) ? k.to_sym : k }
|
|
84
|
+
elsif value.is_a?(Symbol)
|
|
85
|
+
data_keys = [type]
|
|
86
|
+
else
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
missing = required - data_keys
|
|
91
|
+
|
|
92
|
+
raise Error::MissingResultKeys.new(use_case_class, kind, type, missing) unless missing.empty?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module Disabled
|
|
97
|
+
extend self
|
|
98
|
+
|
|
99
|
+
def use_case_or_flow!(_arg); end
|
|
100
|
+
def micro_case_instance!(_arg); end
|
|
101
|
+
def result_instance!(_arg); end
|
|
102
|
+
def result_not_defined!(_is_defined); end
|
|
103
|
+
def result_type!(_type); end
|
|
104
|
+
def result_data!(_data, _is_success, _type, _use_case); end
|
|
105
|
+
def expected_result!(_result, _context); end
|
|
106
|
+
def expected_self_result!(_actual, _expected, _context); end
|
|
107
|
+
def then_use_case_or_flow!(_arg, _owner_label); end
|
|
108
|
+
def flow_use_cases!(_use_cases); end
|
|
109
|
+
def map_args!(_args); end
|
|
110
|
+
def hash!(arg); arg; end
|
|
111
|
+
def results_contract!(_use_case_class, _kind, _type, _value); end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/micro/case/config.rb
CHANGED
|
@@ -23,6 +23,18 @@ module Micro
|
|
|
23
23
|
@disable_safe_features = false
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def disable_runtime_checks=(value)
|
|
27
|
+
@disable_runtime_checks = Kind::Boolean[value]
|
|
28
|
+
|
|
29
|
+
::Micro::Case.check = @disable_runtime_checks ? ::Micro::Case::Check::Disabled : ::Micro::Case::Check::Enabled
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def disable_runtime_checks
|
|
33
|
+
return @disable_runtime_checks if defined?(@disable_runtime_checks)
|
|
34
|
+
|
|
35
|
+
@disable_runtime_checks = false
|
|
36
|
+
end
|
|
37
|
+
|
|
26
38
|
def enable_activemodel_validation=(value)
|
|
27
39
|
return unless Kind::Boolean[value]
|
|
28
40
|
|
data/lib/micro/case/error.rb
CHANGED
|
@@ -60,9 +60,32 @@ module Micro
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
class UnexpectedResultType < TypeError
|
|
64
|
+
def initialize(use_case_class, kind, type, declared_types)
|
|
65
|
+
declared_list = declared_types.map { |t| ":#{t}" }.join(', ')
|
|
66
|
+
declared_list = '(none)' if declared_list.empty?
|
|
67
|
+
|
|
68
|
+
super(
|
|
69
|
+
"#{use_case_class.name} declared a results contract — " \
|
|
70
|
+
"#{kind} type :#{type} is not declared. Declared #{kind} types: #{declared_list}."
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class MissingResultKeys < ArgumentError
|
|
76
|
+
def initialize(use_case_class, kind, type, missing_keys)
|
|
77
|
+
missing_list = missing_keys.map { |k| ":#{k}" }.join(', ')
|
|
78
|
+
|
|
79
|
+
super(
|
|
80
|
+
"#{use_case_class.name} declared a results contract — " \
|
|
81
|
+
"#{kind} :#{type} is missing required result keys: #{missing_list}."
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
63
86
|
def self.by_wrong_usage?(exception)
|
|
64
87
|
case exception
|
|
65
|
-
when Kind::Error, ArgumentError, InvalidResult, UnexpectedResult then true
|
|
88
|
+
when Kind::Error, ArgumentError, InvalidResult, UnexpectedResult, UnexpectedResultType then true
|
|
66
89
|
else false
|
|
67
90
|
end
|
|
68
91
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Micro
|
|
4
|
+
class Case
|
|
5
|
+
class Result
|
|
6
|
+
class Contract
|
|
7
|
+
attr_reader :successes, :failures
|
|
8
|
+
|
|
9
|
+
def self.define(&block)
|
|
10
|
+
contract = new
|
|
11
|
+
block.call(Definition.new(contract))
|
|
12
|
+
contract
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@successes = {}
|
|
17
|
+
@failures = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_success(type, keys)
|
|
21
|
+
@successes[type] = Array(keys).map(&:to_sym)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_failure(type, keys)
|
|
25
|
+
@failures[type] = Array(keys).map(&:to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def success_declared?(type)
|
|
29
|
+
@successes.key?(type)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def failure_declared?(type)
|
|
33
|
+
@failures.key?(type)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def success_keys(type)
|
|
37
|
+
@successes[type]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def failure_keys(type)
|
|
41
|
+
@failures[type]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Definition
|
|
45
|
+
def initialize(contract)
|
|
46
|
+
@contract = contract
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def success(type = :ok, result: nil)
|
|
50
|
+
@contract.add_success(Kind::Symbol[type], result)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def failure(type = :error, result: nil)
|
|
54
|
+
@contract.add_failure(Kind::Symbol[type], result)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/micro/case/result.rb
CHANGED
|
@@ -174,14 +174,14 @@ module Micro
|
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def __set__(is_success, data, type, use_case)
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
::Micro::Case.check.result_type!(type)
|
|
178
|
+
::Micro::Case.check.micro_case_instance!(use_case)
|
|
179
179
|
|
|
180
180
|
@__success, @type, @use_case = is_success, type, use_case
|
|
181
181
|
|
|
182
182
|
@data = FetchData.call(data).freeze
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
::Micro::Case.check.result_data!(@data, is_success, type, use_case)
|
|
185
185
|
|
|
186
186
|
@__accumulated_data.merge!(@data)
|
|
187
187
|
|
|
@@ -227,7 +227,7 @@ module Micro
|
|
|
227
227
|
use_case_method = self.use_case.method(use_case)
|
|
228
228
|
__call_method(use_case_method, attributes)
|
|
229
229
|
else
|
|
230
|
-
|
|
230
|
+
::Micro::Case.check.then_use_case_or_flow!(use_case, 'Micro::Case::Result#')
|
|
231
231
|
|
|
232
232
|
input = attributes.is_a?(Hash) ? self.data.merge(attributes) : self.data
|
|
233
233
|
|
|
@@ -244,9 +244,9 @@ module Micro
|
|
|
244
244
|
|
|
245
245
|
result = fn.arity.zero? ? fn.call : fn.call(__fetch_accessible_attributes)
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
::Micro::Case.check.expected_self_result!(result, self, "#{Result.name}##{expected}")
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
self
|
|
250
250
|
end
|
|
251
251
|
|
|
252
252
|
def __call_method(methd, attributes = nil)
|
|
@@ -254,9 +254,9 @@ module Micro
|
|
|
254
254
|
|
|
255
255
|
result = methd.arity.zero? ? methd.call : methd.call(**__fetch_accessible_attributes)
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
::Micro::Case.check.expected_self_result!(result, self, "#{use_case.class.name}#method(:#{methd.name})")
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
self
|
|
260
260
|
end
|
|
261
261
|
|
|
262
262
|
def __success_type?(expected_type)
|
data/lib/micro/case/version.rb
CHANGED
data/lib/micro/case.rb
CHANGED
|
@@ -11,12 +11,19 @@ module Micro
|
|
|
11
11
|
require 'micro/case/utils'
|
|
12
12
|
require 'micro/case/error'
|
|
13
13
|
require 'micro/case/result'
|
|
14
|
+
require 'micro/case/result/contract'
|
|
15
|
+
require 'micro/case/check'
|
|
14
16
|
require 'micro/case/config'
|
|
15
17
|
require 'micro/case/safe'
|
|
16
18
|
require 'micro/case/strict'
|
|
17
19
|
|
|
18
20
|
require 'micro/cases'
|
|
19
21
|
|
|
22
|
+
class << self
|
|
23
|
+
attr_accessor :check
|
|
24
|
+
end
|
|
25
|
+
self.check = Check::Enabled
|
|
26
|
+
|
|
20
27
|
include Micro::Attributes
|
|
21
28
|
include Micro::Attributes::Features::Accept
|
|
22
29
|
|
|
@@ -46,7 +53,7 @@ module Micro
|
|
|
46
53
|
else
|
|
47
54
|
return yield_self if !use_case && can_yield_self
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
::Micro::Case.check.then_use_case_or_flow!(use_case, 'Micro::Case.')
|
|
50
57
|
|
|
51
58
|
self.call.then(use_case)
|
|
52
59
|
end
|
|
@@ -60,6 +67,20 @@ module Micro
|
|
|
60
67
|
@__flow_use_cases = Cases::Utils.map_use_cases(args)
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
def self.results(&block)
|
|
71
|
+
raise ArgumentError, 'a block is required'.freeze unless block
|
|
72
|
+
raise ArgumentError, 'must be called on a Micro::Case subclass, not on Micro::Case itself'.freeze if self == ::Micro::Case
|
|
73
|
+
|
|
74
|
+
@__results_contract = Result::Contract.define(&block)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.__results_contract__
|
|
78
|
+
return @__results_contract if defined?(@__results_contract)
|
|
79
|
+
|
|
80
|
+
parent = superclass
|
|
81
|
+
parent.respond_to?(:__results_contract__) ? parent.__results_contract__ : nil
|
|
82
|
+
end
|
|
83
|
+
|
|
63
84
|
class << self
|
|
64
85
|
alias __call__ call
|
|
65
86
|
|
|
@@ -169,8 +190,8 @@ module Micro
|
|
|
169
190
|
end
|
|
170
191
|
|
|
171
192
|
def __set_result__(result)
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
::Micro::Case.check.result_instance!(result)
|
|
194
|
+
::Micro::Case.check.result_not_defined!(defined?(@__result))
|
|
174
195
|
|
|
175
196
|
@__result = result
|
|
176
197
|
|
|
@@ -180,7 +201,7 @@ module Micro
|
|
|
180
201
|
private
|
|
181
202
|
|
|
182
203
|
def call(use_case, defaults = Kind::Empty::HASH)
|
|
183
|
-
|
|
204
|
+
::Micro::Case.check.use_case_or_flow!(use_case)
|
|
184
205
|
|
|
185
206
|
input =
|
|
186
207
|
defaults.empty? ? attributes : attributes.merge(Utils::Hashes.stringify_keys(defaults))
|
|
@@ -211,9 +232,9 @@ module Micro
|
|
|
211
232
|
|
|
212
233
|
result = call!
|
|
213
234
|
|
|
214
|
-
|
|
235
|
+
::Micro::Case.check.expected_result!(result, "#{self.class.name}#call!")
|
|
215
236
|
|
|
216
|
-
|
|
237
|
+
result
|
|
217
238
|
end
|
|
218
239
|
|
|
219
240
|
def __attributes_errors_present?
|
|
@@ -221,9 +242,10 @@ module Micro
|
|
|
221
242
|
end
|
|
222
243
|
|
|
223
244
|
def __failure_from_attributes_errors
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
245
|
+
__get_result(
|
|
246
|
+
false,
|
|
247
|
+
{ errors: attributes_errors },
|
|
248
|
+
Config.instance.activemodel_validation_errors_failure
|
|
227
249
|
)
|
|
228
250
|
end
|
|
229
251
|
|
|
@@ -238,6 +260,8 @@ module Micro
|
|
|
238
260
|
def Success(type = :ok, result: nil)
|
|
239
261
|
value = result || type
|
|
240
262
|
|
|
263
|
+
::Micro::Case.check.results_contract!(self.class, :success, type, value)
|
|
264
|
+
|
|
241
265
|
__get_result(true, value, type)
|
|
242
266
|
end
|
|
243
267
|
|
|
@@ -254,10 +278,11 @@ module Micro
|
|
|
254
278
|
|
|
255
279
|
type = MapFailureType.call(value, type)
|
|
256
280
|
|
|
281
|
+
::Micro::Case.check.results_contract!(self.class, :failure, type, value)
|
|
282
|
+
|
|
257
283
|
__get_result(false, value, type)
|
|
258
284
|
end
|
|
259
285
|
|
|
260
|
-
|
|
261
286
|
def Check(type = nil, result: nil, on: Kind::Empty::HASH)
|
|
262
287
|
result_key = type || :check
|
|
263
288
|
|
data/lib/micro/cases/flow.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Micro
|
|
|
11
11
|
def self.build(args)
|
|
12
12
|
use_cases = Utils.map_use_cases(args)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
::Micro::Case.check.flow_use_cases!(use_cases)
|
|
15
15
|
|
|
16
16
|
new(use_cases)
|
|
17
17
|
end
|
|
@@ -63,7 +63,7 @@ module Micro
|
|
|
63
63
|
else
|
|
64
64
|
return yield_self if !use_case && can_yield_self
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
::Micro::Case.check.then_use_case_or_flow!(use_case, "#{self.class.name}#")
|
|
67
67
|
|
|
68
68
|
self.call.then(use_case)
|
|
69
69
|
end
|
data/lib/micro/cases/map.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Micro
|
|
|
10
10
|
attr_reader :use_cases
|
|
11
11
|
|
|
12
12
|
def self.build(args)
|
|
13
|
-
|
|
13
|
+
::Micro::Case.check.map_args!(args)
|
|
14
14
|
|
|
15
15
|
new(args)
|
|
16
16
|
end
|
|
@@ -30,7 +30,7 @@ module Micro
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def call(arg = {})
|
|
33
|
-
hash =
|
|
33
|
+
hash = ::Micro::Case.check.hash!(arg)
|
|
34
34
|
|
|
35
35
|
use_cases.map(&GetUseCaseResult[hash])
|
|
36
36
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: u-case
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rodrigo Serradura
|
|
@@ -118,9 +118,11 @@ files:
|
|
|
118
118
|
- gemfiles/rails_8_1.gemfile
|
|
119
119
|
- gemfiles/rails_edge.gemfile
|
|
120
120
|
- lib/micro/case.rb
|
|
121
|
+
- lib/micro/case/check.rb
|
|
121
122
|
- lib/micro/case/config.rb
|
|
122
123
|
- lib/micro/case/error.rb
|
|
123
124
|
- lib/micro/case/result.rb
|
|
125
|
+
- lib/micro/case/result/contract.rb
|
|
124
126
|
- lib/micro/case/result/transitions.rb
|
|
125
127
|
- lib/micro/case/result/wrapper.rb
|
|
126
128
|
- lib/micro/case/safe.rb
|