f_service 0.1.1 → 0.3.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/.github/.dependabot.yml +8 -0
- data/.github/workflows/tests-and-linter.yml +2 -2
- data/.rubocop.yml +19 -1
- data/CHANGELOG.md +43 -7
- data/Gemfile +2 -0
- data/Gemfile.lock +22 -9
- data/README.md +201 -17
- data/f_service.gemspec +3 -3
- data/lib/f_service/base.rb +190 -23
- data/lib/f_service/result/base.rb +126 -15
- data/lib/f_service/result/failure.rb +43 -7
- data/lib/f_service/result/success.rb +46 -9
- data/lib/f_service/rspec/support/helpers/result.rb +32 -0
- data/lib/f_service/rspec/support/helpers.rb +3 -0
- data/lib/f_service/rspec/support/matchers/result.rb +61 -0
- data/lib/f_service/rspec/support/matchers.rb +3 -0
- data/lib/f_service/rspec/support.rb +4 -0
- data/lib/f_service/rspec.rb +3 -0
- data/lib/f_service/version.rb +1 -1
- data/lib/f_service.rb +23 -0
- data/logo.png +0 -0
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cb29d87cf5c970b2b185ed2c3da41020f40122e2d0857af068302fb32e55400
|
4
|
+
data.tar.gz: 27a89660bb6d1e65d537f5d6617cc8175f600ab71efca79f65875a1b6adf0c97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b444953aee676163a2b076d814b3cc801b0808a30660c4edd2f19172754d58c41a7fcb3129a52cf95c1a0fdb8546419a55a8101b6882459673cd647d520f660
|
7
|
+
data.tar.gz: 0e2f10e1ad6cf4ba7ba2375b2302661429721b7bf4023afcb1559909205eb7e8f013efbb3da07c4b7926b2e967592c31a6c930ca6d6e69b195bba97342407b69
|
@@ -11,12 +11,12 @@ jobs:
|
|
11
11
|
runs-on: ubuntu-latest
|
12
12
|
strategy:
|
13
13
|
matrix:
|
14
|
-
ruby: [2.
|
14
|
+
ruby: [2.6, 2.7, 3.0, 3.1]
|
15
15
|
|
16
16
|
steps:
|
17
17
|
- uses: actions/checkout@v2
|
18
18
|
- name: Set up Ruby ${{ matrix.ruby }}
|
19
|
-
uses:
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
20
|
with:
|
21
21
|
ruby-version: ${{ matrix.ruby }}
|
22
22
|
- name: Build and test with Rake
|
data/.rubocop.yml
CHANGED
@@ -12,4 +12,22 @@ Metrics/BlockLength:
|
|
12
12
|
- "spec/**/*"
|
13
13
|
|
14
14
|
Style/DocumentationMethod:
|
15
|
-
Enabled: true
|
15
|
+
Enabled: true
|
16
|
+
|
17
|
+
Naming/MethodName:
|
18
|
+
Exclude:
|
19
|
+
- lib/f_service/base.rb
|
20
|
+
|
21
|
+
RSpec/ContextWording:
|
22
|
+
Prefixes:
|
23
|
+
- and
|
24
|
+
- but
|
25
|
+
- when
|
26
|
+
- with
|
27
|
+
- without
|
28
|
+
|
29
|
+
RSpec/ExampleLength:
|
30
|
+
Max: 20
|
31
|
+
|
32
|
+
RSpec/NestedGroups:
|
33
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -4,16 +4,52 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
-
## Unreleased
|
8
|
-
|
7
|
+
## Unreleased (master)
|
8
|
+
<!-- ### Added -->
|
9
|
+
<!-- ### Changed -->
|
10
|
+
<!-- ### Removed -->
|
11
|
+
---
|
12
|
+
|
13
|
+
## 0.3.0
|
14
|
+
### Added
|
15
|
+
- Added Rspec Helper `#mock_service` #41;
|
16
|
+
- Added Rspec Matcher `#have_succeed_with` and `#have_failed_with` #41;
|
17
|
+
- Added `Success()`, `Failure()`, `Check()`, `Try()` now can be multipe types #41;
|
18
|
+
- Changed Depreacate `Result#type` method #41;
|
19
|
+
- Changed Deprecate method `#then` for Success and Failure classes #40
|
20
|
+
- Added RSpec support for mock and match results #35
|
21
|
+
- Deprecate method `#then` for Success and Failure classes #40;
|
22
|
+
- Removed deprecated method `#on` #33
|
23
|
+
- Changed Capture just one callback per result #30;
|
24
|
+
|
25
|
+
## 0.2.0
|
26
|
+
### Added
|
27
|
+
- Add `and_then` as alias for `then` (partially fix [#23](https://github.com/Fretadao/f_service/issues/23)).
|
28
|
+
- Add `catch` to `Failure` and `Success`. It acts as an inverted `then` and has a `or_else` alias.
|
29
|
+
- Add support to custom `data` property to be passed when calling `Base#Check`.
|
30
|
+
- Add support to multiple type checks on `Result#on_success` and `Result#on_failure` hooks.
|
31
|
+
- Yields result type on blocks (`then`, `on_success` and `on_failure`).
|
32
|
+
- Add type check on `Result#on_success` and `Result#on_failure` hooks.
|
33
|
+
- Add method `Base#Try`. It wraps exceptions in Failures.
|
34
|
+
- Add method `Base#Check`. It converts booleans to Results.
|
35
|
+
- Add methods `#Success(type, data:)` and `#Failure(type, data:)` on `FService::Base`.
|
36
|
+
These methods allow defining the type and value of the Result object.
|
37
|
+
- Allow adding types on `Result`s.
|
38
|
+
- Add `#on_success` and `#on_failure` hooks on Result objects.
|
39
|
+
- Link to Changelog on gemspec.
|
40
|
+
|
41
|
+
### Changed
|
42
|
+
- **[Deprecation]** Mark `Base#result` as deprecated. They will be removed on the next release. Use the `Base#Check` instead.
|
43
|
+
- **[Deprecation]** Mark `Base#success` and `Base#failure` as deprecated. They will be removed on the next release. Use the `Base#Success` and `Base#Failure` instead.
|
44
|
+
- **[Deprecation]** Mark `Result#on` as deprecated. It will be removed on the next release. Use the`Result#on_success` and/or `Result#on_failure` hooks instead.
|
9
45
|
|
10
46
|
## 0.1.1
|
11
47
|
### Added
|
12
48
|
First usable version with:
|
13
|
-
- Result based services
|
14
|
-
- Type check on results
|
15
|
-
- Pattern matching with `#call`ables
|
16
|
-
- Safe chaining calls with `#then
|
49
|
+
- Result based services
|
50
|
+
- Type check on results
|
51
|
+
- Pattern matching with `#call`ables
|
52
|
+
- Safe chaining calls with `#then`
|
17
53
|
|
18
54
|
## 0.1.0
|
19
|
-
- **Yanked**
|
55
|
+
- **Yanked**
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
f_service (0.
|
4
|
+
f_service (0.3.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -9,22 +9,31 @@ GEM
|
|
9
9
|
ast (2.4.0)
|
10
10
|
backport (1.1.2)
|
11
11
|
benchmark (0.1.0)
|
12
|
+
coderay (1.1.3)
|
12
13
|
diff-lcs (1.3)
|
13
|
-
docile (1.3.
|
14
|
+
docile (1.3.5)
|
14
15
|
e2mmap (0.1.0)
|
15
16
|
jaro_winkler (1.5.4)
|
16
17
|
maruku (0.7.3)
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
method_source (1.0.0)
|
19
|
+
mini_portile2 (2.8.1)
|
20
|
+
nokogiri (1.14.3)
|
21
|
+
mini_portile2 (~> 2.8.0)
|
22
|
+
racc (~> 1.4)
|
20
23
|
parallel (1.19.1)
|
21
24
|
parser (2.7.1.1)
|
22
25
|
ast (~> 2.4.0)
|
26
|
+
pry (0.14.1)
|
27
|
+
coderay (~> 1.1)
|
28
|
+
method_source (~> 1.0)
|
29
|
+
pry-nav (1.0.0)
|
30
|
+
pry (>= 0.9.10, < 0.15)
|
31
|
+
racc (1.6.2)
|
23
32
|
rainbow (3.0.0)
|
24
33
|
rake (13.0.1)
|
25
34
|
reverse_markdown (1.4.0)
|
26
35
|
nokogiri
|
27
|
-
rexml (3.2.
|
36
|
+
rexml (3.2.5)
|
28
37
|
rspec (3.9.0)
|
29
38
|
rspec-core (~> 3.9.0)
|
30
39
|
rspec-expectations (~> 3.9.0)
|
@@ -49,10 +58,12 @@ GEM
|
|
49
58
|
rubocop-rspec (1.38.1)
|
50
59
|
rubocop (>= 0.68.1)
|
51
60
|
ruby-progressbar (1.10.1)
|
52
|
-
simplecov (0.
|
61
|
+
simplecov (0.21.2)
|
53
62
|
docile (~> 1.1)
|
54
63
|
simplecov-html (~> 0.11)
|
55
|
-
|
64
|
+
simplecov_json_formatter (~> 0.1)
|
65
|
+
simplecov-html (0.12.3)
|
66
|
+
simplecov_json_formatter (0.1.2)
|
56
67
|
solargraph (0.38.6)
|
57
68
|
backport (~> 1.1)
|
58
69
|
benchmark
|
@@ -78,6 +89,8 @@ PLATFORMS
|
|
78
89
|
DEPENDENCIES
|
79
90
|
bundler (~> 2.0)
|
80
91
|
f_service!
|
92
|
+
pry
|
93
|
+
pry-nav
|
81
94
|
rake (~> 13.0.0)
|
82
95
|
rspec (~> 3.0)
|
83
96
|
rubocop (~> 0.82.0)
|
@@ -87,4 +100,4 @@ DEPENDENCIES
|
|
87
100
|
yard
|
88
101
|
|
89
102
|
BUNDLED WITH
|
90
|
-
2.
|
103
|
+
2.2.32
|
data/README.md
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
<p align="center">
|
2
|
+
<img src="https://raw.githubusercontent.com/Fretadao/f_service/master/logo.png" height="150">
|
3
|
+
|
4
|
+
<h1 align="center">FService</h1>
|
5
|
+
|
6
|
+
<p align="center">
|
7
|
+
<i>Simpler, safer and more composable operations</i>
|
8
|
+
<br>
|
9
|
+
<br>
|
10
|
+
<img src="https://img.shields.io/gem/v/f_service">
|
11
|
+
<img src="https://github.com/Fretadao/f_service/workflows/Ruby/badge.svg">
|
12
|
+
<a href="https://github.com/Fretadao/f_service/blob/master/LICENSE">
|
13
|
+
<img src="https://img.shields.io/github/license/Fretadao/f_service.svg" alt="License">
|
14
|
+
</a>
|
15
|
+
</p>
|
16
|
+
</p>
|
4
17
|
|
5
18
|
FService is a small gem that provides a base class for your services (aka operations).
|
6
19
|
The goal is to make services simpler, safer, and more composable.
|
@@ -23,7 +36,9 @@ Or install it yourself as:
|
|
23
36
|
$ gem install f_service
|
24
37
|
|
25
38
|
## Usage
|
39
|
+
|
26
40
|
### Creating your service
|
41
|
+
|
27
42
|
To start using it, you have to create your service class inheriting from FService::Base.
|
28
43
|
|
29
44
|
```ruby
|
@@ -32,6 +47,7 @@ end
|
|
32
47
|
```
|
33
48
|
|
34
49
|
Now, define your initializer to setup data.
|
50
|
+
|
35
51
|
```ruby
|
36
52
|
class User::Create < FService::Base
|
37
53
|
def initialize(name:)
|
@@ -41,23 +57,25 @@ end
|
|
41
57
|
```
|
42
58
|
|
43
59
|
The next step is writing the `#run` method, which is where the work should be done.
|
44
|
-
Use the methods `#
|
60
|
+
Use the methods `#Success` and `#Failure` to handle your return values.
|
61
|
+
You can optionally specify a list of types which represents that result and a value for your result.
|
45
62
|
|
46
63
|
```ruby
|
47
64
|
class User::Create < FService::Base
|
48
65
|
# ...
|
49
66
|
def run
|
50
|
-
return
|
67
|
+
return Failure(:no_name, :invalid_attribute) if @name.nil?
|
51
68
|
|
52
69
|
user = UserRepository.create(name: @name)
|
53
|
-
if user.
|
54
|
-
|
70
|
+
if user.save
|
71
|
+
Success(:success, :created, data: user)
|
55
72
|
else
|
56
|
-
|
73
|
+
Failure(:creation_failed, data: user.errors)
|
57
74
|
end
|
58
75
|
end
|
59
76
|
end
|
60
77
|
```
|
78
|
+
|
61
79
|
> Remember, you **have** to return an `FService::Result` at the end of your services.
|
62
80
|
|
63
81
|
### Using your service
|
@@ -70,7 +88,9 @@ User::Create.(name: name)
|
|
70
88
|
User::Create.call(name: name)
|
71
89
|
```
|
72
90
|
|
73
|
-
> We do **not** recommend manually initializing your service because it **will not**
|
91
|
+
> We do **not** recommend manually initializing and running your service because it **will not**
|
92
|
+
> type check your result (and you could lose nice features like [pattern
|
93
|
+
> matching](#pattern-matching) and [service chaining](#chaining-services))!
|
74
94
|
|
75
95
|
### Using the result
|
76
96
|
|
@@ -91,24 +111,92 @@ class UsersController < BaseController
|
|
91
111
|
end
|
92
112
|
end
|
93
113
|
```
|
114
|
+
|
94
115
|
> Note that you're not limited to using services inside controllers. They're just PORO's (Play Old Ruby Objects), so you can use in controllers, models, etc. (even other services!).
|
95
116
|
|
96
117
|
### Pattern matching
|
97
|
-
|
118
|
+
|
119
|
+
The code above could be rewritten using the `#on_success` and `#on_failure` hooks. They work similar to pattern matching:
|
98
120
|
|
99
121
|
```ruby
|
100
122
|
class UsersController < BaseController
|
101
123
|
def create
|
102
|
-
User::Create.(user_params)
|
103
|
-
|
104
|
-
|
105
|
-
)
|
124
|
+
User::Create.(user_params)
|
125
|
+
.on_success { |value| return json_success(value) }
|
126
|
+
.on_failure { |error| return json_error(error) }
|
106
127
|
end
|
107
128
|
end
|
108
129
|
```
|
109
|
-
|
130
|
+
|
131
|
+
Or else it is possible to specify an unhandled option to ensure that the callback will process that message anyway the
|
132
|
+
error.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class UsersController < BaseController
|
136
|
+
def create
|
137
|
+
User::Create.(user_params)
|
138
|
+
.on_success(unhandled: true) { |value| return json_success(value) }
|
139
|
+
.on_failure(unhandled: true) { |error| return json_error(error) }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
class UsersController < BaseController
|
146
|
+
def create
|
147
|
+
User::Create.(user_params)
|
148
|
+
.on_success { |value| return json_success(value) }
|
149
|
+
.on_failure { |error| return json_error(error) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
> You can ignore any of the callbacks, if you want to.
|
155
|
+
|
156
|
+
Going further, you can match the Result type, in case you want to handle them differently:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class UsersController < BaseController
|
160
|
+
def create
|
161
|
+
User::Create.(user_params)
|
162
|
+
.on_success(:user_created) { |value| return json_success(value) }
|
163
|
+
.on_success(:user_already_exists) { |value| return json_success(value) }
|
164
|
+
.on_failure(:invalid_data) { |error| return json_error(error) }
|
165
|
+
.on_failure(:critical_error) do |error|
|
166
|
+
MyLogger.report_failure(error)
|
167
|
+
|
168
|
+
return json_error(error)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
It's possible to provide multiple types to the hooks too. If the result type matches any of the given types,
|
175
|
+
the hook will run.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
class UsersController < BaseController
|
179
|
+
def create
|
180
|
+
User::Create.(user_params)
|
181
|
+
.on_success(:user_created, :user_already_exists) { |value| return json_success(value) }
|
182
|
+
.on_failure(:invalid_data) { |error| return json_error(error) }
|
183
|
+
.on_failure(:critical_error) do |error|
|
184
|
+
MyLogger.report_failure(error)
|
185
|
+
|
186
|
+
return json_error(error)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
### Types precedence
|
193
|
+
|
194
|
+
FService matches types from left to right, from more specific to more generic.
|
195
|
+
Example (:unprocessable_entity, :client_error, :http_response)
|
196
|
+
Then, result will match first :unprocessable_entity, after :client_error, after :http_response, then not matched.
|
110
197
|
|
111
198
|
### Chaining services
|
199
|
+
|
112
200
|
Since all services return Results, you can chain service calls making a data pipeline.
|
113
201
|
If some step fails, it will short circuit the call chain.
|
114
202
|
|
@@ -116,8 +204,8 @@ If some step fails, it will short circuit the call chain.
|
|
116
204
|
class UsersController < BaseController
|
117
205
|
def create
|
118
206
|
result = User::Create.(user_params)
|
119
|
-
.
|
120
|
-
.
|
207
|
+
.and_then { |user| User::Login.(user) }
|
208
|
+
.and_then { |user| User::SendWelcomeEmail.(user) }
|
121
209
|
|
122
210
|
if result.successful?
|
123
211
|
json_success(result.value)
|
@@ -128,6 +216,102 @@ class UsersController < BaseController
|
|
128
216
|
end
|
129
217
|
```
|
130
218
|
|
219
|
+
You can use the `.to_proc` method on FService::Base to avoid explicit inputs when chaining services:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
class UsersController < BaseController
|
223
|
+
def create
|
224
|
+
result = User::Create.(user_params)
|
225
|
+
.and_then(&User::Login)
|
226
|
+
.and_then(&User::SendWelcomeEmail)
|
227
|
+
# ...
|
228
|
+
end
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
### `Check` and `Try`
|
233
|
+
|
234
|
+
You can use `Check` to converts a boolean to a Result, truthy values map to `Success`, and falsey values map to `Failures`:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
Check(:math_works) { 1 < 2 }
|
238
|
+
# => #<Success @value=true, @types=[:math_works]>
|
239
|
+
|
240
|
+
Check(:math_works) { 1 > 2 }
|
241
|
+
# => #<Failure @error=false, @types=[:math_works]>
|
242
|
+
```
|
243
|
+
|
244
|
+
`Try` transforms an exception into a `Failure` if some exception is raised for the given block. You can specify which exception class to watch for
|
245
|
+
using the parameter `catch`.
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
class IHateEvenNumbers < FService::Base
|
249
|
+
def run
|
250
|
+
Try(:rand_int) do
|
251
|
+
n = rand(1..10)
|
252
|
+
raise "Yuck! It's a #{n}" if n.even?
|
253
|
+
|
254
|
+
n
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
IHateEvenNumbers.call
|
260
|
+
# => #<Success @value=9, @types=[:rand_int]>
|
261
|
+
|
262
|
+
IHateEvenNumbers.call
|
263
|
+
# => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @types=[:rand_int]>
|
264
|
+
```
|
265
|
+
|
266
|
+
## Testing
|
267
|
+
|
268
|
+
We provide some helpers and matchers to make ease to test code envolving Fservice services.
|
269
|
+
|
270
|
+
To make available in the system, in the file 'spec/spec_helper.rb' or 'spec/rails_helper.rb'
|
271
|
+
|
272
|
+
add the folowing require:
|
273
|
+
|
274
|
+
```rb
|
275
|
+
require 'f_service/rspec'
|
276
|
+
```
|
277
|
+
|
278
|
+
### Mocking a result
|
279
|
+
|
280
|
+
```
|
281
|
+
mock_service(Uer::Create)
|
282
|
+
# => Mocks a successful result with all values nil
|
283
|
+
|
284
|
+
mock_service(Uer::Create, result: :success)
|
285
|
+
# => Mocks a successful result with all values nil
|
286
|
+
|
287
|
+
mock_service(Uer::Create, result: :success, types: [:created, :success])
|
288
|
+
# => Mocks a successful result with type created
|
289
|
+
|
290
|
+
mock_service(Uer::Create, result: :success, types: :created, value: instance_spy(User))
|
291
|
+
# => Mocks a successful result with type created and a value
|
292
|
+
|
293
|
+
mock_service(Uer::Create, result: :failure)
|
294
|
+
# => Mocs a failure with all nil values
|
295
|
+
|
296
|
+
mock_service(User::Create, result: :failure, types: [:unprocessable_entity, :client_error])
|
297
|
+
# => Mocs a failure with a failure type
|
298
|
+
|
299
|
+
mock_service(User::Create, result: :failure, types: [:unprocessable_entity, :client_error], value: { name: ["can't be blank"] })
|
300
|
+
# => Mocs a failure with a failure type and an error value
|
301
|
+
```
|
302
|
+
|
303
|
+
### Matching a result
|
304
|
+
|
305
|
+
```rb
|
306
|
+
expect(User::Create.(name: 'Joe')).to have_succeed_with(:created)
|
307
|
+
|
308
|
+
expect(User::Create.(name: 'Joe')).to have_succeed_with(:created).and_value(an_instance_of(User))
|
309
|
+
|
310
|
+
expect(User::Create.(name: nil)).to have_failed_with(:invalid_attributes)
|
311
|
+
|
312
|
+
expect(User::Create.(name: nil)).to have_failed_with(:invalid_attributes).and_error({ name: ["can't be blank"] })
|
313
|
+
```
|
314
|
+
|
131
315
|
## API Docs
|
132
316
|
|
133
317
|
You can access the API docs [here](https://www.rubydoc.info/gems/f_service/).
|
data/f_service.gemspec
CHANGED
@@ -7,8 +7,8 @@ require 'f_service/version'
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = 'f_service'
|
9
9
|
spec.version = FService::VERSION
|
10
|
-
spec.authors = ['
|
11
|
-
spec.email
|
10
|
+
spec.authors = ['Fretadao Tech Team']
|
11
|
+
spec.email = ['tech@fretadao.com.br']
|
12
12
|
|
13
13
|
spec.summary = 'A small, monad-based service class'
|
14
14
|
spec.description = <<-DESCRIPTION
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.metadata['homepage_uri'] = spec.homepage
|
24
24
|
spec.metadata['source_code_uri'] = 'https://github.com/Fretadao/f_service'
|
25
25
|
spec.metadata['documentation_uri'] = 'https://www.rubydoc.info/gems/f_service'
|
26
|
-
|
26
|
+
spec.metadata['changelog_uri'] = 'https://github.com/Fretadao/f_service/blob/master/CHANGELOG.md'
|
27
27
|
|
28
28
|
# Specify which files should be added to the gem when it is released.
|
29
29
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
data/lib/f_service/base.rb
CHANGED
@@ -10,20 +10,44 @@ module FService
|
|
10
10
|
#
|
11
11
|
# @abstract
|
12
12
|
class Base
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
13
|
+
class << self
|
14
|
+
# Initializes and runs a new service.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# User::UpdateName.(user: user, new_name: new_name)
|
18
|
+
# # or
|
19
|
+
# User::UpdateName.call(user: user, new_name: new_name)
|
20
|
+
#
|
21
|
+
# @note this method shouldn't be overridden in the subclasses
|
22
|
+
# @return [Result::Success, Result::Failure]
|
23
|
+
def call(*args)
|
24
|
+
result = new(*args).run
|
25
|
+
raise(FService::Error, 'Services must return a Result') unless result.is_a? Result::Base
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
25
29
|
|
26
|
-
|
30
|
+
ruby2_keywords :call if respond_to?(:ruby2_keywords, true)
|
31
|
+
|
32
|
+
# Allows running a service without explicit giving params.
|
33
|
+
# This is useful when chaining services or mapping inputs to be processed.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# # Assuming all classes here subclass FService::Base:
|
37
|
+
#
|
38
|
+
# User::Create
|
39
|
+
# .and_then(&User::Login)
|
40
|
+
# .and_then(&SendWelcomeEmail)
|
41
|
+
#
|
42
|
+
# # Mapping inputs:
|
43
|
+
#
|
44
|
+
# [{ n:1 }, { n: 2 }].map(&DoubleNumber).map(&:value)
|
45
|
+
# # => [2, 4]
|
46
|
+
#
|
47
|
+
# @return [Proc]
|
48
|
+
def to_proc
|
49
|
+
proc { |args| call(**args) }
|
50
|
+
end
|
27
51
|
end
|
28
52
|
|
29
53
|
# This method is where the main work of your service must be.
|
@@ -38,18 +62,18 @@ module FService
|
|
38
62
|
# end
|
39
63
|
#
|
40
64
|
# def run
|
41
|
-
# return
|
65
|
+
# return Failure(:missing_user) if user.nil?
|
42
66
|
#
|
43
67
|
# if @user.update(name: @new_name)
|
44
|
-
#
|
68
|
+
# Success(:created, data: user)
|
45
69
|
# else
|
46
|
-
#
|
70
|
+
# Failure(:creation_failed, data: user.errors)
|
47
71
|
# end
|
48
72
|
# end
|
49
73
|
# end
|
50
74
|
#
|
51
75
|
# @note this method SHOULD be overridden in the subclasses
|
52
|
-
# @return [
|
76
|
+
# @return [Result::Success, Result::Failure]
|
53
77
|
def run
|
54
78
|
raise NotImplementedError, 'Services must implement #run'
|
55
79
|
end
|
@@ -72,9 +96,138 @@ module FService
|
|
72
96
|
# end
|
73
97
|
# end
|
74
98
|
#
|
75
|
-
# @
|
99
|
+
# @deprecated Use {#Success} instead.
|
100
|
+
# @return [Result::Success] a successful operation
|
76
101
|
def success(data = nil)
|
77
|
-
FService
|
102
|
+
FService.deprecate!(
|
103
|
+
name: "#{self.class}##{__method__}",
|
104
|
+
alternative: '#Success',
|
105
|
+
from: caller[0]
|
106
|
+
)
|
107
|
+
|
108
|
+
Result::Success.new(data)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns a successful result.
|
112
|
+
# You can optionally specify a list of types and a value for your result.
|
113
|
+
# You'll probably want to return this inside {#run}.
|
114
|
+
#
|
115
|
+
#
|
116
|
+
# @example
|
117
|
+
# def run
|
118
|
+
# Success()
|
119
|
+
# # => #<Success @value=nil, @types=[]>
|
120
|
+
#
|
121
|
+
# Success(:ok)
|
122
|
+
# # => #<Success @value=nil, @types=[:ok]>
|
123
|
+
#
|
124
|
+
# Success(data: 10)
|
125
|
+
# # => #<Success @value=10, @types=[]>
|
126
|
+
#
|
127
|
+
# Success(:ok, data: 10)
|
128
|
+
# # => #<Success @value=10, @types=[:ok]>
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# @param types the Result types
|
132
|
+
# @param data the result value
|
133
|
+
# @return [Result::Success] a successful result
|
134
|
+
def Success(*types, data: nil)
|
135
|
+
Result::Success.new(data, types)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns a failed result.
|
139
|
+
# You can optionally specify types and a value for your result.
|
140
|
+
# You'll probably want to return this inside {#run}.
|
141
|
+
#
|
142
|
+
#
|
143
|
+
# @example
|
144
|
+
# def run
|
145
|
+
# Failure()
|
146
|
+
# # => #<Failure @error=nil, @types=[]>
|
147
|
+
#
|
148
|
+
# Failure(:not_a_number)
|
149
|
+
# # => #<Failure @error=nil, @types=[:not_a_number]>
|
150
|
+
#
|
151
|
+
# Failure(data: "10")
|
152
|
+
# # => #<Failure @error="10", @types=[]>
|
153
|
+
#
|
154
|
+
# Failure(:not_a_number, data: "10")
|
155
|
+
# # => #<Failure @error="10", @types=[:not_a_number]>
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# @param types the Result types
|
159
|
+
# @param data the result value
|
160
|
+
# @return [Result::Failure] a failed result
|
161
|
+
def Failure(*types, data: nil)
|
162
|
+
Result::Failure.new(data, types)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Converts a boolean to a Result.
|
166
|
+
# Truthy values map to Success, and falsey values map to Failures.
|
167
|
+
# You can optionally provide a types for the result.
|
168
|
+
# The result value defaults as the evaluated value of the given block.
|
169
|
+
# If you want another value you can pass it through the `data:` argument.
|
170
|
+
#
|
171
|
+
# @example
|
172
|
+
# class CheckMathWorks < FService::Base
|
173
|
+
# def run
|
174
|
+
# Check(:math_works) { 1 < 2 }
|
175
|
+
# # => #<Success @value=true, @types=[:math_works]>
|
176
|
+
#
|
177
|
+
# Check(:math_works) { 1 > 2 }
|
178
|
+
# # => #<Failure @error=false, @types=[:math_works]>
|
179
|
+
#
|
180
|
+
# Check(:math_works, data: 1 + 2) { 1 > 2 }
|
181
|
+
# # => #<Failure @types=:math_works, @error=3>
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
# Check(:math_works, data: 1 + 2) { 1 < 2 }
|
185
|
+
# # => #<Success @types=[:math_works], @value=3>
|
186
|
+
# end
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# @param types the Result types
|
190
|
+
# @return [Result::Success, Result::Failure] a Result from the boolean expression
|
191
|
+
def Check(*types, data: nil)
|
192
|
+
res = yield
|
193
|
+
|
194
|
+
final_data = data || res
|
195
|
+
|
196
|
+
res ? Success(*types, data: final_data) : Failure(*types, data: final_data)
|
197
|
+
end
|
198
|
+
|
199
|
+
# If the given block raises an exception, it wraps it in a Failure.
|
200
|
+
# Otherwise, maps the block value in a Success object.
|
201
|
+
# You can specify which exceptions to watch for.
|
202
|
+
# It's possible to provide a types for the result too.
|
203
|
+
#
|
204
|
+
# @example
|
205
|
+
# class IHateEvenNumbers < FService::Base
|
206
|
+
# def run
|
207
|
+
# Try(:rand_int) do
|
208
|
+
# n = rand(1..10)
|
209
|
+
# raise "Yuck! It's a #{n}" if n.even?
|
210
|
+
#
|
211
|
+
# n
|
212
|
+
# end
|
213
|
+
# end
|
214
|
+
# end
|
215
|
+
#
|
216
|
+
# IHateEvenNumbers.call
|
217
|
+
# # => #<Success @value=9, @types=[:rand_int]>
|
218
|
+
#
|
219
|
+
# IHateEvenNumbers.call
|
220
|
+
# # => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @types=[:rand_int]>
|
221
|
+
#
|
222
|
+
# @param types the Result types
|
223
|
+
# @param catch the exception list to catch
|
224
|
+
# @return [Result::Success, Result::Failure] a result from the boolean expression
|
225
|
+
def Try(*types, catch: StandardError)
|
226
|
+
res = yield
|
227
|
+
|
228
|
+
Success(*types, data: res)
|
229
|
+
rescue *catch => e
|
230
|
+
Failure(*types, data: e)
|
78
231
|
end
|
79
232
|
|
80
233
|
# Returns a failed operation.
|
@@ -93,12 +246,19 @@ module FService
|
|
93
246
|
# end
|
94
247
|
# end
|
95
248
|
#
|
96
|
-
# @
|
249
|
+
# @deprecated Use {#Failure} instead.
|
250
|
+
# @return [Result::Failure] a failed operation
|
97
251
|
def failure(data = nil)
|
98
|
-
FService
|
252
|
+
FService.deprecate!(
|
253
|
+
name: "#{self.class}##{__method__}",
|
254
|
+
alternative: '#Failure',
|
255
|
+
from: caller[0]
|
256
|
+
)
|
257
|
+
|
258
|
+
Result::Failure.new(data)
|
99
259
|
end
|
100
260
|
|
101
|
-
# Return either {
|
261
|
+
# Return either {Result::Failure Success} or {Result::Failure Failure}
|
102
262
|
# given the condition.
|
103
263
|
#
|
104
264
|
# @example
|
@@ -120,8 +280,15 @@ module FService
|
|
120
280
|
# end
|
121
281
|
# end
|
122
282
|
#
|
123
|
-
# @
|
283
|
+
# @deprecated Use {#Check} instead.
|
284
|
+
# @return [Result::Success, Result::Failure]
|
124
285
|
def result(condition, data = nil)
|
286
|
+
FService.deprecate!(
|
287
|
+
name: "#{self.class}##{__method__}",
|
288
|
+
alternative: '#Check',
|
289
|
+
from: caller[0]
|
290
|
+
)
|
291
|
+
|
125
292
|
condition ? success(data) : failure(data)
|
126
293
|
end
|
127
294
|
end
|
@@ -7,24 +7,54 @@ module FService
|
|
7
7
|
#
|
8
8
|
# @abstract
|
9
9
|
class Base
|
10
|
-
|
10
|
+
attr_reader :types
|
11
|
+
|
12
|
+
%i[and_then successful? failed? value value! error].each do |method_name|
|
11
13
|
define_method(method_name) do |*_args|
|
12
14
|
raise NotImplementedError, "called #{method_name} on class Result::Base"
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
18
|
+
# You usually shouldn't call this directly. See {FService::Base#Failure} and {FService::Base#Success}.
|
19
|
+
def initialize(types = [])
|
20
|
+
@handled = false
|
21
|
+
@types = types
|
22
|
+
@matching_types = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Implements old attribute type. Its deprecated in favor of using types.
|
26
|
+
def type
|
27
|
+
FService.deprecate!(name: "#{self.class}##{__method__}", alternative: '#types', from: caller[0])
|
28
|
+
|
29
|
+
types.size == 1 ? types.first : Array(@matching_types).first
|
30
|
+
end
|
31
|
+
|
32
|
+
# This hook runs if the result is successful.
|
33
|
+
# Can receive one or more types to be checked before running the given block.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# class UsersController < BaseController
|
37
|
+
# def update
|
38
|
+
# User::Update.(user: user)
|
39
|
+
# .on_success(:type, :type2) { return json_success({ status: :ok }) } # run only if type matches
|
40
|
+
# .on_success { |value| return json_success(value) }
|
41
|
+
# .on_failure { |error| return json_error(error) } # this won't run
|
42
|
+
# end
|
19
43
|
#
|
44
|
+
# private
|
45
|
+
#
|
46
|
+
# def user
|
47
|
+
# @user ||= User.find_by!(slug: params[:slug])
|
48
|
+
# end
|
49
|
+
# end
|
20
50
|
#
|
21
51
|
# @example
|
22
52
|
# class UsersController < BaseController
|
23
53
|
# def update
|
24
|
-
# User::Update.(user: user)
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
54
|
+
# User::Update.(user: user)
|
55
|
+
# .on_success(:type, :type2) { return json_success({ status: :ok }) } # run only if type matches
|
56
|
+
# .on_success(unhandled: true) { |value| return json_success(value) }
|
57
|
+
# .on_failure(unhandled: true) { |error| return json_error(error) } # this won't run
|
28
58
|
# end
|
29
59
|
#
|
30
60
|
# private
|
@@ -34,15 +64,96 @@ module FService
|
|
34
64
|
# end
|
35
65
|
# end
|
36
66
|
#
|
37
|
-
# @
|
38
|
-
# @
|
67
|
+
# @yieldparam value value of the failure object
|
68
|
+
# @yieldparam type type of the failure object
|
69
|
+
# @return [Success, Failure] the original Result object
|
39
70
|
# @api public
|
40
|
-
def
|
41
|
-
if successful?
|
42
|
-
|
43
|
-
|
44
|
-
|
71
|
+
def on_success(*target_types, unhandled: false)
|
72
|
+
if successful? && unhandled? && expected_type?(target_types, unhandled: unhandled)
|
73
|
+
match_types(target_types)
|
74
|
+
yield(*to_ary)
|
75
|
+
@handled = true
|
76
|
+
freeze
|
45
77
|
end
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# This hook runs if the result is failed.
|
83
|
+
# Can receive one or more types to be checked before running the given block.
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# class UsersController < BaseController
|
87
|
+
# def update
|
88
|
+
# User::Update.(user: user)
|
89
|
+
# .on_success { |value| return json_success(value) } # this won't run
|
90
|
+
# .on_failure(:type, :type2) { |error| return json_error(error) } # runs only if type matches
|
91
|
+
# .on_failure { |error| return json_error(error) }
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# private
|
95
|
+
#
|
96
|
+
# def user
|
97
|
+
# @user ||= User.find_by!(slug: params[:slug])
|
98
|
+
# end
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# class UsersController < BaseController
|
103
|
+
# def update
|
104
|
+
# User::Update.(user: user)
|
105
|
+
# .on_success(:unhandled: true) { |value| return json_success(value) } # this won't run
|
106
|
+
# .on_failure(:type, :type2) { |error| return json_error(error) } # runs only if type matches
|
107
|
+
# .on_failure(:unhandled: true) { |error| return json_error(error) }
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# private
|
111
|
+
#
|
112
|
+
# def user
|
113
|
+
# @user ||= User.find_by!(slug: params[:slug])
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# @yieldparam value value of the failure object
|
118
|
+
# @yieldparam type type of the failure object
|
119
|
+
# @return [Success, Failure] the original Result object
|
120
|
+
# @api public
|
121
|
+
def on_failure(*target_types, unhandled: false)
|
122
|
+
if failed? && unhandled? && expected_type?(target_types, unhandled: unhandled)
|
123
|
+
match_types(target_types)
|
124
|
+
yield(*to_ary)
|
125
|
+
@handled = true
|
126
|
+
freeze
|
127
|
+
end
|
128
|
+
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
# Splits the result object into its components.
|
133
|
+
#
|
134
|
+
# @return [Array] value and type of the result object
|
135
|
+
def to_ary
|
136
|
+
data = successful? ? value : error
|
137
|
+
|
138
|
+
[data, @matching_types.first]
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def handled?
|
144
|
+
@handled
|
145
|
+
end
|
146
|
+
|
147
|
+
def unhandled?
|
148
|
+
!handled?
|
149
|
+
end
|
150
|
+
|
151
|
+
def expected_type?(target_types, unhandled:)
|
152
|
+
target_types.empty? || unhandled || target_types.any? { |target_type| types.include?(target_type) }
|
153
|
+
end
|
154
|
+
|
155
|
+
def match_types(target_types)
|
156
|
+
@matching_types = target_types.empty? ? types : target_types & types
|
46
157
|
end
|
47
158
|
end
|
48
159
|
end
|
@@ -8,18 +8,21 @@ module FService
|
|
8
8
|
# Represents a value of a failed operation.
|
9
9
|
# The error field can contain any information you want.
|
10
10
|
#
|
11
|
+
# @!attribute [r] error
|
12
|
+
# @return [Object] the provided error for the result
|
13
|
+
# @!attribute [r] types
|
14
|
+
# @return [Object] the provided types for the result. Defaults to nil.
|
11
15
|
# @api public
|
12
16
|
class Failure < Result::Base
|
13
|
-
# Returns the provided error
|
14
17
|
attr_reader :error
|
15
18
|
|
16
19
|
# Creates a failed operation.
|
17
|
-
# You usually shouldn't call this directly. See {FService::Base#
|
20
|
+
# You usually shouldn't call this directly. See {FService::Base#Failure}.
|
18
21
|
#
|
19
22
|
# @param error [Object] failure value.
|
20
|
-
def initialize(error)
|
23
|
+
def initialize(error, types = [])
|
24
|
+
super(types)
|
21
25
|
@error = error
|
22
|
-
freeze
|
23
26
|
end
|
24
27
|
|
25
28
|
# Returns false.
|
@@ -55,6 +58,33 @@ module FService
|
|
55
58
|
raise Result::Error, 'Failure objects do not have value'
|
56
59
|
end
|
57
60
|
|
61
|
+
# Returns the current error to the given block.
|
62
|
+
# Use this to chain multiple service calls (since all services return Results).
|
63
|
+
# It works just like the `.and_then` method, but only runs if the result is a Failure.
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# class UpdateUserOnExternalService
|
68
|
+
# attribute :user_params
|
69
|
+
#
|
70
|
+
# def run
|
71
|
+
# check_api_status
|
72
|
+
# .and_then { update_user }
|
73
|
+
# .or_else { create_update_worker }
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# private
|
77
|
+
# # some code
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @yieldparam error pass {#error} to a block
|
81
|
+
# @yieldparam type pass {#type} to a block
|
82
|
+
def catch
|
83
|
+
yield(*to_ary)
|
84
|
+
end
|
85
|
+
|
86
|
+
alias or_else catch
|
87
|
+
|
58
88
|
# Returns itself to the given block.
|
59
89
|
# Use this to chain multiple service calls (since all services return Results).
|
60
90
|
# It will short circuit your service call chain.
|
@@ -64,8 +94,8 @@ module FService
|
|
64
94
|
# class UsersController < BaseController
|
65
95
|
# def create
|
66
96
|
# result = User::Create.(user_params) # if this fails the following calls won't run
|
67
|
-
#
|
68
|
-
#
|
97
|
+
# .and_then { |user| User::SendWelcomeEmail.(user: user) }
|
98
|
+
# .and_then { |user| User::Login.(user: user) }
|
69
99
|
#
|
70
100
|
# if result.successful?
|
71
101
|
# json_success(result.value)
|
@@ -76,10 +106,16 @@ module FService
|
|
76
106
|
# end
|
77
107
|
#
|
78
108
|
# @return [self]
|
79
|
-
def
|
109
|
+
def and_then
|
80
110
|
self
|
81
111
|
end
|
82
112
|
|
113
|
+
# See #and_then
|
114
|
+
def then
|
115
|
+
FService.deprecate!(name: "#{self.class}##{__method__}", alternative: '#and_then', from: caller[0])
|
116
|
+
and_then
|
117
|
+
end
|
118
|
+
|
83
119
|
# Outputs a string representation of the object
|
84
120
|
#
|
85
121
|
#
|
@@ -7,18 +7,21 @@ module FService
|
|
7
7
|
# Represents a value of a successful operation.
|
8
8
|
# The value field can contain any information you want.
|
9
9
|
#
|
10
|
+
# @!attribute [r] value
|
11
|
+
# @return [Object] the provided value for the result
|
12
|
+
# @!attribute [r] types
|
13
|
+
# @return [Object] the provided types for the result. Defaults to nil.
|
10
14
|
# @api public
|
11
15
|
class Success < Result::Base
|
12
|
-
# Returns the provided value.
|
13
16
|
attr_reader :value
|
14
17
|
|
15
18
|
# Creates a successful operation.
|
16
|
-
# You usually shouldn't call this directly. See {FService::Base#
|
19
|
+
# You usually shouldn't call this directly. See {FService::Base#Success}.
|
17
20
|
#
|
18
21
|
# @param value [Object] success value.
|
19
|
-
def initialize(value)
|
22
|
+
def initialize(value, types = [])
|
23
|
+
super(types)
|
20
24
|
@value = value
|
21
|
-
freeze
|
22
25
|
end
|
23
26
|
|
24
27
|
# Returns true.
|
@@ -63,8 +66,8 @@ module FService
|
|
63
66
|
# class UsersController < BaseController
|
64
67
|
# def create
|
65
68
|
# result = User::Create.(user_params)
|
66
|
-
#
|
67
|
-
#
|
69
|
+
# .and_then { |user| User::SendWelcomeEmail.(user: user) }
|
70
|
+
# .and_then { |user| User::Login.(user: user) }
|
68
71
|
#
|
69
72
|
# if result.successful?
|
70
73
|
# json_success(result.value)
|
@@ -74,11 +77,45 @@ module FService
|
|
74
77
|
# end
|
75
78
|
# end
|
76
79
|
#
|
77
|
-
# @yieldparam
|
78
|
-
|
79
|
-
|
80
|
+
# @yieldparam value pass {#value} to a block
|
81
|
+
# @yieldparam types pass {#types} to a block
|
82
|
+
def and_then
|
83
|
+
yield(*to_ary)
|
80
84
|
end
|
81
85
|
|
86
|
+
# See #and_then
|
87
|
+
def then(&block)
|
88
|
+
FService.deprecate!(name: "#{self.class}##{__method__}", alternative: '#and_then', from: caller[0])
|
89
|
+
|
90
|
+
and_then(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns itself to the given block.
|
94
|
+
# Use this to chain multiple actions or service calls (only valid when they return a Result).
|
95
|
+
# It works just like the `.and_then` method, but only runs if service is a Failure.
|
96
|
+
#
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# class UpdateUserOnExternalService
|
100
|
+
# attribute :user_params
|
101
|
+
#
|
102
|
+
# def run
|
103
|
+
# check_api_status
|
104
|
+
# .and_then { update_user }
|
105
|
+
# .or_else { create_update_worker }
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# private
|
109
|
+
# # some code
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# @return [self]
|
113
|
+
def catch
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
alias or_else catch
|
118
|
+
|
82
119
|
# Outputs a string representation of the object
|
83
120
|
#
|
84
121
|
#
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Methods to mock a FService result from a service call.
|
4
|
+
module FServiceResultHelpers
|
5
|
+
# Create an Fservice result Success or Failure.
|
6
|
+
def f_service_result(result, value = nil, types = [])
|
7
|
+
if result == :success
|
8
|
+
FService::Result::Success.new(value, Array(types))
|
9
|
+
else
|
10
|
+
FService::Result::Failure.new(value, Array(types))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Mock a Fservice service call returning a result.
|
15
|
+
def mock_service(service, result: :success, value: nil, type: :not_passed, types: [])
|
16
|
+
result_types = Array(types)
|
17
|
+
|
18
|
+
if type != :not_passed
|
19
|
+
alternative = "mock_service(..., types: [#{type.inspect}])"
|
20
|
+
name = 'mock_service'
|
21
|
+
FService.deprecate_argument_name(name: name, argument_name: :type, alternative: alternative, from: caller[0])
|
22
|
+
result_types = Array(type)
|
23
|
+
end
|
24
|
+
|
25
|
+
service_result = f_service_result(result, value, result_types)
|
26
|
+
allow(service).to receive(:call).and_return(service_result)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
RSpec.configure do |config|
|
31
|
+
config.include FServiceResultHelpers
|
32
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/expectations'
|
4
|
+
|
5
|
+
RSpec::Matchers.define :have_failed_with do |*expected_types|
|
6
|
+
match do |actual|
|
7
|
+
matched = actual.is_a?(FService::Result::Failure) && actual.types == expected_types
|
8
|
+
|
9
|
+
matched &&= actual.error == @expected_error if defined?(@expected_error)
|
10
|
+
|
11
|
+
matched
|
12
|
+
end
|
13
|
+
|
14
|
+
chain :and_error do |expected_error|
|
15
|
+
@expected_error = expected_error
|
16
|
+
end
|
17
|
+
|
18
|
+
failure_message do |actual|
|
19
|
+
if actual.is_a?(FService::Result::Failure)
|
20
|
+
message = "expected failure's types '#{actual.types.inspect}' to be equal '#{expected_types.inspect}'"
|
21
|
+
if defined?(@expected_error)
|
22
|
+
has_description = @expected_error.respond_to?(:description)
|
23
|
+
message += " and error '#{actual.error.inspect}' to be "
|
24
|
+
message += has_description ? @expected_error.description : "equal '#{@expected_error.inspect}'"
|
25
|
+
end
|
26
|
+
|
27
|
+
message
|
28
|
+
else
|
29
|
+
"result '#{actual.inspect}' is not a Failure object"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Matchers.define :have_succeed_with do |*expected_types|
|
35
|
+
match do |actual|
|
36
|
+
matched = actual.is_a?(FService::Result::Success) && actual.types == expected_types
|
37
|
+
|
38
|
+
matched &&= values_match?(@expected_value, actual.value) if defined?(@expected_value)
|
39
|
+
|
40
|
+
matched
|
41
|
+
end
|
42
|
+
|
43
|
+
chain :and_value do |expected_value|
|
44
|
+
@expected_value = expected_value
|
45
|
+
end
|
46
|
+
|
47
|
+
failure_message do |actual|
|
48
|
+
if actual.is_a?(FService::Result::Success)
|
49
|
+
message = "expected success's types '#{actual.types.inspect}' to be equal '#{expected_types.inspect}'"
|
50
|
+
if defined?(@expected_value)
|
51
|
+
has_description = @expected_value.respond_to?(:description)
|
52
|
+
message += " and value '#{actual.value.inspect}' to be "
|
53
|
+
message += has_description ? @expected_value.description : "equal '#{@expected_value.inspect}'"
|
54
|
+
end
|
55
|
+
|
56
|
+
message
|
57
|
+
else
|
58
|
+
"result '#{actual.inspect}' is not a Success object"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/f_service/version.rb
CHANGED
data/lib/f_service.rb
CHANGED
@@ -6,4 +6,27 @@ require_relative 'f_service/base'
|
|
6
6
|
#
|
7
7
|
# @api public
|
8
8
|
module FService
|
9
|
+
# Marks a method as deprecated
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
def self.deprecate!(name:, alternative:, from: nil)
|
13
|
+
warn_message = ["\n[DEPRECATED] #{name} is deprecated; "]
|
14
|
+
warn_message << ["called from #{from}; "] unless from.nil?
|
15
|
+
warn_message << "use #{alternative} instead. "
|
16
|
+
warn_message << 'It will be removed on the next release.'
|
17
|
+
|
18
|
+
warn warn_message.join("\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Marks an argument as deprecated
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
def self.deprecate_argument_name(name:, argument_name:, alternative:, from: nil)
|
25
|
+
warn_message = ["\n[DEPRECATED] #{name} passing #{argument_name.inspect} is deprecated; "]
|
26
|
+
warn_message << ["called from #{from}; "] unless from.nil?
|
27
|
+
warn_message << "use #{alternative} instead. "
|
28
|
+
warn_message << 'It will be removed on the next release.'
|
29
|
+
|
30
|
+
warn warn_message.join("\n")
|
31
|
+
end
|
9
32
|
end
|
data/logo.png
ADDED
Binary file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: f_service
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Fretadao Tech Team
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -29,11 +29,12 @@ description: |2
|
|
29
29
|
The goal is to make services simpler, safer and more composable.
|
30
30
|
It uses the Result monad for handling operations.
|
31
31
|
email:
|
32
|
-
-
|
32
|
+
- tech@fretadao.com.br
|
33
33
|
executables: []
|
34
34
|
extensions: []
|
35
35
|
extra_rdoc_files: []
|
36
36
|
files:
|
37
|
+
- ".github/.dependabot.yml"
|
37
38
|
- ".github/workflows/tests-and-linter.yml"
|
38
39
|
- ".gitignore"
|
39
40
|
- ".rubocop.yml"
|
@@ -52,7 +53,14 @@ files:
|
|
52
53
|
- lib/f_service/result/base.rb
|
53
54
|
- lib/f_service/result/failure.rb
|
54
55
|
- lib/f_service/result/success.rb
|
56
|
+
- lib/f_service/rspec.rb
|
57
|
+
- lib/f_service/rspec/support.rb
|
58
|
+
- lib/f_service/rspec/support/helpers.rb
|
59
|
+
- lib/f_service/rspec/support/helpers/result.rb
|
60
|
+
- lib/f_service/rspec/support/matchers.rb
|
61
|
+
- lib/f_service/rspec/support/matchers/result.rb
|
55
62
|
- lib/f_service/version.rb
|
63
|
+
- logo.png
|
56
64
|
homepage: https://github.com/Fretadao/f_service
|
57
65
|
licenses:
|
58
66
|
- MIT
|
@@ -60,6 +68,7 @@ metadata:
|
|
60
68
|
homepage_uri: https://github.com/Fretadao/f_service
|
61
69
|
source_code_uri: https://github.com/Fretadao/f_service
|
62
70
|
documentation_uri: https://www.rubydoc.info/gems/f_service
|
71
|
+
changelog_uri: https://github.com/Fretadao/f_service/blob/master/CHANGELOG.md
|
63
72
|
post_install_message:
|
64
73
|
rdoc_options: []
|
65
74
|
require_paths:
|
@@ -75,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
84
|
- !ruby/object:Gem::Version
|
76
85
|
version: '0'
|
77
86
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
87
|
+
rubygems_version: 3.3.5
|
79
88
|
signing_key:
|
80
89
|
specification_version: 4
|
81
90
|
summary: A small, monad-based service class
|