u-case 2.2.0 → 2.6.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/.tool-versions +1 -1
- data/.travis.sh +5 -0
- data/.travis.yml +1 -0
- data/Gemfile +14 -1
- data/README.md +361 -76
- data/lib/micro/case.rb +47 -19
- data/lib/micro/case/error.rb +25 -9
- data/lib/micro/case/flow.rb +1 -1
- data/lib/micro/case/flow/reducer.rb +45 -16
- data/lib/micro/case/result.rb +91 -3
- data/lib/micro/case/safe/flow.rb +1 -1
- data/lib/micro/case/utils.rb +19 -0
- data/lib/micro/case/version.rb +1 -1
- data/lib/micro/case/{with_validation.rb → with_activemodel_validation.rb} +7 -5
- data/lib/u-case/with_activemodel_validation.rb +5 -0
- data/lib/u-case/with_validation.rb +4 -1
- data/u-case.gemspec +3 -14
- metadata +22 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de7b8e442e0b578b478d9d63f05901c9023884f3f2b15a1819228af36fe7afc5
|
4
|
+
data.tar.gz: 50e9128500306350a56102b93ca8bd171c75ecae24899c6157a7955e845221a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93fcec72232ac2d1ce479cdc2efa02d7b4eddf0d3b57e2c93346ecb6359a0f9ccba33f7851b2115c813ef53f79f9276984864f172c341be23be8c11120e933fe
|
7
|
+
data.tar.gz: 9993e5cdcbe6f1e864c32a9db7f637c1cdb839bd838d1823e64b2646b704b079d9f83915af65e364abe8edd66034952202909a42833a634d04aeac11eee384bd
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 2.6.
|
1
|
+
ruby 2.6.5
|
data/.travis.sh
CHANGED
@@ -11,3 +11,8 @@ if [[ ! $ruby_v =~ '2.2.0' ]]; then
|
|
11
11
|
ACTIVEMODEL_VERSION='5.2' bundle update
|
12
12
|
ACTIVEMODEL_VERSION='5.2' bundle exec rake test
|
13
13
|
fi
|
14
|
+
|
15
|
+
if [[ $ruby_v =~ '2.5.' ]] || [[ $ruby_v =~ '2.6.' ]] || [[ $ruby_v =~ '2.7.' ]]; then
|
16
|
+
ACTIVEMODEL_VERSION='6.0' bundle update
|
17
|
+
ACTIVEMODEL_VERSION='6.0' bundle exec rake test
|
18
|
+
fi
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -7,6 +7,7 @@ activemodel_version = ENV.fetch('ACTIVEMODEL_VERSION', '6.1.0')
|
|
7
7
|
activemodel = case activemodel_version
|
8
8
|
when '3.2' then '3.2.22'
|
9
9
|
when '5.2' then '5.2.3'
|
10
|
+
when '6.0' then '6.0.2'
|
10
11
|
end
|
11
12
|
|
12
13
|
if activemodel_version < '6.1.0'
|
@@ -17,7 +18,19 @@ end
|
|
17
18
|
group :test do
|
18
19
|
gem 'minitest', activemodel_version < '4.1' ? '~> 4.2' : '~> 5.0'
|
19
20
|
gem 'simplecov', require: false
|
20
|
-
|
21
|
+
end
|
22
|
+
|
23
|
+
pry_byebug_version =
|
24
|
+
case RUBY_VERSION
|
25
|
+
when /\A2.2/ then '3.6'
|
26
|
+
when /\A2.3/ then '3.7'
|
27
|
+
else '3.9'
|
28
|
+
end
|
29
|
+
|
30
|
+
group :development, :test do
|
31
|
+
gem 'awesome_print', '~> 1.8'
|
32
|
+
|
33
|
+
gem 'pry-byebug', "~> #{pry_byebug_version}"
|
21
34
|
end
|
22
35
|
|
23
36
|
# Specify your gem's dependencies in u-case.gemspec
|
data/README.md
CHANGED
@@ -1,15 +1,16 @@
|
|
1
|
+

|
1
2
|
[](https://rubygems.org/gems/u-case)
|
2
3
|
[](https://travis-ci.com/serradura/u-case)
|
3
4
|
[](https://codeclimate.com/github/serradura/u-case/maintainability)
|
4
5
|
[](https://codeclimate.com/github/serradura/u-case/test_coverage)
|
5
6
|
|
6
|
-
μ-case (Micro::Case)
|
7
|
-
|
7
|
+
μ-case (Micro::Case) <!-- omit in toc -->
|
8
|
+
====================
|
8
9
|
|
9
10
|
Create simple and powerful use cases as objects.
|
10
11
|
|
11
12
|
The main project goals are:
|
12
|
-
1.
|
13
|
+
1. Easy to use and easy to learn (input **>>** process **>>** output).
|
13
14
|
2. Promote referential transparency (transforming instead of modifying) and data integrity.
|
14
15
|
3. No callbacks (e.g: before, after, around).
|
15
16
|
4. Solve complex business logic, by allowing the composition of use cases.
|
@@ -18,43 +19,49 @@ The main project goals are:
|
|
18
19
|
> Note: Check out the repo https://github.com/serradura/from-fat-controllers-to-use-cases to see a Rails application that uses this gem to handle its business logic.
|
19
20
|
|
20
21
|
## Table of Contents <!-- omit in toc -->
|
21
|
-
- [
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
- [
|
26
|
-
|
27
|
-
- [
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
- [
|
39
|
-
|
40
|
-
- [
|
41
|
-
|
42
|
-
- [
|
43
|
-
- [Micro::Case](#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
- [
|
48
|
-
|
49
|
-
- [
|
50
|
-
- [
|
51
|
-
- [
|
52
|
-
- [
|
53
|
-
|
54
|
-
- [
|
55
|
-
|
56
|
-
- [
|
57
|
-
- [
|
22
|
+
- [Required Ruby version](#required-ruby-version)
|
23
|
+
- [Dependencies](#dependencies)
|
24
|
+
- [Installation](#installation)
|
25
|
+
- [Usage](#usage)
|
26
|
+
- [`Micro::Case` - How to define a use case?](#microcase---how-to-define-a-use-case)
|
27
|
+
- [`Micro::Case::Result` - What is a use case result?](#microcaseresult---what-is-a-use-case-result)
|
28
|
+
- [What are the default result types?](#what-are-the-default-result-types)
|
29
|
+
- [How to define custom result types?](#how-to-define-custom-result-types)
|
30
|
+
- [Is it possible to define a custom result type without a block?](#is-it-possible-to-define-a-custom-result-type-without-a-block)
|
31
|
+
- [How to use the result hooks?](#how-to-use-the-result-hooks)
|
32
|
+
- [Why the failure hook (without a type) exposes a different kind of data?](#why-the-failure-hook-without-a-type-exposes-a-different-kind-of-data)
|
33
|
+
- [What happens if a result hook was declared multiple times?](#what-happens-if-a-result-hook-was-declared-multiple-times)
|
34
|
+
- [How to use the `Micro::Case::Result#then` method?](#how-to-use-the-microcaseresultthen-method)
|
35
|
+
- [How to make attributes data injection using this feature?](#how-to-make-attributes-data-injection-using-this-feature)
|
36
|
+
- [`Micro::Case::Flow` - How to compose use cases?](#microcaseflow---how-to-compose-use-cases)
|
37
|
+
- [Is it possible to compose a use case flow with other ones?](#is-it-possible-to-compose-a-use-case-flow-with-other-ones)
|
38
|
+
- [Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases)
|
39
|
+
- [How to understand what is happening during a flow execution?](#how-to-understand-what-is-happening-during-a-flow-execution)
|
40
|
+
- [`Micro::Case::Result#transitions` schema](#microcaseresulttransitions-schema)
|
41
|
+
- [Is it possible to declare a flow which includes the use case itself?](#is-it-possible-to-declare-a-flow-which-includes-the-use-case-itself)
|
42
|
+
- [`Micro::Case::Strict` - What is a strict use case?](#microcasestrict---what-is-a-strict-use-case)
|
43
|
+
- [`Micro::Case::Safe` - Is there some feature to auto handle exceptions inside of a use case or flow?](#microcasesafe---is-there-some-feature-to-auto-handle-exceptions-inside-of-a-use-case-or-flow)
|
44
|
+
- [`Micro::Case::Safe::Flow`](#microcasesafeflow)
|
45
|
+
- [`Micro::Case::Result#on_exception`](#microcaseresulton_exception)
|
46
|
+
- [`u-case/with_activemodel_validation` - How to validate use case attributes?](#u-casewith_activemodel_validation---how-to-validate-use-case-attributes)
|
47
|
+
- [If I enabled the auto validation, is it possible to disable it only in specific use case classes?](#if-i-enabled-the-auto-validation-is-it-possible-to-disable-it-only-in-specific-use-case-classes)
|
48
|
+
- [`Kind::Validator`](#kindvalidator)
|
49
|
+
- [Benchmarks](#benchmarks)
|
50
|
+
- [`Micro::Case`](#microcase)
|
51
|
+
- [Best overall](#best-overall)
|
52
|
+
- [Success results](#success-results)
|
53
|
+
- [Failure results](#failure-results)
|
54
|
+
- [`Micro::Case::Flow`](#microcaseflow)
|
55
|
+
- [Comparisons](#comparisons)
|
56
|
+
- [Examples](#examples)
|
57
|
+
- [1️⃣ Rails App (API)](#1️⃣-rails-app-api)
|
58
|
+
- [2️⃣ CLI calculator](#2️⃣-cli-calculator)
|
59
|
+
- [3️⃣ Users creation](#3️⃣-users-creation)
|
60
|
+
- [4️⃣ Rescuing exception inside of the use cases](#4️⃣-rescuing-exception-inside-of-the-use-cases)
|
61
|
+
- [Development](#development)
|
62
|
+
- [Contributing](#contributing)
|
63
|
+
- [License](#license)
|
64
|
+
- [Code of Conduct](#code-of-conduct)
|
58
65
|
|
59
66
|
## Required Ruby version
|
60
67
|
|
@@ -62,8 +69,15 @@ The main project goals are:
|
|
62
69
|
|
63
70
|
## Dependencies
|
64
71
|
|
65
|
-
|
66
|
-
|
72
|
+
1. [`kind`](https://github.com/serradura/kind) gem.
|
73
|
+
|
74
|
+
A simple type system (at runtime) for Ruby.
|
75
|
+
|
76
|
+
Used to validate method inputs, expose `Kind.of.Micro::Case::Result` type checker and its [`activemodel validation`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations) module is auto required by [`u-case/with_activemodel_validation`](#u-casewith_activemodel_validation---how-to-validate-use-case-attributes) mode.
|
77
|
+
2. [`u-attributes`](https://github.com/serradura/u-attributes) gem.
|
78
|
+
|
79
|
+
This gem allows defining read-only attributes, that is, your objects will have only getters to access their attributes data.
|
80
|
+
It is used to define the use case attributes.
|
67
81
|
|
68
82
|
## Installation
|
69
83
|
|
@@ -145,6 +159,7 @@ A `Micro::Case::Result` stores the use cases output data. These are their main m
|
|
145
159
|
- `#type` a Symbol which gives meaning for the result, this is useful to declare different types of failures or success.
|
146
160
|
- `#on_success` or `#on_failure` are hook methods that help you define the application flow.
|
147
161
|
- `#use_case` if is a failure result, the use case responsible for it will be accessible through this method. This feature is handy to handle a flow failure (this topic will be covered ahead).
|
162
|
+
- `#then` allows if the current result is a success, the `then` method will allow to applying a new use case for its value.
|
148
163
|
|
149
164
|
[⬆️ Back to Top](#table-of-contents-)
|
150
165
|
|
@@ -415,6 +430,65 @@ accum # 24
|
|
415
430
|
result.value * 4 == accum # true
|
416
431
|
```
|
417
432
|
|
433
|
+
#### How to use the `Micro::Case::Result#then` method?
|
434
|
+
|
435
|
+
This method allows you to create dynamic flows, so, with it,
|
436
|
+
you can add new use cases or flows to continue the result transformation. e.g:
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
class ForbidNegativeNumber < Micro::Case
|
440
|
+
attribute :number
|
441
|
+
|
442
|
+
def call!
|
443
|
+
return Success { attributes } if number >= 0
|
444
|
+
|
445
|
+
Failure { attributes }
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
class Add3 < Micro::Case
|
450
|
+
attribute :number
|
451
|
+
|
452
|
+
def call!
|
453
|
+
Success { { number: number + 3 } }
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
result1 =
|
458
|
+
ForbidNegativeNumber
|
459
|
+
.call(number: -1)
|
460
|
+
.then(Add3)
|
461
|
+
|
462
|
+
result1.value # {'number' => -1}
|
463
|
+
result1.failure? # true
|
464
|
+
|
465
|
+
# ---
|
466
|
+
|
467
|
+
result2 =
|
468
|
+
ForbidNegativeNumber
|
469
|
+
.call(number: 1)
|
470
|
+
.then(Add3)
|
471
|
+
|
472
|
+
result2.value # {'number' => 4}
|
473
|
+
result2.success? # true
|
474
|
+
```
|
475
|
+
|
476
|
+
> **Note:** this method changes the [`Micro::Case::Result#transitions`](#how-to-understand-what-is-happening-during-a-flow-execution).
|
477
|
+
|
478
|
+
[⬆️ Back to Top](#table-of-contents-)
|
479
|
+
|
480
|
+
##### How to make attributes data injection using this feature?
|
481
|
+
|
482
|
+
Pass a Hash as the second argument of the `Micro::Case::Result#then` method.
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
Todo::FindAllForUser
|
486
|
+
.call(user: current_user, params: params)
|
487
|
+
.then(Paginate)
|
488
|
+
.then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
|
489
|
+
.on_success { |result| render_json(200, data: result[:todos]) }
|
490
|
+
```
|
491
|
+
|
418
492
|
[⬆️ Back to Top](#table-of-contents-)
|
419
493
|
|
420
494
|
### `Micro::Case::Flow` - How to compose use cases?
|
@@ -598,12 +672,149 @@ Note: You can blend any of the [available syntaxes/approaches](#how-to-create-a-
|
|
598
672
|
|
599
673
|
[⬆️ Back to Top](#table-of-contents-)
|
600
674
|
|
601
|
-
#### Is it possible a flow accumulates its input and merges each success result to use as the argument of
|
675
|
+
#### Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?
|
676
|
+
|
677
|
+
Answer: Yes, it is! Look at the example below to understand how the data accumulation works inside of the flow execution.
|
678
|
+
|
679
|
+
```ruby
|
680
|
+
module Users
|
681
|
+
class Find < Micro::Case
|
682
|
+
attribute :email
|
683
|
+
|
684
|
+
def call!
|
685
|
+
user = User.find_by(email: email)
|
686
|
+
|
687
|
+
return Success { { user: user } } if user
|
688
|
+
|
689
|
+
Failure(:user_not_found)
|
690
|
+
end
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
module Users
|
695
|
+
class ValidatePassword < Micro::Case::Strict
|
696
|
+
attributes :user, :password
|
697
|
+
|
698
|
+
def call!
|
699
|
+
return Failure(:user_must_be_persisted) if user.new_record?
|
700
|
+
return Failure(:wrong_password) if user.wrong_password?(password)
|
701
|
+
|
702
|
+
return Success { attributes(:user) }
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
module Users
|
708
|
+
Authenticate = Micro::Case::Flow([
|
709
|
+
Find,
|
710
|
+
ValidatePassword
|
711
|
+
])
|
712
|
+
end
|
713
|
+
|
714
|
+
Users::Authenticate
|
715
|
+
.call(email: 'somebody@test.com', password: 'password')
|
716
|
+
.on_success { |result| sign_in(result[:user]) }
|
717
|
+
.on_failure(:wrong_password) { |result| render status: 401 }
|
718
|
+
.on_failure(:user_not_found) { |result| render status: 404 }
|
719
|
+
```
|
602
720
|
|
603
|
-
|
721
|
+
First, lets see the attribute of each use case:
|
722
|
+
|
723
|
+
```ruby
|
724
|
+
class Users::Find < Micro::Case
|
725
|
+
attribute :email
|
726
|
+
end
|
727
|
+
|
728
|
+
class Users::ValidatePassword < Micro::Case
|
729
|
+
attributes :user, :password
|
730
|
+
end
|
731
|
+
```
|
732
|
+
|
733
|
+
As you can see the `Users::ValidatePassword` expects a user as its input. So, how does it receives the user?
|
734
|
+
It receives the user from the `Users::Find` success result!
|
735
|
+
|
736
|
+
And this, is the power of use cases composition because the output
|
737
|
+
of one flow will compose the input of the next use case in the flow!
|
738
|
+
|
739
|
+
> input **>>** process **>>** output
|
740
|
+
|
741
|
+
> **Note:** Check out these test examples [Micro::Case::Flow](https://github.com/serradura/u-case/blob/b6d63b0db0caada67d2a6cf5cc5937000c0acf04/test/micro/case/flow/reducer_test.rb) and [Micro::Case::Safe::Flow](https://github.com/serradura/u-case/blob/b1d84b355f2b92d329e10d5d56d8012df1d32681/test/micro/case/safe/flow/reducer_test.rb) to see different use cases sharing their own data.
|
604
742
|
|
605
743
|
[⬆️ Back to Top](#table-of-contents-)
|
606
744
|
|
745
|
+
#### How to understand what is happening during a flow execution?
|
746
|
+
|
747
|
+
Use `Micro::Case::Result#transitions`!
|
748
|
+
|
749
|
+
Let's use the [previous section example](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases) to ilustrate how to use this feature.
|
750
|
+
|
751
|
+
```ruby
|
752
|
+
user_authenticated =
|
753
|
+
Users::Authenticate.call(email: 'rodrigo@test.com', password: user_password)
|
754
|
+
|
755
|
+
user_authenticated.transitions
|
756
|
+
[
|
757
|
+
{
|
758
|
+
:use_case => {
|
759
|
+
:class => Users::Find,
|
760
|
+
:attributes => { :email => "rodrigo@test.com" }
|
761
|
+
},
|
762
|
+
:success => {
|
763
|
+
:type => :ok,
|
764
|
+
:value => {
|
765
|
+
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
766
|
+
}
|
767
|
+
},
|
768
|
+
:accessible_attributes => [ :email, :password ]
|
769
|
+
},
|
770
|
+
{
|
771
|
+
:use_case => {
|
772
|
+
:class => Users::ValidatePassword,
|
773
|
+
:attributes => {
|
774
|
+
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
775
|
+
:password => "123456"
|
776
|
+
}
|
777
|
+
},
|
778
|
+
:success => {
|
779
|
+
:type => :ok,
|
780
|
+
:value => {
|
781
|
+
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
|
782
|
+
}
|
783
|
+
},
|
784
|
+
:accessible_attributes => [ :email, :password, :user ]
|
785
|
+
}
|
786
|
+
]
|
787
|
+
```
|
788
|
+
|
789
|
+
The example above shows the output generated by the `Micro::Case::Result#transitions`.
|
790
|
+
With it is possible to analyze the use cases execution order and what were the given `inputs` (attributes) and `outputs` (`success.value`) in the entire execution.
|
791
|
+
|
792
|
+
And look up the `accessible_attributes` property, because it shows whats attributes are accessible in that flow step. For example, in the last step, you can see that the `accessible_attributes` increased because of the [flow data accumulation](#is-it-possible-a-flow-accumulates-its-input-and-merges-each-success-result-to-use-as-the-argument-of-the-next-use-cases).
|
793
|
+
|
794
|
+
> **Note:** The [`Micro::Case::Result#then`](#how-to-use-the-microcaseresultthen-method) increments the `Micro::Case::Result#transitions`.
|
795
|
+
|
796
|
+
PS: Use the `Micro::Case::Result.disable_transition_tracking` global feature toggle to disable this feature (use once) and increase the use cases' performance.
|
797
|
+
|
798
|
+
##### `Micro::Case::Result#transitions` schema
|
799
|
+
```ruby
|
800
|
+
[
|
801
|
+
{
|
802
|
+
use_case: {
|
803
|
+
class: <Micro::Case>,# Use case which was executed
|
804
|
+
attributes: <Hash> # (Input) The use case's attributes
|
805
|
+
},
|
806
|
+
[success:, failure:] => { # (Output)
|
807
|
+
type: <Symbol>, # Result type. Defaults:
|
808
|
+
# Success = :ok, Failure = :error/:exception
|
809
|
+
value: <Hash> # The data returned by the use case
|
810
|
+
},
|
811
|
+
accessible_attributes: <Array>, # Properties that can be accessed by the use case's attributes,
|
812
|
+
# starting with Hash used to invoke it and which are incremented
|
813
|
+
# with each result value of the flow's use cases.
|
814
|
+
}
|
815
|
+
]
|
816
|
+
```
|
817
|
+
|
607
818
|
#### Is it possible to declare a flow which includes the use case itself?
|
608
819
|
|
609
820
|
Answer: Yes, it is! You can use the `self.call!` macro. e.g:
|
@@ -717,7 +928,9 @@ end
|
|
717
928
|
# Examples: https://github.com/serradura/u-case/blob/5a85fc238b63811a32737493dc6c59965f92491d/test/micro/case/safe_test.rb#L95-L123
|
718
929
|
```
|
719
930
|
|
720
|
-
|
931
|
+
[⬆️ Back to Top](#table-of-contents-)
|
932
|
+
|
933
|
+
#### `Micro::Case::Safe::Flow`
|
721
934
|
|
722
935
|
As the safe use cases, safe flows can intercept an exception in any of its steps. These are the ways to define one:
|
723
936
|
|
@@ -751,7 +964,6 @@ module Users
|
|
751
964
|
end
|
752
965
|
end
|
753
966
|
|
754
|
-
|
755
967
|
# !------------------------------------------ ! #
|
756
968
|
# ! Deprecated: Micro::Case::Safe::Flow mixin ! #
|
757
969
|
# !-------------------------------------------! #
|
@@ -772,11 +984,59 @@ end
|
|
772
984
|
|
773
985
|
[⬆️ Back to Top](#table-of-contents-)
|
774
986
|
|
775
|
-
|
987
|
+
#### `Micro::Case::Result#on_exception`
|
988
|
+
|
989
|
+
In functional programming errors/exceptions are handled as regular data, the idea is to transform the output even when it happens an unexpected behavior. For many, [exceptions are very similar to the GOTO statement](https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why), jumping the application flow to paths which could be difficult to figure out how things work in a system.
|
990
|
+
|
991
|
+
To address this the `Micro::Case::Result` has a special hook `#on_exception` to helping you to handle the control flow in the case of exceptions.
|
992
|
+
|
993
|
+
> **Note**: this feature will work better if you use it with a `Micro::Case::Safe` use case/flow.
|
994
|
+
|
995
|
+
How does it work?
|
996
|
+
|
997
|
+
```ruby
|
998
|
+
class Divide < Micro::Case::Safe
|
999
|
+
attributes :a, :b
|
1000
|
+
|
1001
|
+
def call!
|
1002
|
+
Success(division: a / b)
|
1003
|
+
end
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
Divide
|
1007
|
+
.call(a: 2, b: 0)
|
1008
|
+
.on_success { |result| puts result[:division] }
|
1009
|
+
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
|
1010
|
+
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
|
1011
|
+
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
|
1012
|
+
|
1013
|
+
# Output:
|
1014
|
+
# -------
|
1015
|
+
# Can't divide a number by 0
|
1016
|
+
# Oh no, something went wrong!
|
1017
|
+
|
1018
|
+
Divide.
|
1019
|
+
.call(a: 2, b: '2').
|
1020
|
+
.on_success { |result| puts result[:division] }
|
1021
|
+
.on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
|
1022
|
+
.on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
|
1023
|
+
.on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }
|
1024
|
+
|
1025
|
+
# Output:
|
1026
|
+
# -------
|
1027
|
+
# Please, use only numeric attributes.
|
1028
|
+
# Oh no, something went wrong!
|
1029
|
+
```
|
1030
|
+
|
1031
|
+
As you can see, this hook has the same behavior of `result.on_failure(:exception)`, but, the ideia here is to have a better communication in the code, making an explicit reference when some failure happened because of an exception.
|
1032
|
+
|
1033
|
+
[⬆️ Back to Top](#table-of-contents-)
|
1034
|
+
|
1035
|
+
### `u-case/with_activemodel_validation` - How to validate use case attributes?
|
776
1036
|
|
777
1037
|
**Requirement:**
|
778
1038
|
|
779
|
-
To do this your application must have the [activemodel >= 3.2](https://rubygems.org/gems/activemodel) as a dependency.
|
1039
|
+
To do this your application must have the [activemodel >= 3.2, < 6.1.0](https://rubygems.org/gems/activemodel) as a dependency.
|
780
1040
|
|
781
1041
|
```ruby
|
782
1042
|
#
|
@@ -800,10 +1060,10 @@ end
|
|
800
1060
|
# your use cases on validation errors, you can use:
|
801
1061
|
|
802
1062
|
# In some file. e.g: A Rails initializer
|
803
|
-
require 'u-case/
|
1063
|
+
require 'u-case/with_activemodel_validation' # or require 'micro/case/with_validation'
|
804
1064
|
|
805
1065
|
# In the Gemfile
|
806
|
-
gem 'u-case', require: 'u-case/
|
1066
|
+
gem 'u-case', require: 'u-case/with_activemodel_validation'
|
807
1067
|
|
808
1068
|
# Using this approach, you can rewrite the previous example with less code. e.g:
|
809
1069
|
|
@@ -828,7 +1088,7 @@ end
|
|
828
1088
|
Answer: Yes, it is. To do this, you only need to use the `disable_auto_validation` macro. e.g:
|
829
1089
|
|
830
1090
|
```ruby
|
831
|
-
require 'u-case/
|
1091
|
+
require 'u-case/with_activemodel_validation'
|
832
1092
|
|
833
1093
|
class Multiply < Micro::Case
|
834
1094
|
disable_auto_validation
|
@@ -850,6 +1110,31 @@ Multiply.call(a: 2, b: 'a')
|
|
850
1110
|
|
851
1111
|
[⬆️ Back to Top](#table-of-contents-)
|
852
1112
|
|
1113
|
+
#### `Kind::Validator`
|
1114
|
+
|
1115
|
+
The [kind gem](https://github.com/serradura/kind) has a module to enable the validation of data type through [`ActiveModel validations`](https://guides.rubyonrails.org/active_model_basics.html#validations). So, when you require the `'u-case/with_activemodel_validation'`, this module will require the [`Kind::Validator`](https://github.com/serradura/kind#kindvalidator-activemodelvalidations).
|
1116
|
+
|
1117
|
+
The example below shows how to validate the attributes data types.
|
1118
|
+
|
1119
|
+
```ruby
|
1120
|
+
class Todo::List::AddItem < Micro::Case
|
1121
|
+
attributes :user, :params
|
1122
|
+
|
1123
|
+
validates :user, kind: User
|
1124
|
+
validates :params, kind: ActionController::Parameters
|
1125
|
+
|
1126
|
+
def call!
|
1127
|
+
todo_params = Todo::Params.to_save(params)
|
1128
|
+
|
1129
|
+
todo = user.todos.create(todo_params)
|
1130
|
+
|
1131
|
+
Success { { todo: todo} }
|
1132
|
+
rescue ActionController::ParameterMissing => e
|
1133
|
+
Failure(:parameter_missing) { { message: e.message } }
|
1134
|
+
end
|
1135
|
+
end
|
1136
|
+
```
|
1137
|
+
|
853
1138
|
## Benchmarks
|
854
1139
|
|
855
1140
|
### `Micro::Case`
|
@@ -858,25 +1143,25 @@ Multiply.call(a: 2, b: 'a')
|
|
858
1143
|
|
859
1144
|
The table below contains the average between the [Success results](#success-results) and [Failure results](#failure-results) benchmarks.
|
860
1145
|
|
861
|
-
| Gem / Abstraction | Iterations per second | Comparison
|
862
|
-
| ---------------------- | --------------------: |
|
863
|
-
| **Micro::Case** | 116629.7 | _**The
|
864
|
-
| Dry::Monads | 101796.3 | 1.14x slower
|
865
|
-
| Interactor | 21230.5 | 5.49x slower
|
866
|
-
| Trailblazer::Operation | 16466.6 | 7.08x slower
|
867
|
-
| Dry::Transaction | 5069.5 | 23.00x slower
|
1146
|
+
| Gem / Abstraction | Iterations per second | Comparison |
|
1147
|
+
| ---------------------- | --------------------: | ----------------: |
|
1148
|
+
| **Micro::Case** | 116629.7 | _**The Fastest**_ |
|
1149
|
+
| Dry::Monads | 101796.3 | 1.14x slower |
|
1150
|
+
| Interactor | 21230.5 | 5.49x slower |
|
1151
|
+
| Trailblazer::Operation | 16466.6 | 7.08x slower |
|
1152
|
+
| Dry::Transaction | 5069.5 | 23.00x slower |
|
868
1153
|
|
869
1154
|
---
|
870
1155
|
|
871
1156
|
#### Success results
|
872
1157
|
|
873
|
-
| Gem / Abstraction | Iterations per second | Comparison
|
874
|
-
| ----------------- | --------------------: |
|
875
|
-
| Dry::Monads | 139352.5 | _**The
|
876
|
-
| **Micro::Case** | 124749.4 | 1.12x slower
|
877
|
-
| Interactor | 28974.4 | 4.81x slower
|
878
|
-
| Trailblazer::Operation | 17275.6 | 8.07x slower
|
879
|
-
| Dry::Transaction | 5571.7 | 25.01x slower
|
1158
|
+
| Gem / Abstraction | Iterations per second | Comparison |
|
1159
|
+
| ----------------- | --------------------: | ----------------: |
|
1160
|
+
| Dry::Monads | 139352.5 | _**The Fastest**_ |
|
1161
|
+
| **Micro::Case** | 124749.4 | 1.12x slower |
|
1162
|
+
| Interactor | 28974.4 | 4.81x slower |
|
1163
|
+
| Trailblazer::Operation | 17275.6 | 8.07x slower |
|
1164
|
+
| Dry::Transaction | 5571.7 | 25.01x slower |
|
880
1165
|
|
881
1166
|
<details>
|
882
1167
|
<summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
|
@@ -916,13 +1201,13 @@ https://github.com/serradura/u-case/blob/master/benchmarks/use_case/with_success
|
|
916
1201
|
|
917
1202
|
#### Failure results
|
918
1203
|
|
919
|
-
| Gem / Abstraction | Iterations per second | Comparison
|
920
|
-
| ----------------- | --------------------: |
|
921
|
-
| **Micro::Case** | 108510.0 | _**The
|
922
|
-
| Dry::Monads | 64240.1 | 1.69x slower
|
923
|
-
| Trailblazer::Operation | 15657.7 | 6.93x slower
|
924
|
-
| Interactor | 13486.7 | 8.05x slower
|
925
|
-
| Dry::Transaction | 4567.3 | 23.76x slower
|
1204
|
+
| Gem / Abstraction | Iterations per second | Comparison |
|
1205
|
+
| ----------------- | --------------------: | ----------------: |
|
1206
|
+
| **Micro::Case** | 108510.0 | _**The Fastest**_ |
|
1207
|
+
| Dry::Monads | 64240.1 | 1.69x slower |
|
1208
|
+
| Trailblazer::Operation | 15657.7 | 6.93x slower |
|
1209
|
+
| Interactor | 13486.7 | 8.05x slower |
|
1210
|
+
| Dry::Transaction | 4567.3 | 23.76x slower |
|
926
1211
|
|
927
1212
|
<details>
|
928
1213
|
<summary>Show the full <a href="https://github.com/evanphx/benchmark-ips">benchmark/ips</a> results.</summary>
|
@@ -965,10 +1250,10 @@ https://github.com/serradura/u-case/blob/master/benchmarks/use_case/with_failure
|
|
965
1250
|
### `Micro::Case::Flow`
|
966
1251
|
|
967
1252
|
| Gems / Abstraction | [Success results](https://github.com/serradura/u-case/blob/master/benchmarks/flow/with_success_result.rb#L40) | [Failure results](https://github.com/serradura/u-case/blob/master/benchmarks/flow/with_failure_result.rb#L40) |
|
968
|
-
| ------------------ |
|
969
|
-
| Micro::Case::Flow | _**The
|
970
|
-
| Micro::Case::Safe::Flow | 0x slower
|
971
|
-
| Interactor::Organizer | 1.47x slower
|
1253
|
+
| ------------------ | ----------------: | ----------------: |
|
1254
|
+
| Micro::Case::Flow | _**The Fastest**_ | _**The Fastest**_ |
|
1255
|
+
| Micro::Case::Safe::Flow | 0x slower | 0x slower |
|
1256
|
+
| Interactor::Organizer | 1.47x slower | 5.51x slower |
|
972
1257
|
|
973
1258
|
\* The `Dry::Monads`, `Dry::Transaction`, `Trailblazer::Operation` are out of this analysis because all of them doesn't have this kind of feature.
|
974
1259
|
|
data/lib/micro/case.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'kind'
|
3
4
|
require 'micro/attributes'
|
4
|
-
# frozen_string_literal: true
|
5
5
|
|
6
6
|
module Micro
|
7
7
|
class Case
|
8
8
|
require 'micro/case/version'
|
9
|
+
require 'micro/case/utils'
|
9
10
|
require 'micro/case/result'
|
10
11
|
require 'micro/case/error'
|
11
12
|
require 'micro/case/safe'
|
@@ -42,37 +43,60 @@ module Micro
|
|
42
43
|
instance
|
43
44
|
end
|
44
45
|
|
46
|
+
def self.__call_and_set_transition__(result, arg)
|
47
|
+
input =
|
48
|
+
arg.is_a?(Hash) ? result.__set_transitions_accessible_attributes__(arg) : arg
|
49
|
+
|
50
|
+
__new__(result, input).call
|
51
|
+
end
|
52
|
+
|
53
|
+
FLOW_STEP = 'Flow_Step'.freeze
|
54
|
+
|
55
|
+
private_constant :FLOW_STEP
|
56
|
+
|
45
57
|
def self.__call!
|
46
|
-
return const_get(
|
58
|
+
return const_get(FLOW_STEP) if const_defined?(FLOW_STEP, false)
|
47
59
|
|
48
|
-
|
49
|
-
private def __call
|
50
|
-
__call_use_case
|
51
|
-
end
|
52
|
-
end)
|
60
|
+
class_eval("class #{FLOW_STEP} < #{self.name}; private def __call; __call_use_case; end; end")
|
53
61
|
end
|
54
62
|
|
55
63
|
def self.call!
|
56
64
|
self
|
57
65
|
end
|
58
66
|
|
67
|
+
def self.inherited(subclass)
|
68
|
+
subclass.attributes(self.attributes_data({}))
|
69
|
+
subclass.extend ::Micro::Attributes.const_get('Macros::ForSubclasses'.freeze)
|
70
|
+
|
71
|
+
if self.send(:__flow_use_cases) && !subclass.name.to_s.end_with?(FLOW_STEP)
|
72
|
+
raise "Wooo, you can't do this! Inherits from a use case which has an inner flow violates "\
|
73
|
+
"one of the project principles: Solve complex business logic, by allowing the composition of use cases. "\
|
74
|
+
"Instead of doing this, declare a new class/constant with the steps needed.\n\n"\
|
75
|
+
"Related issue: https://github.com/serradura/u-case/issues/19\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
59
79
|
def self.__flow_reducer
|
60
80
|
Flow::Reducer
|
61
81
|
end
|
62
82
|
|
63
83
|
def self.__flow_get
|
64
|
-
@__flow
|
84
|
+
return @__flow if defined?(@__flow)
|
65
85
|
end
|
66
86
|
|
67
|
-
private_class_method def self.
|
68
|
-
@__flow_use_cases
|
87
|
+
private_class_method def self.__flow_use_cases
|
88
|
+
return @__flow_use_cases if defined?(@__flow_use_cases)
|
69
89
|
end
|
70
90
|
|
71
91
|
private_class_method def self.__flow_use_cases_get
|
72
|
-
Array(
|
92
|
+
Array(__flow_use_cases)
|
73
93
|
.map { |use_case| use_case == self ? self.__call! : use_case }
|
74
94
|
end
|
75
95
|
|
96
|
+
private_class_method def self.__flow_use_cases_set(args)
|
97
|
+
@__flow_use_cases = args
|
98
|
+
end
|
99
|
+
|
76
100
|
private_class_method def self.__flow_set(args)
|
77
101
|
return if __flow_get
|
78
102
|
|
@@ -84,7 +108,7 @@ module Micro
|
|
84
108
|
end
|
85
109
|
|
86
110
|
def self.__flow_set!
|
87
|
-
__flow_set(__flow_use_cases_get) if !__flow_get &&
|
111
|
+
__flow_set(__flow_use_cases_get) if !__flow_get && __flow_use_cases
|
88
112
|
end
|
89
113
|
|
90
114
|
def self.flow(*args)
|
@@ -105,7 +129,7 @@ module Micro
|
|
105
129
|
|
106
130
|
def __set_result__(result)
|
107
131
|
raise Error::InvalidResultInstance unless result.is_a?(Result)
|
108
|
-
raise Error::ResultIsAlreadyDefined if @__result
|
132
|
+
raise Error::ResultIsAlreadyDefined if defined?(@__result)
|
109
133
|
|
110
134
|
@__result = result
|
111
135
|
end
|
@@ -145,18 +169,14 @@ module Micro
|
|
145
169
|
def Success(arg = :ok)
|
146
170
|
value, type = block_given? ? [yield, arg] : [arg, :ok]
|
147
171
|
|
148
|
-
|
172
|
+
__get_result_with(true, value, type)
|
149
173
|
end
|
150
174
|
|
151
175
|
def Failure(arg = :error)
|
152
176
|
value = block_given? ? yield : arg
|
153
177
|
type = __map_failure_type(value, block_given? ? arg : :error)
|
154
178
|
|
155
|
-
|
156
|
-
end
|
157
|
-
|
158
|
-
def __get_result__
|
159
|
-
@__result ||= Result.new
|
179
|
+
__get_result_with(false, value, type)
|
160
180
|
end
|
161
181
|
|
162
182
|
def __map_failure_type(arg, type)
|
@@ -166,5 +186,13 @@ module Micro
|
|
166
186
|
|
167
187
|
type
|
168
188
|
end
|
189
|
+
|
190
|
+
def __get_result__
|
191
|
+
@__result ||= Result.new
|
192
|
+
end
|
193
|
+
|
194
|
+
def __get_result_with(is_success, value, type)
|
195
|
+
__get_result__.__set__(is_success, value, type, self)
|
196
|
+
end
|
169
197
|
end
|
170
198
|
end
|
data/lib/micro/case/error.rb
CHANGED
@@ -9,20 +9,36 @@ module Micro
|
|
9
9
|
def initialize(klass); super(klass.name + MESSAGE); end
|
10
10
|
end
|
11
11
|
|
12
|
-
ResultIsAlreadyDefined
|
12
|
+
class ResultIsAlreadyDefined < ArgumentError
|
13
|
+
def initialize; super('result is already defined'.freeze); end
|
14
|
+
end
|
13
15
|
|
14
|
-
InvalidResultType
|
15
|
-
|
16
|
+
class InvalidResultType < TypeError
|
17
|
+
def initialize; super('type must be a Symbol'.freeze); end
|
18
|
+
end
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
class InvalidResultInstance < ArgumentError
|
21
|
+
def initialize; super('argument must be an instance of Micro::Case::Result'.freeze); end
|
22
|
+
end
|
19
23
|
|
20
|
-
|
24
|
+
class InvalidUseCase < TypeError
|
25
|
+
def initialize; super('use case must be a kind or an instance of Micro::Case'.freeze); end
|
26
|
+
end
|
21
27
|
|
22
|
-
class
|
23
|
-
|
28
|
+
class InvalidUseCases < ArgumentError
|
29
|
+
def initialize; super('argument must be a collection of `Micro::Case` classes'.freeze); end
|
30
|
+
end
|
24
31
|
|
25
|
-
|
32
|
+
class InvalidInvocationOfTheThenMethod < StandardError
|
33
|
+
def initialize; super('Invalid invocation of the Micro::Case::Result#then method'); end
|
34
|
+
end
|
35
|
+
|
36
|
+
class UndefinedFlow < ArgumentError
|
37
|
+
def initialize; super("This class hasn't declared its flow. Please, use the `flow()` macro to define one.".freeze); end
|
38
|
+
end
|
39
|
+
|
40
|
+
class InvalidAccessToTheUseCaseObject < StandardError
|
41
|
+
def initialize; super('only a failure result can access its use case object'.freeze); end
|
26
42
|
end
|
27
43
|
|
28
44
|
module ByWrongUsage
|
data/lib/micro/case/flow.rb
CHANGED
@@ -23,19 +23,18 @@ module Micro
|
|
23
23
|
|
24
24
|
def initialize(use_cases)
|
25
25
|
@use_cases = use_cases
|
26
|
+
@first_use_case = use_cases[0]
|
27
|
+
@next_use_cases = use_cases[1..-1]
|
26
28
|
end
|
27
29
|
|
28
30
|
def call(arg = {})
|
29
31
|
memo = arg.is_a?(Hash) ? arg.dup : {}
|
30
32
|
|
31
|
-
|
32
|
-
break result if result.failure?
|
33
|
+
first_result = first_use_case_result(arg)
|
33
34
|
|
34
|
-
|
35
|
-
input = value.is_a?(Hash) ? memo.tap { |data| data.merge!(value) } : value
|
35
|
+
return first_result if @next_use_cases.empty?
|
36
36
|
|
37
|
-
|
38
|
-
end
|
37
|
+
next_use_cases_result(first_result, memo)
|
39
38
|
end
|
40
39
|
|
41
40
|
def >>(arg)
|
@@ -52,16 +51,8 @@ module Micro
|
|
52
51
|
|
53
52
|
private
|
54
53
|
|
55
|
-
def
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
def initial_result(arg)
|
60
|
-
return arg.call if arg_to_call?(arg)
|
61
|
-
return arg if arg.is_a?(Micro::Case::Result)
|
62
|
-
|
63
|
-
result = ::Micro::Case::Result.new
|
64
|
-
result.__set__(true, arg, :ok, nil)
|
54
|
+
def is_a_result?(arg)
|
55
|
+
arg.is_a?(Micro::Case::Result)
|
65
56
|
end
|
66
57
|
|
67
58
|
def arg_to_call?(arg)
|
@@ -69,6 +60,44 @@ module Micro
|
|
69
60
|
return true if arg.is_a?(Class) && (arg < ::Micro::Case || arg < ::Micro::Case::Flow)
|
70
61
|
return false
|
71
62
|
end
|
63
|
+
|
64
|
+
def call_arg(arg)
|
65
|
+
output = arg.call
|
66
|
+
|
67
|
+
is_a_result?(output) ? output.value : output
|
68
|
+
end
|
69
|
+
|
70
|
+
def first_use_case_input(arg)
|
71
|
+
return call_arg(arg) if arg_to_call?(arg)
|
72
|
+
return arg.value if is_a_result?(arg)
|
73
|
+
|
74
|
+
arg
|
75
|
+
end
|
76
|
+
|
77
|
+
def first_use_case_result(arg)
|
78
|
+
input = first_use_case_input(arg)
|
79
|
+
|
80
|
+
result = ::Micro::Case::Result.new
|
81
|
+
|
82
|
+
@first_use_case.__call_and_set_transition__(result, input)
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_use_case_result(use_case, result, input)
|
86
|
+
use_case.__new__(result, input).call
|
87
|
+
end
|
88
|
+
|
89
|
+
def next_use_cases_result(first_result, memo)
|
90
|
+
@next_use_cases.reduce(first_result) do |result, use_case|
|
91
|
+
break result if result.failure?
|
92
|
+
|
93
|
+
value = result.value
|
94
|
+
input = value.is_a?(Hash) ? memo.tap { |data| data.merge!(value) } : value
|
95
|
+
|
96
|
+
result.__set_transitions_accessible_attributes__(memo)
|
97
|
+
|
98
|
+
next_use_case_result(use_case, result, input)
|
99
|
+
end
|
100
|
+
end
|
72
101
|
end
|
73
102
|
end
|
74
103
|
end
|
data/lib/micro/case/result.rb
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
|
3
5
|
module Micro
|
4
6
|
class Case
|
5
7
|
class Result
|
8
|
+
Kind::Types.add(self)
|
9
|
+
|
10
|
+
@@transition_tracking_disabled = false
|
11
|
+
|
12
|
+
def self.disable_transition_tracking
|
13
|
+
@@transition_tracking_disabled = true
|
14
|
+
end
|
15
|
+
|
6
16
|
class Data
|
7
17
|
attr_reader :value, :type
|
8
18
|
|
@@ -17,12 +27,19 @@ module Micro
|
|
17
27
|
|
18
28
|
attr_reader :value, :type
|
19
29
|
|
30
|
+
def initialize
|
31
|
+
@__transitions__ = []
|
32
|
+
@__transitions_accessible_attributes__ = {}
|
33
|
+
end
|
34
|
+
|
20
35
|
def __set__(is_success, value, type, use_case)
|
21
36
|
raise Error::InvalidResultType unless type.is_a?(Symbol)
|
22
|
-
raise Error::InvalidUseCase if !
|
37
|
+
raise Error::InvalidUseCase if !is_a_use_case?(use_case)
|
23
38
|
|
24
39
|
@success, @value, @type, @use_case = is_success, value, type, use_case
|
25
40
|
|
41
|
+
__set_transition__ unless @@transition_tracking_disabled
|
42
|
+
|
26
43
|
self
|
27
44
|
end
|
28
45
|
|
@@ -41,7 +58,9 @@ module Micro
|
|
41
58
|
end
|
42
59
|
|
43
60
|
def on_success(expected_type = nil)
|
44
|
-
|
61
|
+
yield(value) if success_type?(expected_type)
|
62
|
+
|
63
|
+
self
|
45
64
|
end
|
46
65
|
|
47
66
|
def on_failure(expected_type = nil)
|
@@ -49,11 +68,65 @@ module Micro
|
|
49
68
|
|
50
69
|
data = expected_type.nil? ? Data.new(value, type).tap(&:freeze) : value
|
51
70
|
|
52
|
-
|
71
|
+
yield(data, @use_case)
|
72
|
+
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def on_exception(expected_exception = nil)
|
77
|
+
return self unless failure_type?(:exception)
|
78
|
+
|
79
|
+
if !expected_exception || (Kind.is(Exception, expected_exception) && value.is_a?(expected_exception))
|
80
|
+
yield(value, @use_case)
|
81
|
+
end
|
82
|
+
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def then(arg = nil, attributes = nil, &block)
|
87
|
+
can_yield_self = respond_to?(:yield_self)
|
88
|
+
|
89
|
+
if block
|
90
|
+
raise Error::InvalidInvocationOfTheThenMethod if arg
|
91
|
+
raise NotImplementedError if !can_yield_self
|
92
|
+
|
93
|
+
yield_self(&block)
|
94
|
+
else
|
95
|
+
return yield_self if !arg && can_yield_self
|
96
|
+
|
97
|
+
raise Error::InvalidInvocationOfTheThenMethod if !is_a_use_case?(arg)
|
98
|
+
|
99
|
+
return self if failure?
|
100
|
+
|
101
|
+
input = attributes.is_a?(Hash) ? self.value.merge(attributes) : self.value
|
102
|
+
|
103
|
+
arg.__call_and_set_transition__(self, input)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def transitions
|
108
|
+
@__transitions__.clone
|
109
|
+
end
|
110
|
+
|
111
|
+
def __set_transitions_accessible_attributes__(attributes_data)
|
112
|
+
return attributes_data if @@transition_tracking_disabled
|
113
|
+
|
114
|
+
__set_transitions_accessible_attributes__!(attributes_data)
|
53
115
|
end
|
54
116
|
|
55
117
|
private
|
56
118
|
|
119
|
+
def __set_transitions_accessible_attributes__!(attributes_data)
|
120
|
+
attributes = Utils.symbolize_hash_keys(attributes_data)
|
121
|
+
|
122
|
+
__update_transitions_accessible_attributes__(attributes)
|
123
|
+
end
|
124
|
+
|
125
|
+
def __update_transitions_accessible_attributes__(attributes)
|
126
|
+
@__transitions_accessible_attributes__.merge!(attributes)
|
127
|
+
@__transitions_accessible_attributes__
|
128
|
+
end
|
129
|
+
|
57
130
|
def success_type?(expected_type)
|
58
131
|
success? && (expected_type.nil? || expected_type == type)
|
59
132
|
end
|
@@ -65,6 +138,21 @@ module Micro
|
|
65
138
|
def is_a_use_case?(arg)
|
66
139
|
(arg.is_a?(Class) && arg < ::Micro::Case) || arg.is_a?(::Micro::Case)
|
67
140
|
end
|
141
|
+
|
142
|
+
def __set_transition__
|
143
|
+
use_case_class = @use_case.class
|
144
|
+
use_case_attributes = Utils.symbolize_hash_keys(@use_case.attributes)
|
145
|
+
|
146
|
+
__update_transitions_accessible_attributes__(use_case_attributes)
|
147
|
+
|
148
|
+
result = @success ? :success : :failure
|
149
|
+
|
150
|
+
@__transitions__ << {
|
151
|
+
use_case: { class: use_case_class, attributes: use_case_attributes },
|
152
|
+
result => { type: @type, value: @value },
|
153
|
+
accessible_attributes: @__transitions_accessible_attributes__.keys
|
154
|
+
}
|
155
|
+
end
|
68
156
|
end
|
69
157
|
end
|
70
158
|
end
|
data/lib/micro/case/safe/flow.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
class Case
|
5
|
+
module Utils
|
6
|
+
def self.symbolize_hash_keys(hash)
|
7
|
+
if Kind::Of::Hash(hash).respond_to?(:transform_keys)
|
8
|
+
hash.transform_keys { |key| key.to_sym rescue key }
|
9
|
+
else
|
10
|
+
hash.each_with_object({}) do |(k, v), memo|
|
11
|
+
key = k.to_sym rescue k
|
12
|
+
|
13
|
+
memo[key] = v
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/micro/case/version.rb
CHANGED
@@ -7,7 +7,7 @@ module Micro
|
|
7
7
|
include Micro::Attributes::Features::ActiveModelValidations
|
8
8
|
|
9
9
|
def self.auto_validation_disabled?
|
10
|
-
@disable_auto_validation
|
10
|
+
return @disable_auto_validation if defined?(@disable_auto_validation)
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.disable_auto_validation
|
@@ -22,12 +22,14 @@ module Micro
|
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
def
|
26
|
-
return __call_use_case_flow if __call_use_case_flow?
|
27
|
-
|
25
|
+
def __call_use_case
|
28
26
|
return failure_by_validation_error(self) if !self.class.auto_validation_disabled? && invalid?
|
29
27
|
|
30
|
-
|
28
|
+
result = call!
|
29
|
+
|
30
|
+
return result if result.is_a?(Result)
|
31
|
+
|
32
|
+
raise Error::UnexpectedResult.new(self.class)
|
31
33
|
end
|
32
34
|
|
33
35
|
def failure_by_validation_error(object)
|
@@ -1,3 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
warn 'Deprecation: "u-case/with_validation" will be deprecated in the next major release.' \
|
4
|
+
'Please use "u-case/with_activemodel_validation" instead of it.'
|
5
|
+
|
6
|
+
require 'u-case/with_activemodel_validation'
|
data/u-case.gemspec
CHANGED
@@ -14,20 +14,8 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.homepage = 'https://github.com/serradura/u-case'
|
15
15
|
spec.license = 'MIT'
|
16
16
|
|
17
|
-
|
18
|
-
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
-
if spec.respond_to?(:metadata)
|
20
|
-
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
21
|
-
|
22
|
-
# spec.metadata["homepage_uri"] = spec.homepage
|
23
|
-
# spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
24
|
-
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
25
|
-
else
|
26
|
-
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
|
27
|
-
end
|
17
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
|
28
18
|
|
29
|
-
# Specify which files should be added to the gem when it is released.
|
30
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
31
19
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
32
20
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets|benchmarks|comparisons|examples)/}) }
|
33
21
|
end
|
@@ -37,8 +25,9 @@ Gem::Specification.new do |spec|
|
|
37
25
|
|
38
26
|
spec.required_ruby_version = '>= 2.2.0'
|
39
27
|
|
28
|
+
spec.add_runtime_dependency 'kind', '~> 3.0'
|
40
29
|
spec.add_runtime_dependency 'u-attributes', '~> 1.1'
|
41
30
|
|
42
31
|
spec.add_development_dependency 'bundler'
|
43
|
-
spec.add_development_dependency 'rake', '~>
|
32
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
44
33
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: u-case
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodrigo Serradura
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kind
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: u-attributes
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,14 +58,14 @@ dependencies:
|
|
44
58
|
requirements:
|
45
59
|
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
61
|
+
version: '13.0'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
68
|
+
version: '13.0'
|
55
69
|
description: Create simple and powerful use cases as objects.
|
56
70
|
email:
|
57
71
|
- rodrigo.serradura@gmail.com
|
@@ -78,9 +92,11 @@ files:
|
|
78
92
|
- lib/micro/case/safe.rb
|
79
93
|
- lib/micro/case/safe/flow.rb
|
80
94
|
- lib/micro/case/strict.rb
|
95
|
+
- lib/micro/case/utils.rb
|
81
96
|
- lib/micro/case/version.rb
|
82
|
-
- lib/micro/case/
|
97
|
+
- lib/micro/case/with_activemodel_validation.rb
|
83
98
|
- lib/u-case.rb
|
99
|
+
- lib/u-case/with_activemodel_validation.rb
|
84
100
|
- lib/u-case/with_validation.rb
|
85
101
|
- test.sh
|
86
102
|
- u-case.gemspec
|
@@ -103,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
119
|
- !ruby/object:Gem::Version
|
104
120
|
version: '0'
|
105
121
|
requirements: []
|
106
|
-
rubygems_version: 3.0.
|
122
|
+
rubygems_version: 3.0.6
|
107
123
|
signing_key:
|
108
124
|
specification_version: 4
|
109
125
|
summary: Create simple and powerful use cases as objects.
|