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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 936bb237d3decf217151d48e7f32339ac698d65bde22930a05e4b90e92d760ea
4
- data.tar.gz: e9ecc10a25c778a1393f803bd32ad6ebf7481c1a9144a4d0ac0fce843aed53d2
3
+ metadata.gz: 4e57208798d170fbccde7b54f5beb6220cecbfd70c3983fedcb701070a73c7bc
4
+ data.tar.gz: e4a0c472d1fd6edacf6ac3495d902ca851a88f5420360b708bdf2f204818cc1a
5
5
  SHA512:
6
- metadata.gz: 5a17f2832229d00b96976ebd9d75053b2a0790625ebc8323e2d2def3d7f90d8aa78a255c834a53e7dcf88b578c9c7ba294a1097b317d0a0f74a579f1c29986a4
7
- data.tar.gz: 5a357a9a1cbcd6487c62259f85316304348171de648d1bdf35193b92a2952bfc9a51d98d4e16f0572eb7d4e64e12d1c511e161235b3a092176712abcc5e87157
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. If you change a
94
- documented API, update both READMEs in the same commit.
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.3.1 | https://github.com/serradura/u-case/blob/v5.x/README.md
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? 🇧🇷&nbsp;🇵🇹 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.3.1 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
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.3.1 | https://github.com/serradura/u-case/blob/v5.x/README.md
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.3.1 | v5.x | >= 2.7 | >= 6.0 | >= 2.8, < 4.0 |
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
@@ -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
 
@@ -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
@@ -174,14 +174,14 @@ module Micro
174
174
  end
175
175
 
176
176
  def __set__(is_success, data, type, use_case)
177
- raise Error::InvalidResultType unless type.is_a?(Symbol)
178
- raise Error::InvalidUseCase unless use_case.is_a?(::Micro::Case)
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
- raise Micro::Case::Error::InvalidResult.new(is_success, type, use_case) unless @data
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
- raise INVALID_INVOCATION_OF_THE_THEN_METHOD unless ::Micro.case_or_flow?(use_case)
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
- return self if result === self
247
+ ::Micro::Case.check.expected_self_result!(result, self, "#{Result.name}##{expected}")
248
248
 
249
- raise Error::UnexpectedResult.new("#{Result.name}##{expected}")
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
- return self if result === self
257
+ ::Micro::Case.check.expected_self_result!(result, self, "#{use_case.class.name}#method(:#{methd.name})")
258
258
 
259
- raise Error::UnexpectedResult.new("#{use_case.class.name}#method(:#{methd.name})")
259
+ self
260
260
  end
261
261
 
262
262
  def __success_type?(expected_type)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  class Case
5
- VERSION = '5.3.1'.freeze
5
+ VERSION = '5.5.0'.freeze
6
6
  end
7
7
  end
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
- raise INVALID_INVOCATION_OF_THE_THEN_METHOD unless ::Micro.case_or_flow?(use_case)
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
- raise Error::InvalidResultInstance unless result.is_a?(Result)
173
- raise Error::ResultIsAlreadyDefined if defined?(@__result)
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
- raise Error::InvalidUseCase unless ::Micro.case_or_flow?(use_case)
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
- return result if result.is_a?(Result)
235
+ ::Micro::Case.check.expected_result!(result, "#{self.class.name}#call!")
215
236
 
216
- raise Error::UnexpectedResult.new("#{self.class.name}#call!")
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
- Failure(
225
- Config.instance.activemodel_validation_errors_failure,
226
- result: { errors: attributes_errors }
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
 
@@ -11,7 +11,7 @@ module Micro
11
11
  def self.build(args)
12
12
  use_cases = Utils.map_use_cases(args)
13
13
 
14
- raise Error::InvalidUseCases if use_cases.none?(&IsAValidUseCase)
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
- raise_invalid_invocation_of_the_then_method unless ::Micro.case_or_flow?(use_case)
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
@@ -10,7 +10,7 @@ module Micro
10
10
  attr_reader :use_cases
11
11
 
12
12
  def self.build(args)
13
- raise Error::InvalidUseCases unless HasValidArgs[args]
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 = Kind::Hash[arg]
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.3.1
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