u-service 0.14.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|