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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a511f12c5bc76025f0156c65d934988065f1c51c0d540b67a2cf2f4dcde48f22
4
- data.tar.gz: 12489e03b009e02bef85329051952f50866c06f0d585812e55146569b4d535ad
3
+ metadata.gz: d75c44a6cbf48201cc58937ce39118ddba4af411cccdb6c9c930e4bcac80b451
4
+ data.tar.gz: 343e21ab69a17bb3cc392bb4ae9ec16ae55e0af73313ccbf376f4381371aed81
5
5
  SHA512:
6
- metadata.gz: '0811b8d7893d0cb810345a582b3bb2c2a1dd2c0a21753e512212b9445e6f42bcab601e55ee1b683f2477ec72fa6ee690b741d23f105ffe63d1758dffa66fed50'
7
- data.tar.gz: 950935aee3330a885206a85333894c2d27886bb1373ac7151efc95dbb9cdacc1ebe489967dafb1cca0351473460f0122e3d2cbbcea9ca52ea6836995a85de241
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, compose a pipeline of service objects to represents complex business logic. (input >> process/transform >> output)
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 create a Service Object?](#how-to-create-a-service-object)
21
- - [How to use the result hooks?](#how-to-use-the-result-hooks)
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
- - [It's possible to compose pipelines with other pipelines?](#its-possible-to-compose-pipelines-with-other-pipelines)
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 create a Service Object?
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(:invalid_data)
79
+ Failure { '`a` and `b` attributes must be numeric' }
67
80
  end
68
81
  end
69
82
  end
70
83
 
71
- #====================#
72
- # Calling a service #
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
- p result.success? # true
78
- p result.value # 4
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
- # The result of a Micro::Service#call
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
- result = Multiply.new(a: 2, b: 3).call
118
+ ### What is a `Micro::Service::Result`?
89
119
 
90
- p result.success? # true
91
- p result.value # 6
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
- # Verify the result failure #
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 = Multiply.call(a: '2', b: 2)
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
- p result.success? # false
100
- p result.failure? # true
101
- p result.value # :invalid_data
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
- ### How to use the result hooks?
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 when is a success:
129
- # 6
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 output (raised an error) when is a failure:
142
- # ArgumentError (the number must be greater than 0)
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
- # Attention: #
227
- # When happening a failure, the service object responsible for it #
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 by the failure" } # Steps::ConvertToNumbers was the service responsible by the failure
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
- A: Is a service object which will require all keywords (attributes) on its initialization.
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
- Note: To do this your application must have the [activemodel >= 3.2](https://rubygems.org/gems/activemodel) as a dependency.
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(errors: self.errors) unless valid?
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', '~> 0.12.0', require: 'u-service/with_validation'
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
- ### It's possible to compose pipelines with other pipelines?
657
+ [⬆️ Back to Top](#table-of-contents)
310
658
 
311
- Answer: Yes
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).
@@ -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'
@@ -5,14 +5,16 @@ module Micro
5
5
  class Base
6
6
  include Micro::Attributes.without(:strict_initialize)
7
7
 
8
- UNEXPECTED_RESULT = '#call! must return an instance of Micro::Service::Result'.freeze
9
- InvalidResultInstance = ArgumentError.new('argument must be an instance of Micro::Service::Result'.freeze)
10
- ResultIsAlreadyDefined = ArgumentError.new('result is already defined'.freeze)
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
- Micro::Service::Pipeline[self, service]
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 = allocate
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?(Service::Result)
48
- raise TypeError, self.class.name + UNEXPECTED_RESULT
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, type = block_given? ? [yield, arg] : [arg, :error]
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 = Reducer.build(args)
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
- private_constant :ClassMethods
73
-
74
- def self.[](*args)
75
- Reducer.build(args)
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
- UndefinedPipeline = ArgumentError.new("This class hasn't declared its pipeline. Please, use the `pipeline()` macro to define one.".freeze)
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(<<-RUBY)
85
- def initialize(options)
86
- @options = options
87
- pipeline = self.class.__pipeline__
88
- raise UndefinedPipeline unless pipeline
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
@@ -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 InvalidType unless type.is_a?(Symbol)
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 = :ok)
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 = :error)
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 == :ok || arg == type)
42
+ success? && (arg.nil? || arg == type)
52
43
  end
53
44
 
54
45
  def failure_type?(arg)
55
- failure? && (arg == :error || arg == type)
46
+ failure? && (arg.nil? || arg == type)
56
47
  end
57
48
 
58
49
  def is_a_service?(arg)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ module Service
5
+ class Safe < Service::Base
6
+ def call
7
+ super
8
+ rescue => exception
9
+ raise exception if Error::ByWrongUsage.check(exception)
10
+ Failure(exception)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -4,6 +4,10 @@ module Micro
4
4
  module Service
5
5
  class Strict < Service::Base
6
6
  include Micro::Attributes::Features::StrictInitialize
7
+
8
+ class Safe < Service::Safe
9
+ include Micro::Attributes::Features::StrictInitialize
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  module Service
5
- VERSION = '0.14.0'.freeze
5
+ VERSION = '1.0.0'.freeze
6
6
  end
7
7
  end
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.14.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-20 00:00:00.000000000 Z
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