f_service 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb3dfcc7d2f09fe4bd1cabca95a9520b06badf64e1d8fd7a238e0b904cdbbfe9
4
- data.tar.gz: 4f71a8ecfb48c4b928b7240d63a63469a36f46635b081f221d064b0603d73be7
3
+ metadata.gz: 6cb29d87cf5c970b2b185ed2c3da41020f40122e2d0857af068302fb32e55400
4
+ data.tar.gz: 27a89660bb6d1e65d537f5d6617cc8175f600ab71efca79f65875a1b6adf0c97
5
5
  SHA512:
6
- metadata.gz: 1e3378dc3e210fd41aa994270792eb5a2dbf003f351964d81e08c4505404bc90b765fd33c4d49d39893a83d64dded6d1a5efa72bc4699ec23d7cd6273535e22d
7
- data.tar.gz: 175330f3f2fbccdcc9b74f93b69ffa0a4002699139b63d19208da97327722346b154f83d46bde3f71bf23016ef23065727cb5c876fb45ed076375652da70e5b7
6
+ metadata.gz: 2b444953aee676163a2b076d814b3cc801b0808a30660c4edd2f19172754d58c41a7fcb3129a52cf95c1a0fdb8546419a55a8101b6882459673cd647d520f660
7
+ data.tar.gz: 0e2f10e1ad6cf4ba7ba2375b2302661429721b7bf4023afcb1559909205eb7e8f013efbb3da07c4b7926b2e967592c31a6c930ca6d6e69b195bba97342407b69
@@ -0,0 +1,8 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "09:00"
8
+ open-pull-requests-limit: 10
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- ruby: [2.5, 2.6, 2.7, '3.0']
14
+ ruby: [2.6, 2.7, 3.0, 3.1]
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v2
data/.rubocop.yml CHANGED
@@ -17,3 +17,17 @@ Style/DocumentationMethod:
17
17
  Naming/MethodName:
18
18
  Exclude:
19
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
@@ -10,9 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
  <!-- ### Removed -->
11
11
  ---
12
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
+
13
25
  ## 0.2.0
14
26
  ### Added
15
- - Add `and_catch` as alias for `then` (partially fix [#23](https://github.com/Fretadao/f_service/issues/23)).
27
+ - Add `and_then` as alias for `then` (partially fix [#23](https://github.com/Fretadao/f_service/issues/23)).
16
28
  - Add `catch` to `Failure` and `Success`. It acts as an inverted `then` and has a `or_else` alias.
17
29
  - Add support to custom `data` property to be passed when calling `Base#Check`.
18
30
  - Add support to multiple type checks on `Result#on_success` and `Result#on_failure` hooks.
data/Gemfile CHANGED
@@ -6,6 +6,8 @@ source 'https://rubygems.org'
6
6
  gemspec
7
7
 
8
8
  group :development, :test do
9
+ gem 'pry'
10
+ gem 'pry-nav'
9
11
  gem 'rake', '~> 13.0.0'
10
12
  gem 'rubocop', '~> 0.82.0', require: false
11
13
  gem 'rubocop-rspec', require: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- f_service (0.2.0)
4
+ f_service (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -9,24 +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
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
- mini_portile2 (2.5.0)
18
- nokogiri (1.11.1)
19
- mini_portile2 (~> 2.5.0)
18
+ method_source (1.0.0)
19
+ mini_portile2 (2.8.1)
20
+ nokogiri (1.14.3)
21
+ mini_portile2 (~> 2.8.0)
20
22
  racc (~> 1.4)
21
23
  parallel (1.19.1)
22
24
  parser (2.7.1.1)
23
25
  ast (~> 2.4.0)
24
- racc (1.5.2)
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)
25
32
  rainbow (3.0.0)
26
33
  rake (13.0.1)
27
34
  reverse_markdown (1.4.0)
28
35
  nokogiri
29
- rexml (3.2.4)
36
+ rexml (3.2.5)
30
37
  rspec (3.9.0)
31
38
  rspec-core (~> 3.9.0)
32
39
  rspec-expectations (~> 3.9.0)
@@ -82,6 +89,8 @@ PLATFORMS
82
89
  DEPENDENCIES
83
90
  bundler (~> 2.0)
84
91
  f_service!
92
+ pry
93
+ pry-nav
85
94
  rake (~> 13.0.0)
86
95
  rspec (~> 3.0)
87
96
  rubocop (~> 0.82.0)
@@ -91,4 +100,4 @@ DEPENDENCIES
91
100
  yard
92
101
 
93
102
  BUNDLED WITH
94
- 2.1.4
103
+ 2.2.32
data/README.md CHANGED
@@ -58,17 +58,17 @@ end
58
58
 
59
59
  The next step is writing the `#run` method, which is where the work should be done.
60
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.
61
+ You can optionally specify a list of types which represents that result and a value for your result.
62
62
 
63
63
  ```ruby
64
64
  class User::Create < FService::Base
65
65
  # ...
66
66
  def run
67
- return Failure(:no_name) if @name.nil?
67
+ return Failure(:no_name, :invalid_attribute) if @name.nil?
68
68
 
69
69
  user = UserRepository.create(name: @name)
70
- if user.valid?
71
- Success(:created, data: user)
70
+ if user.save
71
+ Success(:success, :created, data: user)
72
72
  else
73
73
  Failure(:creation_failed, data: user.errors)
74
74
  end
@@ -128,6 +128,29 @@ class UsersController < BaseController
128
128
  end
129
129
  ```
130
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
+
131
154
  > You can ignore any of the callbacks, if you want to.
132
155
 
133
156
  Going further, you can match the Result type, in case you want to handle them differently:
@@ -166,6 +189,12 @@ class UsersController < BaseController
166
189
  end
167
190
  ```
168
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.
197
+
169
198
  ### Chaining services
170
199
 
171
200
  Since all services return Results, you can chain service calls making a data pipeline.
@@ -206,10 +235,10 @@ You can use `Check` to converts a boolean to a Result, truthy values map to `Suc
206
235
 
207
236
  ```ruby
208
237
  Check(:math_works) { 1 < 2 }
209
- # => #<Success @value=true, @type=:math_works>
238
+ # => #<Success @value=true, @types=[:math_works]>
210
239
 
211
240
  Check(:math_works) { 1 > 2 }
212
- # => #<Failure @error=false, @type=:math_works>
241
+ # => #<Failure @error=false, @types=[:math_works]>
213
242
  ```
214
243
 
215
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
@@ -228,10 +257,59 @@ class IHateEvenNumbers < FService::Base
228
257
  end
229
258
 
230
259
  IHateEvenNumbers.call
231
- # => #<Success @value=9, @type=:rand_int>
260
+ # => #<Success @value=9, @types=[:rand_int]>
232
261
 
233
262
  IHateEvenNumbers.call
234
- # => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @type=:rand_int>
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"] })
235
313
  ```
236
314
 
237
315
  ## API Docs
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 = ['Matheus Richard']
11
- spec.email = ['matheusrichardt@gmail.com']
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
@@ -101,69 +101,70 @@ module FService
101
101
  def success(data = nil)
102
102
  FService.deprecate!(
103
103
  name: "#{self.class}##{__method__}",
104
- alternative: '#Success'
104
+ alternative: '#Success',
105
+ from: caller[0]
105
106
  )
106
107
 
107
108
  Result::Success.new(data)
108
109
  end
109
110
 
110
111
  # Returns a successful result.
111
- # You can optionally specify a type and a value for your result.
112
+ # You can optionally specify a list of types and a value for your result.
112
113
  # You'll probably want to return this inside {#run}.
113
114
  #
114
115
  #
115
116
  # @example
116
117
  # def run
117
118
  # Success()
118
- # # => #<Success @value=nil, @type=nil>
119
+ # # => #<Success @value=nil, @types=[]>
119
120
  #
120
121
  # Success(:ok)
121
- # # => #<Success @value=nil, @type=:ok>
122
+ # # => #<Success @value=nil, @types=[:ok]>
122
123
  #
123
124
  # Success(data: 10)
124
- # # => #<Success @value=10, @type=nil>
125
+ # # => #<Success @value=10, @types=[]>
125
126
  #
126
127
  # Success(:ok, data: 10)
127
- # # => #<Success @value=10, @type=:ok>
128
+ # # => #<Success @value=10, @types=[:ok]>
128
129
  # end
129
130
  #
130
- # @param type the Result type
131
+ # @param types the Result types
131
132
  # @param data the result value
132
133
  # @return [Result::Success] a successful result
133
- def Success(type = nil, data: nil)
134
- Result::Success.new(data, type)
134
+ def Success(*types, data: nil)
135
+ Result::Success.new(data, types)
135
136
  end
136
137
 
137
138
  # Returns a failed result.
138
- # You can optionally specify a type and a value for your result.
139
+ # You can optionally specify types and a value for your result.
139
140
  # You'll probably want to return this inside {#run}.
140
141
  #
141
142
  #
142
143
  # @example
143
144
  # def run
144
145
  # Failure()
145
- # # => #<Failure @error=nil, @type=nil>
146
+ # # => #<Failure @error=nil, @types=[]>
146
147
  #
147
148
  # Failure(:not_a_number)
148
- # # => #<Failure @error=nil, @type=:not_a_number>
149
+ # # => #<Failure @error=nil, @types=[:not_a_number]>
149
150
  #
150
151
  # Failure(data: "10")
151
- # # => #<Failure @error="10", @type=nil>
152
+ # # => #<Failure @error="10", @types=[]>
152
153
  #
153
154
  # Failure(:not_a_number, data: "10")
154
- # # => #<Failure @error="10", @type=:not_a_number>
155
+ # # => #<Failure @error="10", @types=[:not_a_number]>
155
156
  # end
156
157
  #
157
- # @param type the Result type
158
+ # @param types the Result types
158
159
  # @param data the result value
159
160
  # @return [Result::Failure] a failed result
160
- def Failure(type = nil, data: nil)
161
- Result::Failure.new(data, type)
161
+ def Failure(*types, data: nil)
162
+ Result::Failure.new(data, types)
162
163
  end
163
164
 
164
165
  # Converts a boolean to a Result.
165
166
  # Truthy values map to Success, and falsey values map to Failures.
166
- # You can optionally provide a type for the result.
167
+ # You can optionally provide a types for the result.
167
168
  # The result value defaults as the evaluated value of the given block.
168
169
  # If you want another value you can pass it through the `data:` argument.
169
170
  #
@@ -171,34 +172,34 @@ module FService
171
172
  # class CheckMathWorks < FService::Base
172
173
  # def run
173
174
  # Check(:math_works) { 1 < 2 }
174
- # # => #<Success @value=true, @type=:math_works>
175
+ # # => #<Success @value=true, @types=[:math_works]>
175
176
  #
176
177
  # Check(:math_works) { 1 > 2 }
177
- # # => #<Failure @error=false, @type=:math_works>
178
+ # # => #<Failure @error=false, @types=[:math_works]>
178
179
  #
179
180
  # Check(:math_works, data: 1 + 2) { 1 > 2 }
180
- # # => #<Failure @type=:math_works, @error=3>
181
+ # # => #<Failure @types=:math_works, @error=3>
181
182
  # end
182
183
  #
183
184
  # Check(:math_works, data: 1 + 2) { 1 < 2 }
184
- # # => #<Success @type=:math_works, @value=3>
185
+ # # => #<Success @types=[:math_works], @value=3>
185
186
  # end
186
187
  # end
187
188
  #
188
- # @param type the Result type
189
+ # @param types the Result types
189
190
  # @return [Result::Success, Result::Failure] a Result from the boolean expression
190
- def Check(type = nil, data: nil)
191
+ def Check(*types, data: nil)
191
192
  res = yield
192
193
 
193
194
  final_data = data || res
194
195
 
195
- res ? Success(type, data: final_data) : Failure(type, data: final_data)
196
+ res ? Success(*types, data: final_data) : Failure(*types, data: final_data)
196
197
  end
197
198
 
198
199
  # If the given block raises an exception, it wraps it in a Failure.
199
200
  # Otherwise, maps the block value in a Success object.
200
201
  # You can specify which exceptions to watch for.
201
- # It's possible to provide a type for the result too.
202
+ # It's possible to provide a types for the result too.
202
203
  #
203
204
  # @example
204
205
  # class IHateEvenNumbers < FService::Base
@@ -213,20 +214,20 @@ module FService
213
214
  # end
214
215
  #
215
216
  # IHateEvenNumbers.call
216
- # # => #<Success @value=9, @type=:rand_int>
217
+ # # => #<Success @value=9, @types=[:rand_int]>
217
218
  #
218
219
  # IHateEvenNumbers.call
219
- # # => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @type=:rand_int>
220
+ # # => #<Failure @error=#<RuntimeError: Yuck! It's a 4>, @types=[:rand_int]>
220
221
  #
221
- # @param type the Result type
222
+ # @param types the Result types
222
223
  # @param catch the exception list to catch
223
224
  # @return [Result::Success, Result::Failure] a result from the boolean expression
224
- def Try(type = nil, catch: StandardError)
225
+ def Try(*types, catch: StandardError)
225
226
  res = yield
226
227
 
227
- Success(type, data: res)
228
+ Success(*types, data: res)
228
229
  rescue *catch => e
229
- Failure(type, data: e)
230
+ Failure(*types, data: e)
230
231
  end
231
232
 
232
233
  # Returns a failed operation.
@@ -250,7 +251,8 @@ module FService
250
251
  def failure(data = nil)
251
252
  FService.deprecate!(
252
253
  name: "#{self.class}##{__method__}",
253
- alternative: '#Failure'
254
+ alternative: '#Failure',
255
+ from: caller[0]
254
256
  )
255
257
 
256
258
  Result::Failure.new(data)
@@ -283,7 +285,8 @@ module FService
283
285
  def result(condition, data = nil)
284
286
  FService.deprecate!(
285
287
  name: "#{self.class}##{__method__}",
286
- alternative: '#Check'
288
+ alternative: '#Check',
289
+ from: caller[0]
287
290
  )
288
291
 
289
292
  condition ? success(data) : failure(data)
@@ -7,24 +7,38 @@ module FService
7
7
  #
8
8
  # @abstract
9
9
  class Base
10
- %i[initialize then successful? failed? value value! error].each do |method_name|
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
- # "Pattern matching"-like method for results.
17
- # It will run the success path if Result is a Success.
18
- # Otherwise, it will run the failure path.
19
- #
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.
20
34
  #
21
35
  # @example
22
36
  # class UsersController < BaseController
23
37
  # def update
24
- # User::Update.(user: user).on(
25
- # success: ->(value) { return json_success(value) },
26
- # failure: ->(error) { return json_error(error) }
27
- # )
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
28
42
  # end
29
43
  #
30
44
  # private
@@ -34,33 +48,13 @@ module FService
34
48
  # end
35
49
  # end
36
50
  #
37
- # @param success [#call] a lambda (or anything that responds to #call) to run on success
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.
40
- # @api public
41
- def on(success:, failure:)
42
- FService.deprecate!(
43
- name: "#{self.class}##{__method__}",
44
- alternative: '#on_success and/or #on_failure'
45
- )
46
-
47
- if successful?
48
- success.call(value)
49
- else
50
- failure.call(error)
51
- end
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
51
  # @example
58
52
  # class UsersController < BaseController
59
53
  # def update
60
54
  # User::Update.(user: user)
61
55
  # .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
56
+ # .on_success(unhandled: true) { |value| return json_success(value) }
57
+ # .on_failure(unhandled: true) { |error| return json_error(error) } # this won't run
64
58
  # end
65
59
  #
66
60
  # private
@@ -74,8 +68,13 @@ module FService
74
68
  # @yieldparam type type of the failure object
75
69
  # @return [Success, Failure] the original Result object
76
70
  # @api public
77
- def on_success(*target_types)
78
- yield(*to_ary) if successful? && expected_type?(target_types)
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
77
+ end
79
78
 
80
79
  self
81
80
  end
@@ -99,12 +98,33 @@ module FService
99
98
  # end
100
99
  # end
101
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
+ #
102
117
  # @yieldparam value value of the failure object
103
118
  # @yieldparam type type of the failure object
104
119
  # @return [Success, Failure] the original Result object
105
120
  # @api public
106
- def on_failure(*target_types)
107
- yield(*to_ary) if failed? && expected_type?(target_types)
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
108
128
 
109
129
  self
110
130
  end
@@ -115,13 +135,25 @@ module FService
115
135
  def to_ary
116
136
  data = successful? ? value : error
117
137
 
118
- [data, type]
138
+ [data, @matching_types.first]
119
139
  end
120
140
 
121
141
  private
122
142
 
123
- def expected_type?(target_types)
124
- target_types.include?(type) || target_types.empty?
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
125
157
  end
126
158
  end
127
159
  end
@@ -10,20 +10,19 @@ module FService
10
10
  #
11
11
  # @!attribute [r] error
12
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.
13
+ # @!attribute [r] types
14
+ # @return [Object] the provided types for the result. Defaults to nil.
15
15
  # @api public
16
16
  class Failure < Result::Base
17
- attr_reader :error, :type
17
+ attr_reader :error
18
18
 
19
19
  # Creates a failed operation.
20
20
  # You usually shouldn't call this directly. See {FService::Base#Failure}.
21
21
  #
22
22
  # @param error [Object] failure value.
23
- def initialize(error, type = nil)
23
+ def initialize(error, types = [])
24
+ super(types)
24
25
  @error = error
25
- @type = type
26
- freeze
27
26
  end
28
27
 
29
28
  # Returns false.
@@ -110,7 +109,12 @@ module FService
110
109
  def and_then
111
110
  self
112
111
  end
113
- alias then and_then
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
114
118
 
115
119
  # Outputs a string representation of the object
116
120
  #
@@ -9,20 +9,19 @@ module FService
9
9
  #
10
10
  # @!attribute [r] value
11
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.
12
+ # @!attribute [r] types
13
+ # @return [Object] the provided types for the result. Defaults to nil.
14
14
  # @api public
15
15
  class Success < Result::Base
16
- attr_reader :value, :type
16
+ attr_reader :value
17
17
 
18
18
  # Creates a successful operation.
19
19
  # You usually shouldn't call this directly. See {FService::Base#Success}.
20
20
  #
21
21
  # @param value [Object] success value.
22
- def initialize(value, type = nil)
22
+ def initialize(value, types = [])
23
+ super(types)
23
24
  @value = value
24
- @type = type
25
- freeze
26
25
  end
27
26
 
28
27
  # Returns true.
@@ -79,11 +78,17 @@ module FService
79
78
  # end
80
79
  #
81
80
  # @yieldparam value pass {#value} to a block
82
- # @yieldparam type pass {#type} to a block
81
+ # @yieldparam types pass {#types} to a block
83
82
  def and_then
84
83
  yield(*to_ary)
85
84
  end
86
- alias then and_then
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
87
92
 
88
93
  # Returns itself to the given block.
89
94
  # Use this to chain multiple actions or service calls (only valid when they return a Result).
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/result'
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'matchers/result'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'support/helpers'
4
+ require_relative 'support/matchers'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rspec/support'
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FService
4
4
  # Current version of the gem
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
data/lib/f_service.rb CHANGED
@@ -9,9 +9,24 @@ module FService
9
9
  # Marks a method as deprecated
10
10
  #
11
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.'
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")
16
31
  end
17
32
  end
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - Matheus Richard
7
+ - Fretadao Tech Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-08 00:00:00.000000000 Z
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
- - matheusrichardt@gmail.com
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,6 +53,12 @@ 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
56
63
  - logo.png
57
64
  homepage: https://github.com/Fretadao/f_service
@@ -77,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
84
  - !ruby/object:Gem::Version
78
85
  version: '0'
79
86
  requirements: []
80
- rubygems_version: 3.1.4
87
+ rubygems_version: 3.3.5
81
88
  signing_key:
82
89
  specification_version: 4
83
90
  summary: A small, monad-based service class