f_service 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/tests-and-linter.yml +2 -2
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +31 -7
- data/Gemfile.lock +12 -8
- data/README.md +122 -16
- data/f_service.gemspec +1 -1
- data/lib/f_service.rb +8 -0
- data/lib/f_service/base.rb +187 -23
- data/lib/f_service/result/base.rb +79 -0
- data/lib/f_service/result/failure.rb +39 -7
- data/lib/f_service/result/success.rb +41 -9
- data/lib/f_service/version.rb +1 -1
- data/logo.png +0 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb3dfcc7d2f09fe4bd1cabca95a9520b06badf64e1d8fd7a238e0b904cdbbfe9
|
4
|
+
data.tar.gz: 4f71a8ecfb48c4b928b7240d63a63469a36f46635b081f221d064b0603d73be7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e3378dc3e210fd41aa994270792eb5a2dbf003f351964d81e08c4505404bc90b765fd33c4d49d39893a83d64dded6d1a5efa72bc4699ec23d7cd6273535e22d
|
7
|
+
data.tar.gz: 175330f3f2fbccdcc9b74f93b69ffa0a4002699139b63d19208da97327722346b154f83d46bde3f71bf23016ef23065727cb5c876fb45ed076375652da70e5b7
|
@@ -11,12 +11,12 @@ jobs:
|
|
11
11
|
runs-on: ubuntu-latest
|
12
12
|
strategy:
|
13
13
|
matrix:
|
14
|
-
ruby: [2.
|
14
|
+
ruby: [2.5, 2.6, 2.7, '3.0']
|
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
data/CHANGELOG.md
CHANGED
@@ -4,16 +4,40 @@ 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.2.0
|
14
|
+
### Added
|
15
|
+
- Add `and_catch` as alias for `then` (partially fix [#23](https://github.com/Fretadao/f_service/issues/23)).
|
16
|
+
- Add `catch` to `Failure` and `Success`. It acts as an inverted `then` and has a `or_else` alias.
|
17
|
+
- Add support to custom `data` property to be passed when calling `Base#Check`.
|
18
|
+
- Add support to multiple type checks on `Result#on_success` and `Result#on_failure` hooks.
|
19
|
+
- Yields result type on blocks (`then`, `on_success` and `on_failure`).
|
20
|
+
- Add type check on `Result#on_success` and `Result#on_failure` hooks.
|
21
|
+
- Add method `Base#Try`. It wraps exceptions in Failures.
|
22
|
+
- Add method `Base#Check`. It converts booleans to Results.
|
23
|
+
- Add methods `#Success(type, data:)` and `#Failure(type, data:)` on `FService::Base`.
|
24
|
+
These methods allow defining the type and value of the Result object.
|
25
|
+
- Allow adding types on `Result`s.
|
26
|
+
- Add `#on_success` and `#on_failure` hooks on Result objects.
|
27
|
+
- Link to Changelog on gemspec.
|
28
|
+
|
29
|
+
### Changed
|
30
|
+
- **[Deprecation]** Mark `Base#result` as deprecated. They will be removed on the next release. Use the `Base#Check` instead.
|
31
|
+
- **[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.
|
32
|
+
- **[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
33
|
|
10
34
|
## 0.1.1
|
11
35
|
### Added
|
12
36
|
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
|
37
|
+
- Result based services
|
38
|
+
- Type check on results
|
39
|
+
- Pattern matching with `#call`ables
|
40
|
+
- Safe chaining calls with `#then`
|
17
41
|
|
18
42
|
## 0.1.0
|
19
|
-
- **Yanked**
|
43
|
+
- **Yanked**
|
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.2.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -10,16 +10,18 @@ GEM
|
|
10
10
|
backport (1.1.2)
|
11
11
|
benchmark (0.1.0)
|
12
12
|
diff-lcs (1.3)
|
13
|
-
docile (1.3.
|
13
|
+
docile (1.3.5)
|
14
14
|
e2mmap (0.1.0)
|
15
15
|
jaro_winkler (1.5.4)
|
16
16
|
maruku (0.7.3)
|
17
|
-
mini_portile2 (2.
|
18
|
-
nokogiri (1.
|
19
|
-
mini_portile2 (~> 2.
|
17
|
+
mini_portile2 (2.5.0)
|
18
|
+
nokogiri (1.11.1)
|
19
|
+
mini_portile2 (~> 2.5.0)
|
20
|
+
racc (~> 1.4)
|
20
21
|
parallel (1.19.1)
|
21
22
|
parser (2.7.1.1)
|
22
23
|
ast (~> 2.4.0)
|
24
|
+
racc (1.5.2)
|
23
25
|
rainbow (3.0.0)
|
24
26
|
rake (13.0.1)
|
25
27
|
reverse_markdown (1.4.0)
|
@@ -49,10 +51,12 @@ GEM
|
|
49
51
|
rubocop-rspec (1.38.1)
|
50
52
|
rubocop (>= 0.68.1)
|
51
53
|
ruby-progressbar (1.10.1)
|
52
|
-
simplecov (0.
|
54
|
+
simplecov (0.21.2)
|
53
55
|
docile (~> 1.1)
|
54
56
|
simplecov-html (~> 0.11)
|
55
|
-
|
57
|
+
simplecov_json_formatter (~> 0.1)
|
58
|
+
simplecov-html (0.12.3)
|
59
|
+
simplecov_json_formatter (0.1.2)
|
56
60
|
solargraph (0.38.6)
|
57
61
|
backport (~> 1.1)
|
58
62
|
benchmark
|
@@ -87,4 +91,4 @@ DEPENDENCIES
|
|
87
91
|
yard
|
88
92
|
|
89
93
|
BUNDLED WITH
|
90
|
-
2.
|
94
|
+
2.1.4
|
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 type 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) if @name.nil?
|
51
68
|
|
52
69
|
user = UserRepository.create(name: @name)
|
53
70
|
if user.valid?
|
54
|
-
|
71
|
+
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,63 @@ 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:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class UsersController < BaseController
|
123
|
+
def create
|
124
|
+
User::Create.(user_params)
|
125
|
+
.on_success { |value| return json_success(value) }
|
126
|
+
.on_failure { |error| return json_error(error) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
> You can ignore any of the callbacks, if you want to.
|
132
|
+
|
133
|
+
Going further, you can match the Result type, in case you want to handle them differently:
|
98
134
|
|
99
135
|
```ruby
|
100
136
|
class UsersController < BaseController
|
101
137
|
def create
|
102
|
-
User::Create.(user_params)
|
103
|
-
|
104
|
-
|
105
|
-
|
138
|
+
User::Create.(user_params)
|
139
|
+
.on_success(:user_created) { |value| return json_success(value) }
|
140
|
+
.on_success(:user_already_exists) { |value| return json_success(value) }
|
141
|
+
.on_failure(:invalid_data) { |error| return json_error(error) }
|
142
|
+
.on_failure(:critical_error) do |error|
|
143
|
+
MyLogger.report_failure(error)
|
144
|
+
|
145
|
+
return json_error(error)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
It's possible to provide multiple types to the hooks too. If the result type matches any of the given types,
|
152
|
+
the hook will run.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class UsersController < BaseController
|
156
|
+
def create
|
157
|
+
User::Create.(user_params)
|
158
|
+
.on_success(:user_created, :user_already_exists) { |value| return json_success(value) }
|
159
|
+
.on_failure(:invalid_data) { |error| return json_error(error) }
|
160
|
+
.on_failure(:critical_error) do |error|
|
161
|
+
MyLogger.report_failure(error)
|
162
|
+
|
163
|
+
return json_error(error)
|
164
|
+
end
|
106
165
|
end
|
107
166
|
end
|
108
167
|
```
|
109
|
-
> You can use any object that responds to #call, not only Lambdas.
|
110
168
|
|
111
169
|
### Chaining services
|
170
|
+
|
112
171
|
Since all services return Results, you can chain service calls making a data pipeline.
|
113
172
|
If some step fails, it will short circuit the call chain.
|
114
173
|
|
@@ -116,8 +175,8 @@ If some step fails, it will short circuit the call chain.
|
|
116
175
|
class UsersController < BaseController
|
117
176
|
def create
|
118
177
|
result = User::Create.(user_params)
|
119
|
-
.
|
120
|
-
.
|
178
|
+
.and_then { |user| User::Login.(user) }
|
179
|
+
.and_then { |user| User::SendWelcomeEmail.(user) }
|
121
180
|
|
122
181
|
if result.successful?
|
123
182
|
json_success(result.value)
|
@@ -128,6 +187,53 @@ class UsersController < BaseController
|
|
128
187
|
end
|
129
188
|
```
|
130
189
|
|
190
|
+
You can use the `.to_proc` method on FService::Base to avoid explicit inputs when chaining services:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class UsersController < BaseController
|
194
|
+
def create
|
195
|
+
result = User::Create.(user_params)
|
196
|
+
.and_then(&User::Login)
|
197
|
+
.and_then(&User::SendWelcomeEmail)
|
198
|
+
# ...
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
### `Check` and `Try`
|
204
|
+
|
205
|
+
You can use `Check` to converts a boolean to a Result, truthy values map to `Success`, and falsey values map to `Failures`:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
Check(:math_works) { 1 < 2 }
|
209
|
+
# => #<Success @value=true, @type=:math_works>
|
210
|
+
|
211
|
+
Check(:math_works) { 1 > 2 }
|
212
|
+
# => #<Failure @error=false, @type=:math_works>
|
213
|
+
```
|
214
|
+
|
215
|
+
`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
|
216
|
+
using the parameter `catch`.
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class IHateEvenNumbers < FService::Base
|
220
|
+
def run
|
221
|
+
Try(:rand_int) do
|
222
|
+
n = rand(1..10)
|
223
|
+
raise "Yuck! It's a #{n}" if n.even?
|
224
|
+
|
225
|
+
n
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
IHateEvenNumbers.call
|
231
|
+
# => #<Success @value=9, @type=:rand_int>
|
232
|
+
|
233
|
+
IHateEvenNumbers.call
|
234
|
+
# => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @type=:rand_int>
|
235
|
+
```
|
236
|
+
|
131
237
|
## API Docs
|
132
238
|
|
133
239
|
You can access the API docs [here](https://www.rubydoc.info/gems/f_service/).
|
data/f_service.gemspec
CHANGED
@@ -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.rb
CHANGED
@@ -6,4 +6,12 @@ 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:)
|
13
|
+
warn "[DEPRECATED] #{name} is deprecated; " \
|
14
|
+
"use #{alternative} instead. " \
|
15
|
+
'It will be removed on the next release.'
|
16
|
+
end
|
9
17
|
end
|
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,137 @@ 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
|
+
)
|
106
|
+
|
107
|
+
Result::Success.new(data)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns a successful result.
|
111
|
+
# You can optionally specify a type and a value for your result.
|
112
|
+
# You'll probably want to return this inside {#run}.
|
113
|
+
#
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# def run
|
117
|
+
# Success()
|
118
|
+
# # => #<Success @value=nil, @type=nil>
|
119
|
+
#
|
120
|
+
# Success(:ok)
|
121
|
+
# # => #<Success @value=nil, @type=:ok>
|
122
|
+
#
|
123
|
+
# Success(data: 10)
|
124
|
+
# # => #<Success @value=10, @type=nil>
|
125
|
+
#
|
126
|
+
# Success(:ok, data: 10)
|
127
|
+
# # => #<Success @value=10, @type=:ok>
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# @param type the Result type
|
131
|
+
# @param data the result value
|
132
|
+
# @return [Result::Success] a successful result
|
133
|
+
def Success(type = nil, data: nil)
|
134
|
+
Result::Success.new(data, type)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns a failed result.
|
138
|
+
# You can optionally specify a type and a value for your result.
|
139
|
+
# You'll probably want to return this inside {#run}.
|
140
|
+
#
|
141
|
+
#
|
142
|
+
# @example
|
143
|
+
# def run
|
144
|
+
# Failure()
|
145
|
+
# # => #<Failure @error=nil, @type=nil>
|
146
|
+
#
|
147
|
+
# Failure(:not_a_number)
|
148
|
+
# # => #<Failure @error=nil, @type=:not_a_number>
|
149
|
+
#
|
150
|
+
# Failure(data: "10")
|
151
|
+
# # => #<Failure @error="10", @type=nil>
|
152
|
+
#
|
153
|
+
# Failure(:not_a_number, data: "10")
|
154
|
+
# # => #<Failure @error="10", @type=:not_a_number>
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# @param type the Result type
|
158
|
+
# @param data the result value
|
159
|
+
# @return [Result::Failure] a failed result
|
160
|
+
def Failure(type = nil, data: nil)
|
161
|
+
Result::Failure.new(data, type)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Converts a boolean to a Result.
|
165
|
+
# Truthy values map to Success, and falsey values map to Failures.
|
166
|
+
# You can optionally provide a type for the result.
|
167
|
+
# The result value defaults as the evaluated value of the given block.
|
168
|
+
# If you want another value you can pass it through the `data:` argument.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# class CheckMathWorks < FService::Base
|
172
|
+
# def run
|
173
|
+
# Check(:math_works) { 1 < 2 }
|
174
|
+
# # => #<Success @value=true, @type=:math_works>
|
175
|
+
#
|
176
|
+
# Check(:math_works) { 1 > 2 }
|
177
|
+
# # => #<Failure @error=false, @type=:math_works>
|
178
|
+
#
|
179
|
+
# Check(:math_works, data: 1 + 2) { 1 > 2 }
|
180
|
+
# # => #<Failure @type=:math_works, @error=3>
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# Check(:math_works, data: 1 + 2) { 1 < 2 }
|
184
|
+
# # => #<Success @type=:math_works, @value=3>
|
185
|
+
# end
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# @param type the Result type
|
189
|
+
# @return [Result::Success, Result::Failure] a Result from the boolean expression
|
190
|
+
def Check(type = nil, data: nil)
|
191
|
+
res = yield
|
192
|
+
|
193
|
+
final_data = data || res
|
194
|
+
|
195
|
+
res ? Success(type, data: final_data) : Failure(type, data: final_data)
|
196
|
+
end
|
197
|
+
|
198
|
+
# If the given block raises an exception, it wraps it in a Failure.
|
199
|
+
# Otherwise, maps the block value in a Success object.
|
200
|
+
# You can specify which exceptions to watch for.
|
201
|
+
# It's possible to provide a type for the result too.
|
202
|
+
#
|
203
|
+
# @example
|
204
|
+
# class IHateEvenNumbers < FService::Base
|
205
|
+
# def run
|
206
|
+
# Try(:rand_int) do
|
207
|
+
# n = rand(1..10)
|
208
|
+
# raise "Yuck! It's a #{n}" if n.even?
|
209
|
+
#
|
210
|
+
# n
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
# end
|
214
|
+
#
|
215
|
+
# IHateEvenNumbers.call
|
216
|
+
# # => #<Success @value=9, @type=:rand_int>
|
217
|
+
#
|
218
|
+
# IHateEvenNumbers.call
|
219
|
+
# # => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @type=:rand_int>
|
220
|
+
#
|
221
|
+
# @param type the Result type
|
222
|
+
# @param catch the exception list to catch
|
223
|
+
# @return [Result::Success, Result::Failure] a result from the boolean expression
|
224
|
+
def Try(type = nil, catch: StandardError)
|
225
|
+
res = yield
|
226
|
+
|
227
|
+
Success(type, data: res)
|
228
|
+
rescue *catch => e
|
229
|
+
Failure(type, data: e)
|
78
230
|
end
|
79
231
|
|
80
232
|
# Returns a failed operation.
|
@@ -93,12 +245,18 @@ module FService
|
|
93
245
|
# end
|
94
246
|
# end
|
95
247
|
#
|
96
|
-
# @
|
248
|
+
# @deprecated Use {#Failure} instead.
|
249
|
+
# @return [Result::Failure] a failed operation
|
97
250
|
def failure(data = nil)
|
98
|
-
FService
|
251
|
+
FService.deprecate!(
|
252
|
+
name: "#{self.class}##{__method__}",
|
253
|
+
alternative: '#Failure'
|
254
|
+
)
|
255
|
+
|
256
|
+
Result::Failure.new(data)
|
99
257
|
end
|
100
258
|
|
101
|
-
# Return either {
|
259
|
+
# Return either {Result::Failure Success} or {Result::Failure Failure}
|
102
260
|
# given the condition.
|
103
261
|
#
|
104
262
|
# @example
|
@@ -120,8 +278,14 @@ module FService
|
|
120
278
|
# end
|
121
279
|
# end
|
122
280
|
#
|
123
|
-
# @
|
281
|
+
# @deprecated Use {#Check} instead.
|
282
|
+
# @return [Result::Success, Result::Failure]
|
124
283
|
def result(condition, data = nil)
|
284
|
+
FService.deprecate!(
|
285
|
+
name: "#{self.class}##{__method__}",
|
286
|
+
alternative: '#Check'
|
287
|
+
)
|
288
|
+
|
125
289
|
condition ? success(data) : failure(data)
|
126
290
|
end
|
127
291
|
end
|
@@ -36,14 +36,93 @@ module FService
|
|
36
36
|
#
|
37
37
|
# @param success [#call] a lambda (or anything that responds to #call) to run on success
|
38
38
|
# @param failure [#call] a lambda (or anything that responds to #call) to run on failure
|
39
|
+
# @deprecated Use {#on_success} and/or {#on_failure} instead.
|
39
40
|
# @api public
|
40
41
|
def on(success:, failure:)
|
42
|
+
FService.deprecate!(
|
43
|
+
name: "#{self.class}##{__method__}",
|
44
|
+
alternative: '#on_success and/or #on_failure'
|
45
|
+
)
|
46
|
+
|
41
47
|
if successful?
|
42
48
|
success.call(value)
|
43
49
|
else
|
44
50
|
failure.call(error)
|
45
51
|
end
|
46
52
|
end
|
53
|
+
|
54
|
+
# This hook runs if the result is successful.
|
55
|
+
# Can receive one or more types to be checked before running the given block.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# class UsersController < BaseController
|
59
|
+
# def update
|
60
|
+
# User::Update.(user: user)
|
61
|
+
# .on_success(:type, :type2) { return json_success({ status: :ok }) } # run only if type matches
|
62
|
+
# .on_success { |value| return json_success(value) }
|
63
|
+
# .on_failure { |error| return json_error(error) } # this won't run
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# private
|
67
|
+
#
|
68
|
+
# def user
|
69
|
+
# @user ||= User.find_by!(slug: params[:slug])
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @yieldparam value value of the failure object
|
74
|
+
# @yieldparam type type of the failure object
|
75
|
+
# @return [Success, Failure] the original Result object
|
76
|
+
# @api public
|
77
|
+
def on_success(*target_types)
|
78
|
+
yield(*to_ary) if successful? && expected_type?(target_types)
|
79
|
+
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# This hook runs if the result is failed.
|
84
|
+
# Can receive one or more types to be checked before running the given block.
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# class UsersController < BaseController
|
88
|
+
# def update
|
89
|
+
# User::Update.(user: user)
|
90
|
+
# .on_success { |value| return json_success(value) } # this won't run
|
91
|
+
# .on_failure(:type, :type2) { |error| return json_error(error) } # runs only if type matches
|
92
|
+
# .on_failure { |error| return json_error(error) }
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# private
|
96
|
+
#
|
97
|
+
# def user
|
98
|
+
# @user ||= User.find_by!(slug: params[:slug])
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# @yieldparam value value of the failure object
|
103
|
+
# @yieldparam type type of the failure object
|
104
|
+
# @return [Success, Failure] the original Result object
|
105
|
+
# @api public
|
106
|
+
def on_failure(*target_types)
|
107
|
+
yield(*to_ary) if failed? && expected_type?(target_types)
|
108
|
+
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
# Splits the result object into its components.
|
113
|
+
#
|
114
|
+
# @return [Array] value and type of the result object
|
115
|
+
def to_ary
|
116
|
+
data = successful? ? value : error
|
117
|
+
|
118
|
+
[data, type]
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def expected_type?(target_types)
|
124
|
+
target_types.include?(type) || target_types.empty?
|
125
|
+
end
|
47
126
|
end
|
48
127
|
end
|
49
128
|
end
|
@@ -8,17 +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] type
|
14
|
+
# @return [Object] the provided type for the result. Defaults to nil.
|
11
15
|
# @api public
|
12
16
|
class Failure < Result::Base
|
13
|
-
|
14
|
-
attr_reader :error
|
17
|
+
attr_reader :error, :type
|
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, type = nil)
|
21
24
|
@error = error
|
25
|
+
@type = type
|
22
26
|
freeze
|
23
27
|
end
|
24
28
|
|
@@ -55,6 +59,33 @@ module FService
|
|
55
59
|
raise Result::Error, 'Failure objects do not have value'
|
56
60
|
end
|
57
61
|
|
62
|
+
# Returns the current error to the given block.
|
63
|
+
# Use this to chain multiple service calls (since all services return Results).
|
64
|
+
# It works just like the `.and_then` method, but only runs if the result is a Failure.
|
65
|
+
#
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# class UpdateUserOnExternalService
|
69
|
+
# attribute :user_params
|
70
|
+
#
|
71
|
+
# def run
|
72
|
+
# check_api_status
|
73
|
+
# .and_then { update_user }
|
74
|
+
# .or_else { create_update_worker }
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# private
|
78
|
+
# # some code
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# @yieldparam error pass {#error} to a block
|
82
|
+
# @yieldparam type pass {#type} to a block
|
83
|
+
def catch
|
84
|
+
yield(*to_ary)
|
85
|
+
end
|
86
|
+
|
87
|
+
alias or_else catch
|
88
|
+
|
58
89
|
# Returns itself to the given block.
|
59
90
|
# Use this to chain multiple service calls (since all services return Results).
|
60
91
|
# It will short circuit your service call chain.
|
@@ -64,8 +95,8 @@ module FService
|
|
64
95
|
# class UsersController < BaseController
|
65
96
|
# def create
|
66
97
|
# result = User::Create.(user_params) # if this fails the following calls won't run
|
67
|
-
#
|
68
|
-
#
|
98
|
+
# .and_then { |user| User::SendWelcomeEmail.(user: user) }
|
99
|
+
# .and_then { |user| User::Login.(user: user) }
|
69
100
|
#
|
70
101
|
# if result.successful?
|
71
102
|
# json_success(result.value)
|
@@ -76,9 +107,10 @@ module FService
|
|
76
107
|
# end
|
77
108
|
#
|
78
109
|
# @return [self]
|
79
|
-
def
|
110
|
+
def and_then
|
80
111
|
self
|
81
112
|
end
|
113
|
+
alias then and_then
|
82
114
|
|
83
115
|
# Outputs a string representation of the object
|
84
116
|
#
|
@@ -7,17 +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] type
|
13
|
+
# @return [Object] the provided type for the result. Defaults to nil.
|
10
14
|
# @api public
|
11
15
|
class Success < Result::Base
|
12
|
-
|
13
|
-
attr_reader :value
|
16
|
+
attr_reader :value, :type
|
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, type = nil)
|
20
23
|
@value = value
|
24
|
+
@type = type
|
21
25
|
freeze
|
22
26
|
end
|
23
27
|
|
@@ -63,8 +67,8 @@ module FService
|
|
63
67
|
# class UsersController < BaseController
|
64
68
|
# def create
|
65
69
|
# result = User::Create.(user_params)
|
66
|
-
#
|
67
|
-
#
|
70
|
+
# .and_then { |user| User::SendWelcomeEmail.(user: user) }
|
71
|
+
# .and_then { |user| User::Login.(user: user) }
|
68
72
|
#
|
69
73
|
# if result.successful?
|
70
74
|
# json_success(result.value)
|
@@ -74,10 +78,38 @@ module FService
|
|
74
78
|
# end
|
75
79
|
# end
|
76
80
|
#
|
77
|
-
# @yieldparam
|
78
|
-
|
79
|
-
|
81
|
+
# @yieldparam value pass {#value} to a block
|
82
|
+
# @yieldparam type pass {#type} to a block
|
83
|
+
def and_then
|
84
|
+
yield(*to_ary)
|
80
85
|
end
|
86
|
+
alias then and_then
|
87
|
+
|
88
|
+
# Returns itself to the given block.
|
89
|
+
# Use this to chain multiple actions or service calls (only valid when they return a Result).
|
90
|
+
# It works just like the `.and_then` method, but only runs if service is a Failure.
|
91
|
+
#
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# class UpdateUserOnExternalService
|
95
|
+
# attribute :user_params
|
96
|
+
#
|
97
|
+
# def run
|
98
|
+
# check_api_status
|
99
|
+
# .and_then { update_user }
|
100
|
+
# .or_else { create_update_worker }
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# private
|
104
|
+
# # some code
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# @return [self]
|
108
|
+
def catch
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
alias or_else catch
|
81
113
|
|
82
114
|
# Outputs a string representation of the object
|
83
115
|
#
|
data/lib/f_service/version.rb
CHANGED
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matheus Richard
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -53,6 +53,7 @@ files:
|
|
53
53
|
- lib/f_service/result/failure.rb
|
54
54
|
- lib/f_service/result/success.rb
|
55
55
|
- lib/f_service/version.rb
|
56
|
+
- logo.png
|
56
57
|
homepage: https://github.com/Fretadao/f_service
|
57
58
|
licenses:
|
58
59
|
- MIT
|
@@ -60,6 +61,7 @@ metadata:
|
|
60
61
|
homepage_uri: https://github.com/Fretadao/f_service
|
61
62
|
source_code_uri: https://github.com/Fretadao/f_service
|
62
63
|
documentation_uri: https://www.rubydoc.info/gems/f_service
|
64
|
+
changelog_uri: https://github.com/Fretadao/f_service/blob/master/CHANGELOG.md
|
63
65
|
post_install_message:
|
64
66
|
rdoc_options: []
|
65
67
|
require_paths:
|
@@ -75,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
77
|
- !ruby/object:Gem::Version
|
76
78
|
version: '0'
|
77
79
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
80
|
+
rubygems_version: 3.1.4
|
79
81
|
signing_key:
|
80
82
|
specification_version: 4
|
81
83
|
summary: A small, monad-based service class
|