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 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