u-service 0.14.0 → 1.0.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/README.md +395 -105
- data/lib/micro/service.rb +3 -1
- data/lib/micro/service/base.rb +25 -13
- data/lib/micro/service/error.rb +35 -0
- data/lib/micro/service/pipeline.rb +25 -67
- data/lib/micro/service/pipeline/reducer.rb +89 -0
- data/lib/micro/service/result.rb +7 -16
- data/lib/micro/service/safe.rb +14 -0
- data/lib/micro/service/strict.rb +4 -0
- data/lib/micro/service/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d75c44a6cbf48201cc58937ce39118ddba4af411cccdb6c9c930e4bcac80b451
|
4
|
+
data.tar.gz: 343e21ab69a17bb3cc392bb4ae9ec16ae55e0af73313ccbf376f4381371aed81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66447860551389fba8b39affaee2dbfc60734143227b3dc932932ffddf98b427d97fd596b03eeb22039a854f8c73b93bc2753cd94eba2c8a38f38aac4b94c779
|
7
|
+
data.tar.gz: b08c8959f292ae0e06602738410432aa1f4408abe2dcc9c6174d791e69a2b2171e14f51e6c7757b81ecd4389b88e5f5aa440c6ee0246a2ad0a06909a0497fd27
|
data/README.md
CHANGED
@@ -9,21 +9,30 @@
|
|
9
9
|
Create simple and powerful service objects.
|
10
10
|
|
11
11
|
The main goals of this project are:
|
12
|
-
1. The smallest possible learning curve.
|
12
|
+
1. The smallest possible learning curve (input **>>** process/transform **>>** output).
|
13
13
|
2. Referential transparency and data integrity.
|
14
|
-
3. No callbacks
|
14
|
+
3. No callbacks.
|
15
|
+
4. Compose a pipeline of service objects to represents complex business logic.
|
15
16
|
|
17
|
+
## Table of Contents
|
16
18
|
- [μ-service (Micro::Service)](#%ce%bc-service-microservice)
|
19
|
+
- [Table of Contents](#table-of-contents)
|
17
20
|
- [Required Ruby version](#required-ruby-version)
|
18
21
|
- [Installation](#installation)
|
19
22
|
- [Usage](#usage)
|
20
|
-
- [How to
|
21
|
-
- [
|
23
|
+
- [How to define a Service Object?](#how-to-define-a-service-object)
|
24
|
+
- [What is a `Micro::Service::Result`?](#what-is-a-microserviceresult)
|
25
|
+
- [What are the default types of a `Micro::Service::Result`?](#what-are-the-default-types-of-a-microserviceresult)
|
26
|
+
- [How to define custom result types?](#how-to-define-custom-result-types)
|
27
|
+
- [Is it possible to define a custom result type without a block?](#is-it-possible-to-define-a-custom-result-type-without-a-block)
|
28
|
+
- [How to use the result hooks?](#how-to-use-the-result-hooks)
|
29
|
+
- [What happens if a hook is declared multiple times?](#what-happens-if-a-hook-is-declared-multiple-times)
|
22
30
|
- [How to create a pipeline of Service Objects?](#how-to-create-a-pipeline-of-service-objects)
|
31
|
+
- [Is it possible to compose pipelines with other pipelines?](#is-it-possible-to-compose-pipelines-with-other-pipelines)
|
23
32
|
- [What is a strict Service Object?](#what-is-a-strict-service-object)
|
33
|
+
- [Is there some feature to auto handle exceptions inside of services/pipelines?](#is-there-some-feature-to-auto-handle-exceptions-inside-of-servicespipelines)
|
24
34
|
- [How to validate Service Object attributes?](#how-to-validate-service-object-attributes)
|
25
|
-
- [
|
26
|
-
- [Examples](#examples)
|
35
|
+
- [Examples](#examples)
|
27
36
|
- [Comparisons](#comparisons)
|
28
37
|
- [Benchmarks](#benchmarks)
|
29
38
|
- [Development](#development)
|
@@ -53,55 +62,196 @@ Or install it yourself as:
|
|
53
62
|
|
54
63
|
## Usage
|
55
64
|
|
56
|
-
### How to
|
65
|
+
### How to define a Service Object?
|
57
66
|
|
58
67
|
```ruby
|
59
68
|
class Multiply < Micro::Service::Base
|
69
|
+
# 1. Define its inputs as attributes
|
60
70
|
attributes :a, :b
|
61
71
|
|
72
|
+
# 2. Define the method `call!` with its business logic
|
62
73
|
def call!
|
74
|
+
|
75
|
+
# 3. Return the calling result using the `Success()` and `Failure()` methods
|
63
76
|
if a.is_a?(Numeric) && b.is_a?(Numeric)
|
64
77
|
Success(a * b)
|
65
78
|
else
|
66
|
-
Failure
|
79
|
+
Failure { '`a` and `b` attributes must be numeric' }
|
67
80
|
end
|
68
81
|
end
|
69
82
|
end
|
70
83
|
|
71
|
-
|
72
|
-
# Calling a
|
73
|
-
|
84
|
+
#================================#
|
85
|
+
# Calling a Service Object class #
|
86
|
+
#================================#
|
87
|
+
|
88
|
+
# Success result
|
74
89
|
|
75
90
|
result = Multiply.call(a: 2, b: 2)
|
76
91
|
|
77
|
-
|
78
|
-
|
92
|
+
result.success? # true
|
93
|
+
result.value # 4
|
94
|
+
|
95
|
+
# Failure result
|
96
|
+
|
97
|
+
bad_result = Multiply.call(a: 2, b: '2')
|
98
|
+
|
99
|
+
bad_result.failure? # true
|
100
|
+
bad_result.value # "`a` and `b` attributes must be numeric"
|
101
|
+
|
102
|
+
#-----------------------------------#
|
103
|
+
# Calling a Service Object instance #
|
104
|
+
#-----------------------------------#
|
105
|
+
|
106
|
+
result = Multiply.new(a: 2, b: 3).call
|
107
|
+
|
108
|
+
result.value # 6
|
79
109
|
|
80
110
|
# Note:
|
81
|
-
#
|
111
|
+
# ----
|
112
|
+
# The result of a Micro::Service::Base.call
|
82
113
|
# is an instance of Micro::Service::Result
|
114
|
+
```
|
83
115
|
|
84
|
-
|
85
|
-
# Calling a service instance #
|
86
|
-
#----------------------------#
|
116
|
+
[⬆️ Back to Top](#table-of-contents)
|
87
117
|
|
88
|
-
|
118
|
+
### What is a `Micro::Service::Result`?
|
89
119
|
|
90
|
-
|
91
|
-
|
120
|
+
A `Micro::Service::Result` carries the output data of some Service Object. These are their main methods:
|
121
|
+
- `#success?` returns true if is a successful result.
|
122
|
+
- `#failure?` returns true if is an unsuccessful result.
|
123
|
+
- `#value` the result value itself.
|
124
|
+
- `#type` a Symbol which gives meaning for the result, this is useful to declare different types of failures or success.
|
125
|
+
- `#on_success` or `#on_failure` are hook methods which help you define the flow of your application.
|
126
|
+
- `#service` if the result is a failure the service will be accessible through this method. This feature is handy to use with pipeline failures (this topic will be covered ahead).
|
127
|
+
|
128
|
+
[⬆️ Back to Top](#table-of-contents)
|
129
|
+
|
130
|
+
#### What are the default types of a `Micro::Service::Result`?
|
131
|
+
|
132
|
+
Every result has a type and these are the default values: :ok when success, and :error/:exception when failures.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class Divide < Micro::Service::Base
|
136
|
+
attributes :a, :b
|
137
|
+
|
138
|
+
def call!
|
139
|
+
invalid_attributes.empty? ? Success(a / b) : Failure(invalid_attributes)
|
140
|
+
rescue => e
|
141
|
+
Failure(e)
|
142
|
+
end
|
143
|
+
|
144
|
+
private def invalid_attributes
|
145
|
+
attributes.select { |_key, value| !value.is_a?(Numeric) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Success result
|
150
|
+
|
151
|
+
result = Divide.call(a: 2, b: 2)
|
152
|
+
|
153
|
+
result.type # :ok
|
154
|
+
result.value # 1
|
155
|
+
result.success? # true
|
156
|
+
result.service # raises `Micro::Service::Error::InvalidAccessToTheServiceObject: only a failure result can access its service object`
|
157
|
+
|
158
|
+
# Failure result - type == :error
|
159
|
+
|
160
|
+
bad_result = Divide.call(a: 2, b: '2')
|
161
|
+
|
162
|
+
bad_result.type # :error
|
163
|
+
bad_result.value # {"b"=>"2"}
|
164
|
+
bad_result.failure? # true
|
165
|
+
bad_result.service # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=#<Micro::Service::Result:0x0000 @service=#<Divide:0x0000 ...>, @type=:error, @value={"b"=>"2"}, @success=false>>
|
166
|
+
|
167
|
+
# Failure result - type == :exception
|
168
|
+
|
169
|
+
err_result = Divide.call(a: 2, b: 0)
|
170
|
+
|
171
|
+
err_result.type # :exception
|
172
|
+
err_result.value # <ZeroDivisionError: divided by 0>
|
173
|
+
err_result.failure? # true
|
174
|
+
err_result.service # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>0}, @a=2, @b=0, @__result=#<Micro::Service::Result:0x0000 @service=#<Divide:0x0000 ...>, @type=:exception, @value=#<ZeroDivisionError: divided by 0>, @success=false>>
|
175
|
+
|
176
|
+
# Note:
|
177
|
+
# ----
|
178
|
+
# Any Exception instance which is wrapped by
|
179
|
+
# the Failure() method will receive `:exception` instead of the `:error` type.
|
180
|
+
```
|
181
|
+
|
182
|
+
[⬆️ Back to Top](#table-of-contents)
|
183
|
+
|
184
|
+
#### How to define custom result types?
|
185
|
+
|
186
|
+
Answer: Use a symbol as the argument of Success() and Failure() methods and declare a block to set the value.
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class Multiply < Micro::Service::Base
|
190
|
+
attributes :a, :b
|
191
|
+
|
192
|
+
def call!
|
193
|
+
return Success(a * b) if a.is_a?(Numeric) && b.is_a?(Numeric)
|
194
|
+
|
195
|
+
Failure(:invalid_data) do
|
196
|
+
attributes.reject { |_, input| input.is_a?(Numeric) }
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Success result
|
202
|
+
|
203
|
+
result = Multiply.call(a: 3, b: 2)
|
204
|
+
|
205
|
+
result.type # :ok
|
206
|
+
result.value # 6
|
207
|
+
result.success? # true
|
208
|
+
|
209
|
+
# Failure result
|
210
|
+
|
211
|
+
bad_result = Multiply.call(a: 3, b: '2')
|
212
|
+
|
213
|
+
bad_result.type # :invalid_data
|
214
|
+
bad_result.value # {"b"=>"2"}
|
215
|
+
bad_result.failure? # true
|
216
|
+
```
|
217
|
+
|
218
|
+
[⬆️ Back to Top](#table-of-contents)
|
219
|
+
|
220
|
+
##### Is it possible to define a custom result type without a block?
|
221
|
+
|
222
|
+
Answer: Yes, it is. But only for failure results!
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class Multiply < Micro::Service::Base
|
226
|
+
attributes :a, :b
|
92
227
|
|
93
|
-
|
94
|
-
|
95
|
-
|
228
|
+
def call!
|
229
|
+
return Failure(:invalid_data) unless a.is_a?(Numeric) && b.is_a?(Numeric)
|
230
|
+
|
231
|
+
Success(a * b)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
result = Multiply.call(a: 2, b: '2')
|
96
236
|
|
97
|
-
result
|
237
|
+
result.failure? #true
|
238
|
+
result.value #:invalid_data
|
239
|
+
result.type #:invalid_data
|
240
|
+
result.service.attributes # {"a"=>2, "b"=>"2"}
|
98
241
|
|
99
|
-
|
100
|
-
|
101
|
-
|
242
|
+
# Note:
|
243
|
+
# ----
|
244
|
+
# This feature is handy to respond to some pipeline failure
|
245
|
+
# (this topic will be covered ahead).
|
102
246
|
```
|
103
247
|
|
104
|
-
|
248
|
+
[⬆️ Back to Top](#table-of-contents)
|
249
|
+
|
250
|
+
#### How to use the result hooks?
|
251
|
+
|
252
|
+
As mentioned earlier, the `Micro::Service::Result` has two methods to improve the flow control. They are: `#on_success`, `on_failure`.
|
253
|
+
|
254
|
+
The examples below show how to use them:
|
105
255
|
|
106
256
|
```ruby
|
107
257
|
class Double < Micro::Service::Base
|
@@ -125,8 +275,8 @@ Double
|
|
125
275
|
.on_failure(:invalid) { |msg| raise TypeError, msg }
|
126
276
|
.on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
|
127
277
|
|
128
|
-
# The output
|
129
|
-
#
|
278
|
+
# The output because is a success:
|
279
|
+
# 6
|
130
280
|
|
131
281
|
#=============================#
|
132
282
|
# Raising an error if failure #
|
@@ -135,13 +285,55 @@ Double
|
|
135
285
|
Double
|
136
286
|
.call(number: -1)
|
137
287
|
.on_success { |number| p number }
|
288
|
+
.on_failure { |_msg, service| puts "#{service.class.name} was the service responsible for the failure" }
|
138
289
|
.on_failure(:invalid) { |msg| raise TypeError, msg }
|
139
290
|
.on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
|
140
291
|
|
141
|
-
# The
|
142
|
-
#
|
292
|
+
# The outputs because is a failure:
|
293
|
+
# Double was the service responsible for the failure
|
294
|
+
# (throws the error)
|
295
|
+
# ArgumentError (the number must be greater than 0)
|
296
|
+
|
297
|
+
# Note:
|
298
|
+
# ----
|
299
|
+
# The service responsible for the failure will be accessible as the second hook argument
|
143
300
|
```
|
144
301
|
|
302
|
+
[⬆️ Back to Top](#table-of-contents)
|
303
|
+
|
304
|
+
##### What happens if a hook is declared multiple times?
|
305
|
+
|
306
|
+
Answer: The hook will be triggered if it matches the result type.
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
class Double < Micro::Service::Base
|
310
|
+
attributes :number
|
311
|
+
|
312
|
+
def call!
|
313
|
+
return Failure(:invalid) { 'the number must be a numeric value' } unless number.is_a?(Numeric)
|
314
|
+
|
315
|
+
Success(:computed) { number * 2 }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
result = Double.call(number: 3)
|
320
|
+
result.value # 6
|
321
|
+
result.value * 4 # 24
|
322
|
+
|
323
|
+
accum = 0
|
324
|
+
|
325
|
+
result.on_success { |number| accum += number }
|
326
|
+
.on_success { |number| accum += number }
|
327
|
+
.on_success(:computed) { |number| accum += number }
|
328
|
+
.on_success(:computed) { |number| accum += number }
|
329
|
+
|
330
|
+
accum # 24
|
331
|
+
|
332
|
+
result.value * 4 == accum # true
|
333
|
+
```
|
334
|
+
|
335
|
+
[⬆️ Back to Top](#table-of-contents)
|
336
|
+
|
145
337
|
### How to create a pipeline of Service Objects?
|
146
338
|
|
147
339
|
```ruby
|
@@ -222,11 +414,10 @@ SquareAllNumbers
|
|
222
414
|
.call(numbers: %w[1 1 2 2 3 4])
|
223
415
|
.on_success { |value| p value[:numbers] } # [1, 1, 4, 4, 9, 16]
|
224
416
|
|
225
|
-
|
226
|
-
#
|
227
|
-
# When happening a failure, the service object responsible for
|
228
|
-
# will be accessible in the result
|
229
|
-
#=================================================================#
|
417
|
+
# Note:
|
418
|
+
# ----
|
419
|
+
# When happening a failure, the service object responsible for this
|
420
|
+
# will be accessible in the result
|
230
421
|
|
231
422
|
result = SquareAllNumbers.call(numbers: %w[1 1 b 2 3 4])
|
232
423
|
|
@@ -234,13 +425,81 @@ result.failure? # true
|
|
234
425
|
result.service.is_a?(Steps::ConvertToNumbers) # true
|
235
426
|
|
236
427
|
result.on_failure do |_message, service|
|
237
|
-
puts "#{service.class.name} was the service responsible
|
428
|
+
puts "#{service.class.name} was the service responsible for the failure" } # Steps::ConvertToNumbers was the service responsible for the failure
|
238
429
|
end
|
239
430
|
```
|
240
431
|
|
432
|
+
[⬆️ Back to Top](#table-of-contents)
|
433
|
+
|
434
|
+
#### Is it possible to compose pipelines with other pipelines?
|
435
|
+
|
436
|
+
Answer: Yes, it is.
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
module Steps
|
440
|
+
class ConvertToNumbers < Micro::Service::Base
|
441
|
+
attribute :numbers
|
442
|
+
|
443
|
+
def call!
|
444
|
+
if numbers.all? { |value| String(value) =~ /\d+/ }
|
445
|
+
Success(numbers: numbers.map(&:to_i))
|
446
|
+
else
|
447
|
+
Failure('numbers must contain only numeric types')
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
class Add2 < Micro::Service::Strict
|
453
|
+
attribute :numbers
|
454
|
+
|
455
|
+
def call!
|
456
|
+
Success(numbers: numbers.map { |number| number + 2 })
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
class Double < Micro::Service::Strict
|
461
|
+
attribute :numbers
|
462
|
+
|
463
|
+
def call!
|
464
|
+
Success(numbers: numbers.map { |number| number * 2 })
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
class Square < Micro::Service::Strict
|
469
|
+
attribute :numbers
|
470
|
+
|
471
|
+
def call!
|
472
|
+
Success(numbers: numbers.map { |number| number * number })
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
Add2ToAllNumbers = Steps::ConvertToNumbers >> Steps::Add2
|
478
|
+
DoubleAllNumbers = Steps::ConvertToNumbers >> Steps::Double
|
479
|
+
SquareAllNumbers = Steps::ConvertToNumbers >> Steps::Square
|
480
|
+
|
481
|
+
DoubleAllNumbersAndAdd2 = DoubleAllNumbers >> Steps::Add2
|
482
|
+
SquareAllNumbersAndAdd2 = SquareAllNumbers >> Steps::Add2
|
483
|
+
|
484
|
+
SquareAllNumbersAndDouble = SquareAllNumbersAndAdd2 >> DoubleAllNumbers
|
485
|
+
DoubleAllNumbersAndSquareAndAdd2 = DoubleAllNumbers >> SquareAllNumbersAndAdd2
|
486
|
+
|
487
|
+
SquareAllNumbersAndDouble
|
488
|
+
.call(numbers: %w[1 1 2 2 3 4])
|
489
|
+
.on_success { |value| p value[:numbers] } # [6, 6, 12, 12, 22, 36]
|
490
|
+
|
491
|
+
DoubleAllNumbersAndSquareAndAdd2
|
492
|
+
.call(numbers: %w[1 1 2 2 3 4])
|
493
|
+
.on_success { |value| p value[:numbers] } # [6, 6, 18, 18, 38, 66]
|
494
|
+
```
|
495
|
+
|
496
|
+
Note: You can blend any of the [syntaxes/approaches to create the pipelines](#how-to-create-a-pipeline-of-service-objects)) - [examples](https://github.com/serradura/u-service/blob/master/test/micro/service/pipeline/blend_test.rb#L7-L34).
|
497
|
+
|
498
|
+
[⬆️ Back to Top](#table-of-contents)
|
499
|
+
|
241
500
|
### What is a strict Service Object?
|
242
501
|
|
243
|
-
|
502
|
+
Answer: Is a service object which will require all keywords (attributes) on its initialization.
|
244
503
|
|
245
504
|
```ruby
|
246
505
|
class Double < Micro::Service::Strict
|
@@ -257,9 +516,97 @@ Double.call({})
|
|
257
516
|
# ArgumentError (missing keyword: :numbers)
|
258
517
|
```
|
259
518
|
|
519
|
+
[⬆️ Back to Top](#table-of-contents)
|
520
|
+
|
521
|
+
### Is there some feature to auto handle exceptions inside of services/pipelines?
|
522
|
+
|
523
|
+
Answer: Yes, there is!
|
524
|
+
|
525
|
+
**Service Objects:**
|
526
|
+
|
527
|
+
Like `Micro::Service::Strict` the `Micro::Service::Safe` is another special kind of Service object. It has the ability to auto wrap an exception into a failure result. e.g:
|
528
|
+
|
529
|
+
```ruby
|
530
|
+
require 'logger'
|
531
|
+
|
532
|
+
AppLogger = Logger.new(STDOUT)
|
533
|
+
|
534
|
+
class Divide < Micro::Service::Safe
|
535
|
+
attributes :a, :b
|
536
|
+
|
537
|
+
def call!
|
538
|
+
return Success(a / b) if a.is_a?(Integer) && b.is_a?(Integer)
|
539
|
+
Failure(:not_an_integer)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
result = Divide.call(a: 2, b: 0)
|
544
|
+
result.type == :exception # true
|
545
|
+
result.value.is_a?(ZeroDivisionError) # true
|
546
|
+
|
547
|
+
result.on_failure(:exception) do |exception|
|
548
|
+
AppLogger.error(exception.message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
|
549
|
+
end
|
550
|
+
|
551
|
+
# Note:
|
552
|
+
# ----
|
553
|
+
# If you need a specific error handling,
|
554
|
+
# I recommend the usage of a case statement. e,g:
|
555
|
+
|
556
|
+
result.on_failure(:exception) do |exception, service|
|
557
|
+
case exception
|
558
|
+
when ZeroDivisionError then AppLogger.error(exception.message)
|
559
|
+
else AppLogger.debug("#{service.class.name} was the service responsible for the exception")
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
# Another note:
|
564
|
+
# ------------
|
565
|
+
# It is possible to rescue an exception even when is a safe service.
|
566
|
+
# Examples: https://github.com/serradura/u-service/blob/a6d0a8aa5d28d1f062484eaa0d5a17c4fb08b6fb/test/micro/service/safe_test.rb#L95-L123
|
567
|
+
```
|
568
|
+
|
569
|
+
**Pipelines:**
|
570
|
+
|
571
|
+
As the safe services, safe pipelines have the ability to intercept an exception in any of its steps. These are the ways to define one:
|
572
|
+
|
573
|
+
```ruby
|
574
|
+
module Users
|
575
|
+
Create = ProcessParams & ValidateParams & Persist & SendToCRM
|
576
|
+
end
|
577
|
+
|
578
|
+
# Note:
|
579
|
+
# The ampersand is based on the safe navigation operator. https://ruby-doc.org/core-2.6/doc/syntax/calling_methods_rdoc.html#label-Safe+navigation+operator
|
580
|
+
|
581
|
+
# The alternatives are:
|
582
|
+
|
583
|
+
module Users
|
584
|
+
class Create
|
585
|
+
include Micro::Service::Pipeline::Safe
|
586
|
+
|
587
|
+
pipeline ProcessParams, ValidateParams, Persist, SendToCRM
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
# or
|
592
|
+
|
593
|
+
module Users
|
594
|
+
Create = Micro::Service::Pipeline::Safe[
|
595
|
+
ProcessParams,
|
596
|
+
ValidateParams,
|
597
|
+
Persist,
|
598
|
+
SendToCRM
|
599
|
+
]
|
600
|
+
end
|
601
|
+
```
|
602
|
+
|
603
|
+
[⬆️ Back to Top](#table-of-contents)
|
604
|
+
|
260
605
|
### How to validate Service Object attributes?
|
261
606
|
|
262
|
-
|
607
|
+
**Requirement:**
|
608
|
+
|
609
|
+
To do this your application must have the [activemodel >= 3.2](https://rubygems.org/gems/activemodel) as a dependency.
|
263
610
|
|
264
611
|
```ruby
|
265
612
|
#
|
@@ -272,7 +619,7 @@ class Multiply < Micro::Service::Base
|
|
272
619
|
validates :a, :b, presence: true, numericality: true
|
273
620
|
|
274
621
|
def call!
|
275
|
-
return Failure(
|
622
|
+
return Failure(:validation_error) { self.errors } unless valid?
|
276
623
|
|
277
624
|
Success(number: a * b)
|
278
625
|
end
|
@@ -287,7 +634,7 @@ end
|
|
287
634
|
require 'micro/service/with_validation' # or require 'u-service/with_validation'
|
288
635
|
|
289
636
|
# In the Gemfile
|
290
|
-
gem 'u-service',
|
637
|
+
gem 'u-service', require: 'u-service/with_validation'
|
291
638
|
|
292
639
|
# Using this approach, you can rewrite the previous sample with fewer lines of code.
|
293
640
|
|
@@ -302,73 +649,14 @@ class Multiply < Micro::Service::Base
|
|
302
649
|
end
|
303
650
|
|
304
651
|
# Note:
|
652
|
+
# ----
|
305
653
|
# After requiring the validation mode, the
|
306
|
-
# Micro::Service::Strict classes will inherit this new behavior.
|
654
|
+
# Micro::Service::Strict and Micro::Service::Safe classes will inherit this new behavior.
|
307
655
|
```
|
308
656
|
|
309
|
-
|
657
|
+
[⬆️ Back to Top](#table-of-contents)
|
310
658
|
|
311
|
-
|
312
|
-
|
313
|
-
```ruby
|
314
|
-
module Steps
|
315
|
-
class ConvertToNumbers < Micro::Service::Base
|
316
|
-
attribute :numbers
|
317
|
-
|
318
|
-
def call!
|
319
|
-
if numbers.all? { |value| String(value) =~ /\d+/ }
|
320
|
-
Success(numbers: numbers.map(&:to_i))
|
321
|
-
else
|
322
|
-
Failure('numbers must contain only numeric types')
|
323
|
-
end
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
class Add2 < Micro::Service::Strict
|
328
|
-
attribute :numbers
|
329
|
-
|
330
|
-
def call!
|
331
|
-
Success(numbers: numbers.map { |number| number + 2 })
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
class Double < Micro::Service::Strict
|
336
|
-
attribute :numbers
|
337
|
-
|
338
|
-
def call!
|
339
|
-
Success(numbers: numbers.map { |number| number * 2 })
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
class Square < Micro::Service::Strict
|
344
|
-
attribute :numbers
|
345
|
-
|
346
|
-
def call!
|
347
|
-
Success(numbers: numbers.map { |number| number * number })
|
348
|
-
end
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
Add2ToAllNumbers = Steps::ConvertToNumbers >> Steps::Add2
|
353
|
-
DoubleAllNumbers = Steps::ConvertToNumbers >> Steps::Double
|
354
|
-
SquareAllNumbers = Steps::ConvertToNumbers >> Steps::Square
|
355
|
-
DoubleAllNumbersAndAdd2 = DoubleAllNumbers >> Steps::Add2
|
356
|
-
SquareAllNumbersAndAdd2 = SquareAllNumbers >> Steps::Add2
|
357
|
-
SquareAllNumbersAndDouble = SquareAllNumbersAndAdd2 >> DoubleAllNumbers
|
358
|
-
DoubleAllNumbersAndSquareAndAdd2 = DoubleAllNumbers >> SquareAllNumbersAndAdd2
|
359
|
-
|
360
|
-
SquareAllNumbersAndDouble
|
361
|
-
.call(numbers: %w[1 1 2 2 3 4])
|
362
|
-
.on_success { |value| p value[:numbers] } # [6, 6, 12, 12, 22, 36]
|
363
|
-
|
364
|
-
DoubleAllNumbersAndSquareAndAdd2
|
365
|
-
.call(numbers: %w[1 1 2 2 3 4])
|
366
|
-
.on_success { |value| p value[:numbers] } # [6, 6, 18, 18, 38, 66]
|
367
|
-
```
|
368
|
-
|
369
|
-
Note: You can blend any of the [syntaxes/approaches to create the pipelines](#how-to-create-a-pipeline-of-service-objects)) - [examples](https://github.com/serradura/u-service/blob/master/test/micro/service/pipeline/blend_test.rb#L7-L34).
|
370
|
-
|
371
|
-
## Examples
|
659
|
+
### Examples
|
372
660
|
|
373
661
|
1. [Rescuing an exception inside of service objects](https://github.com/serradura/u-service/blob/master/examples/rescuing_exceptions.rb)
|
374
662
|
2. [Users creation](https://github.com/serradura/u-service/blob/master/examples/users_creation.rb)
|
@@ -378,6 +666,8 @@ Note: You can blend any of the [syntaxes/approaches to create the pipelines](#ho
|
|
378
666
|
|
379
667
|
A more complex example which use rake tasks to demonstrate how to handle user data, and how to use different failures type to control the app flow.
|
380
668
|
|
669
|
+
[⬆️ Back to Top](#table-of-contents)
|
670
|
+
|
381
671
|
## Comparisons
|
382
672
|
|
383
673
|
Check it out implementations of the same use case with different libs (abstractions).
|
data/lib/micro/service.rb
CHANGED
@@ -3,8 +3,10 @@
|
|
3
3
|
require 'micro/attributes'
|
4
4
|
|
5
5
|
require 'micro/service/version'
|
6
|
-
|
6
|
+
require 'micro/service/error'
|
7
7
|
require 'micro/service/result'
|
8
8
|
require 'micro/service/base'
|
9
|
+
require 'micro/service/safe'
|
9
10
|
require 'micro/service/strict'
|
11
|
+
require 'micro/service/pipeline/reducer'
|
10
12
|
require 'micro/service/pipeline'
|
data/lib/micro/service/base.rb
CHANGED
@@ -5,14 +5,16 @@ module Micro
|
|
5
5
|
class Base
|
6
6
|
include Micro::Attributes.without(:strict_initialize)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
private_constant :UNEXPECTED_RESULT, :ResultIsAlreadyDefined, :InvalidResultInstance
|
8
|
+
def self.to_proc
|
9
|
+
Proc.new { |arg| call(arg) }
|
10
|
+
end
|
13
11
|
|
14
12
|
def self.>>(service)
|
15
|
-
|
13
|
+
Pipeline[self, service]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.&(service)
|
17
|
+
Pipeline::Safe[self, service]
|
16
18
|
end
|
17
19
|
|
18
20
|
def self.call(options = {})
|
@@ -20,12 +22,21 @@ module Micro
|
|
20
22
|
end
|
21
23
|
|
22
24
|
def self.__new__(result, arg)
|
23
|
-
instance =
|
25
|
+
instance = new(arg)
|
24
26
|
instance.__set_result__(result)
|
25
|
-
instance.send(:initialize, arg)
|
26
27
|
instance
|
27
28
|
end
|
28
29
|
|
30
|
+
def self.__failure_type(arg, type)
|
31
|
+
return type if type != :error
|
32
|
+
|
33
|
+
case arg
|
34
|
+
when Exception then :exception
|
35
|
+
when Symbol then arg
|
36
|
+
else type
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
29
40
|
def call!
|
30
41
|
raise NotImplementedError
|
31
42
|
end
|
@@ -35,8 +46,8 @@ module Micro
|
|
35
46
|
end
|
36
47
|
|
37
48
|
def __set_result__(result)
|
38
|
-
raise InvalidResultInstance unless result.is_a?(Result)
|
39
|
-
raise ResultIsAlreadyDefined if @__result
|
49
|
+
raise Error::InvalidResultInstance unless result.is_a?(Result)
|
50
|
+
raise Error::ResultIsAlreadyDefined if @__result
|
40
51
|
@__result = result
|
41
52
|
end
|
42
53
|
|
@@ -44,8 +55,8 @@ module Micro
|
|
44
55
|
|
45
56
|
def __call
|
46
57
|
result = call!
|
47
|
-
return result if result.is_a?(
|
48
|
-
raise
|
58
|
+
return result if result.is_a?(Result)
|
59
|
+
raise Error::UnexpectedResult.new(self.class)
|
49
60
|
end
|
50
61
|
|
51
62
|
def __get_result__
|
@@ -58,7 +69,8 @@ module Micro
|
|
58
69
|
end
|
59
70
|
|
60
71
|
def Failure(arg = :error)
|
61
|
-
value
|
72
|
+
value = block_given? ? yield : arg
|
73
|
+
type = self.class.__failure_type(value, block_given? ? arg : :error)
|
62
74
|
__get_result__.__set__(false, value, type, self)
|
63
75
|
end
|
64
76
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Service
|
5
|
+
module Error
|
6
|
+
class UnexpectedResult < TypeError
|
7
|
+
MESSAGE = '#call! must return an instance of Micro::Service::Result'.freeze
|
8
|
+
|
9
|
+
def initialize(klass); super(klass.name + MESSAGE); end
|
10
|
+
end
|
11
|
+
|
12
|
+
ResultIsAlreadyDefined = ArgumentError.new('result is already defined'.freeze)
|
13
|
+
|
14
|
+
InvalidResultType = TypeError.new('type must be a Symbol'.freeze)
|
15
|
+
InvalidResultInstance = ArgumentError.new('argument must be an instance of Micro::Service::Result'.freeze)
|
16
|
+
|
17
|
+
InvalidService = TypeError.new('service must be a kind or an instance of Micro::Service::Base'.freeze)
|
18
|
+
InvalidServices = ArgumentError.new('argument must be a collection of `Micro::Service::Base` classes'.freeze)
|
19
|
+
|
20
|
+
UndefinedPipeline = ArgumentError.new("This class hasn't declared its pipeline. Please, use the `pipeline()` macro to define one.".freeze)
|
21
|
+
|
22
|
+
class InvalidAccessToTheServiceObject < StandardError
|
23
|
+
MSG = 'only a failure result can access its service object'.freeze
|
24
|
+
|
25
|
+
def initialize(message = MSG); super; end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ByWrongUsage
|
29
|
+
def self.check(exception)
|
30
|
+
exception.is_a?(Error::UnexpectedResult) || exception.is_a?(ArgumentError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -3,65 +3,13 @@
|
|
3
3
|
module Micro
|
4
4
|
module Service
|
5
5
|
module Pipeline
|
6
|
-
class Reducer
|
7
|
-
attr_reader :services
|
8
|
-
|
9
|
-
InvalidServices = ArgumentError.new('argument must be a collection of `Micro::Service::Base` classes'.freeze)
|
10
|
-
|
11
|
-
private_constant :InvalidServices
|
12
|
-
|
13
|
-
def self.map_services(arg)
|
14
|
-
return arg.services if arg.is_a?(Reducer)
|
15
|
-
return arg.__pipeline__.services if arg.is_a?(Class) && arg < Micro::Service::Pipeline
|
16
|
-
Array(arg)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.build(args)
|
20
|
-
services = Array(args).flat_map { |arg| map_services(arg) }
|
21
|
-
|
22
|
-
raise InvalidServices if services.any? { |klass| !(klass < ::Micro::Service::Base) }
|
23
|
-
|
24
|
-
new(services)
|
25
|
-
end
|
26
|
-
|
27
|
-
def initialize(services)
|
28
|
-
@services = services
|
29
|
-
end
|
30
|
-
|
31
|
-
def call(arg = {})
|
32
|
-
@services.reduce(initial_result(arg)) do |result, service|
|
33
|
-
break result if result.failure?
|
34
|
-
service.__new__(result, result.value).call
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def >>(arg)
|
39
|
-
Reducer.build(services + self.class.map_services(arg))
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def initial_result(arg)
|
45
|
-
return arg.call if arg_to_call?(arg)
|
46
|
-
return arg if arg.is_a?(Micro::Service::Result)
|
47
|
-
result = Micro::Service::Result.new
|
48
|
-
result.__set__(true, arg, :ok, nil)
|
49
|
-
end
|
50
|
-
|
51
|
-
def arg_to_call?(arg)
|
52
|
-
return true if arg.is_a?(Micro::Service::Base) || arg.is_a?(Reducer)
|
53
|
-
return true if arg.is_a?(Class) && (arg < Micro::Service::Base || arg < Micro::Service::Pipeline)
|
54
|
-
return false
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
6
|
module ClassMethods
|
59
7
|
def __pipeline__
|
60
8
|
@__pipeline
|
61
9
|
end
|
62
10
|
|
63
11
|
def pipeline(*args)
|
64
|
-
@__pipeline =
|
12
|
+
@__pipeline = pipeline_reducer.build(args)
|
65
13
|
end
|
66
14
|
|
67
15
|
def call(options = {})
|
@@ -69,30 +17,40 @@ module Micro
|
|
69
17
|
end
|
70
18
|
end
|
71
19
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
20
|
+
CONSTRUCTOR = <<-RUBY
|
21
|
+
def initialize(options)
|
22
|
+
@options = options
|
23
|
+
pipeline = self.class.__pipeline__
|
24
|
+
raise Error::UndefinedPipeline unless pipeline
|
76
25
|
end
|
26
|
+
RUBY
|
77
27
|
|
78
|
-
|
79
|
-
|
80
|
-
private_constant :UndefinedPipeline
|
28
|
+
private_constant :ClassMethods, :CONSTRUCTOR
|
81
29
|
|
82
30
|
def self.included(base)
|
31
|
+
def base.pipeline_reducer; Reducer; end
|
83
32
|
base.extend(ClassMethods)
|
84
|
-
base.class_eval(
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
90
|
-
RUBY
|
33
|
+
base.class_eval(CONSTRUCTOR)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.[](*args)
|
37
|
+
Reducer.build(args)
|
91
38
|
end
|
92
39
|
|
93
40
|
def call
|
94
41
|
self.class.__pipeline__.call(@options)
|
95
42
|
end
|
43
|
+
|
44
|
+
module Safe
|
45
|
+
def self.included(base)
|
46
|
+
base.send(:include, Micro::Service::Pipeline)
|
47
|
+
def base.pipeline_reducer; SafeReducer; end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.[](*args)
|
51
|
+
SafeReducer.build(args)
|
52
|
+
end
|
53
|
+
end
|
96
54
|
end
|
97
55
|
end
|
98
56
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Micro
|
4
|
+
module Service
|
5
|
+
module Pipeline
|
6
|
+
class Reducer
|
7
|
+
attr_reader :services
|
8
|
+
def self.map_services(arg)
|
9
|
+
return arg.services if arg.is_a?(Reducer)
|
10
|
+
return arg.__pipeline__.services if arg.is_a?(Class) && arg < Micro::Service::Pipeline
|
11
|
+
Array(arg)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.build(args)
|
15
|
+
services = Array(args).flat_map { |arg| map_services(arg) }
|
16
|
+
|
17
|
+
raise Error::InvalidServices if services.any? { |klass| !(klass < ::Micro::Service::Base) }
|
18
|
+
|
19
|
+
new(services)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(services)
|
23
|
+
@services = services
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(arg = {})
|
27
|
+
@services.reduce(initial_result(arg)) do |result, service|
|
28
|
+
break result if result.failure?
|
29
|
+
service.__new__(result, result.value).call
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def >>(arg)
|
34
|
+
self.class.build(services + self.class.map_services(arg))
|
35
|
+
end
|
36
|
+
|
37
|
+
def &(arg)
|
38
|
+
raise NoMethodError, "undefined method `&' for #{self.inspect}. Please, use the method `>>' to avoid this error."
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_proc
|
42
|
+
Proc.new { |arg| call(arg) }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def initial_result(arg)
|
48
|
+
return arg.call if arg_to_call?(arg)
|
49
|
+
return arg if arg.is_a?(Micro::Service::Result)
|
50
|
+
result = Micro::Service::Result.new
|
51
|
+
result.__set__(true, arg, :ok, nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
def arg_to_call?(arg)
|
55
|
+
return true if arg.is_a?(Micro::Service::Base) || arg.is_a?(Reducer)
|
56
|
+
return true if arg.is_a?(Class) && (arg < Micro::Service::Base || arg < Micro::Service::Pipeline)
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class SafeReducer < Reducer
|
62
|
+
def call(arg = {})
|
63
|
+
@services.reduce(initial_result(arg)) do |result, service|
|
64
|
+
break result if result.failure?
|
65
|
+
service_result(service, result)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :&, :>>
|
70
|
+
|
71
|
+
def >>(arg)
|
72
|
+
raise NoMethodError, "undefined method `>>' for #{self.inspect}. Please, use the method `&' to avoid this error."
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def service_result(service, result)
|
78
|
+
begin
|
79
|
+
instance = service.__new__(result, result.value)
|
80
|
+
instance.call
|
81
|
+
rescue => exception
|
82
|
+
raise exception if Error::ByWrongUsage.check(exception)
|
83
|
+
result.__set__(false, exception, :exception, instance)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/micro/service/result.rb
CHANGED
@@ -3,20 +3,11 @@
|
|
3
3
|
module Micro
|
4
4
|
module Service
|
5
5
|
class Result
|
6
|
-
InvalidType = TypeError.new('type must be a Symbol'.freeze)
|
7
|
-
InvalidService = TypeError.new('service must be a kind or an instance of Micro::Service::Base'.freeze)
|
8
|
-
|
9
|
-
class InvalidAccessToTheServiceObject < StandardError
|
10
|
-
MSG = 'only a failure result can access its service object'.freeze
|
11
|
-
|
12
|
-
def initialize(message = MSG); super; end
|
13
|
-
end
|
14
|
-
|
15
6
|
attr_reader :value, :type
|
16
7
|
|
17
8
|
def __set__(is_success, value, type, service)
|
18
|
-
raise
|
19
|
-
raise InvalidService if !is_success && !is_a_service?(service)
|
9
|
+
raise Error::InvalidResultType unless type.is_a?(Symbol)
|
10
|
+
raise Error::InvalidService if !is_success && !is_a_service?(service)
|
20
11
|
|
21
12
|
@success, @value, @type, @service = is_success, value, type, service
|
22
13
|
|
@@ -34,25 +25,25 @@ module Micro
|
|
34
25
|
def service
|
35
26
|
return @service if failure?
|
36
27
|
|
37
|
-
raise InvalidAccessToTheServiceObject
|
28
|
+
raise Error::InvalidAccessToTheServiceObject
|
38
29
|
end
|
39
30
|
|
40
|
-
def on_success(arg =
|
31
|
+
def on_success(arg = nil)
|
41
32
|
self.tap { yield(value) if success_type?(arg) }
|
42
33
|
end
|
43
34
|
|
44
|
-
def on_failure(arg =
|
35
|
+
def on_failure(arg = nil)
|
45
36
|
self.tap{ yield(value, @service) if failure_type?(arg) }
|
46
37
|
end
|
47
38
|
|
48
39
|
private
|
49
40
|
|
50
41
|
def success_type?(arg)
|
51
|
-
success? && (arg
|
42
|
+
success? && (arg.nil? || arg == type)
|
52
43
|
end
|
53
44
|
|
54
45
|
def failure_type?(arg)
|
55
|
-
failure? && (arg
|
46
|
+
failure? && (arg.nil? || arg == type)
|
56
47
|
end
|
57
48
|
|
58
49
|
def is_a_service?(arg)
|
data/lib/micro/service/strict.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: u-service
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rodrigo Serradura
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-08-
|
11
|
+
date: 2019-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: u-attributes
|
@@ -72,8 +72,11 @@ files:
|
|
72
72
|
- bin/setup
|
73
73
|
- lib/micro/service.rb
|
74
74
|
- lib/micro/service/base.rb
|
75
|
+
- lib/micro/service/error.rb
|
75
76
|
- lib/micro/service/pipeline.rb
|
77
|
+
- lib/micro/service/pipeline/reducer.rb
|
76
78
|
- lib/micro/service/result.rb
|
79
|
+
- lib/micro/service/safe.rb
|
77
80
|
- lib/micro/service/strict.rb
|
78
81
|
- lib/micro/service/version.rb
|
79
82
|
- lib/micro/service/with_validation.rb
|