base-service 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +98 -99
- data/lib/base_service/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45a8f83a521de9cb92c4eb8c0daa2ea3e538e7de91663b259f1fdb6d98be4782
|
|
4
|
+
data.tar.gz: b79e464c3d7a103b5ebb38db2236fe62247174643c1e84cfe69e527c61e095ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 205d747fa15e805388e7478762c317c30cdeb2f65bd5916f50e3bf4ed5279331d49e94f606752c3be4076245074815815481bf039f50f5887c35639ed3fd2555
|
|
7
|
+
data.tar.gz: e7a8f77f2501781bfebbcd7888b672bbb98908d7df00ca5c8ba6cfb416c1a03cf13016d6f9f253f96f00981f5792180c9b9a8a161d62fad1fe2cfa8320fcf2ed
|
data/README.md
CHANGED
|
@@ -6,43 +6,43 @@
|
|
|
6
6
|
[](https://www.rubydoc.info/gems/base-service)
|
|
7
7
|
[](https://rubygems.org/gems/base-service)
|
|
8
8
|
|
|
9
|
-
`base-service`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
9
|
+
`base-service` provides a lightweight base class for building Ruby service
|
|
10
|
+
objects with a consistent interface:
|
|
11
|
+
|
|
12
|
+
- keyword arguments become instance variables;
|
|
13
|
+
- business logic is defined in a zero-argument `call` method;
|
|
14
|
+
- errors and messages are collected during execution;
|
|
15
|
+
- every call returns a `Service::Result`;
|
|
16
|
+
- named callbacks can be configured through
|
|
17
17
|
[`callback-collection`](https://github.com/nicolasva/callback-collection).
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Add the gem to your application's `Gemfile`:
|
|
22
22
|
|
|
23
23
|
```ruby
|
|
24
24
|
gem "base-service"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Then install the dependencies:
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
bundle install
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
In a Ruby application that does not use Bundler, require the gem explicitly:
|
|
34
34
|
|
|
35
35
|
```ruby
|
|
36
36
|
require "base_service"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Rails
|
|
40
|
-
|
|
39
|
+
Rails normally loads the gem automatically when it is declared in the
|
|
40
|
+
`Gemfile`.
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Creating a service
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
A service inherits from `Service::Base` and implements a public, zero-argument
|
|
45
|
+
`call` method:
|
|
46
46
|
|
|
47
47
|
```ruby
|
|
48
48
|
class DoubleValueService < Service::Base
|
|
@@ -55,19 +55,19 @@ class DoubleValueService < Service::Base
|
|
|
55
55
|
end
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
Keyword arguments passed to the service are automatically exposed as instance
|
|
59
|
+
variables. In this example, `value: 21` becomes `@value`.
|
|
60
60
|
|
|
61
61
|
```ruby
|
|
62
62
|
result = DoubleValueService.call(value: 21)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
There is no need to define an initializer in every service.
|
|
66
66
|
|
|
67
|
-
##
|
|
67
|
+
## Call results
|
|
68
68
|
|
|
69
|
-
`
|
|
70
|
-
|
|
69
|
+
`MyService.call(...)` does not return the business value directly. It always
|
|
70
|
+
returns a `Service::Result`:
|
|
71
71
|
|
|
72
72
|
```ruby
|
|
73
73
|
result = DoubleValueService.call(value: 21)
|
|
@@ -78,21 +78,21 @@ result.errors # => []
|
|
|
78
78
|
result.messages # => ["Value doubled"]
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
|
81
|
+
| Method | Description |
|
|
82
82
|
|---|---|
|
|
83
|
-
| `result.result` |
|
|
84
|
-
| `result.errors` |
|
|
85
|
-
| `result.messages` |
|
|
86
|
-
| `result.successful?` | `true`
|
|
83
|
+
| `result.result` | The value returned by the service's `call` method |
|
|
84
|
+
| `result.errors` | Errors appended during execution |
|
|
85
|
+
| `result.messages` | Messages appended during execution |
|
|
86
|
+
| `result.successful?` | `true` when `result.errors` is empty |
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
The `Service::Result` object and its `errors` and `messages` arrays are frozen
|
|
89
|
+
after execution. The business value stored in `result.result` is not
|
|
90
|
+
automatically frozen.
|
|
91
91
|
|
|
92
|
-
##
|
|
92
|
+
## Adding errors
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
The protected `append_error(type, message)` method adds a `Service::Error` to
|
|
95
|
+
the result:
|
|
96
96
|
|
|
97
97
|
```ruby
|
|
98
98
|
class CreateUserService < Service::Base
|
|
@@ -111,20 +111,19 @@ end
|
|
|
111
111
|
```ruby
|
|
112
112
|
result = CreateUserService.call(user: user)
|
|
113
113
|
|
|
114
|
-
result.successful? # => false
|
|
114
|
+
result.successful? # => false when at least one error was appended
|
|
115
115
|
|
|
116
116
|
error = result.errors.first
|
|
117
117
|
error.type # => :email
|
|
118
118
|
error.message # => ["has already been taken"]
|
|
119
|
-
error.caller_info # =>
|
|
119
|
+
error.caller_info # => location where append_error was called
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
The gem does not transform the error type or message. They can therefore use
|
|
123
|
+
values supplied directly by Rails or by the application's domain.
|
|
124
124
|
|
|
125
|
-
`append_error`
|
|
126
|
-
|
|
127
|
-
doit s'arrêter :
|
|
125
|
+
`append_error` only records an error. It does not raise an exception or stop
|
|
126
|
+
the service automatically. Use `return` when execution must stop:
|
|
128
127
|
|
|
129
128
|
```ruby
|
|
130
129
|
def call
|
|
@@ -137,13 +136,13 @@ def call
|
|
|
137
136
|
end
|
|
138
137
|
```
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
Exceptions raised by business logic, Active Record, or a callback are not
|
|
140
|
+
rescued by `Service::Base`. They propagate to the caller normally.
|
|
142
141
|
|
|
143
|
-
##
|
|
142
|
+
## Adding messages
|
|
144
143
|
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
The protected `append_message(message)` method adds non-blocking information
|
|
145
|
+
to the result:
|
|
147
146
|
|
|
148
147
|
```ruby
|
|
149
148
|
class ImportUserService < Service::Base
|
|
@@ -160,15 +159,15 @@ result = ImportUserService.call(attributes: { email: "ruby@example.com" })
|
|
|
160
159
|
|
|
161
160
|
result.successful? # => true
|
|
162
161
|
result.messages # => ["User 42 imported"]
|
|
163
|
-
result.result # =>
|
|
162
|
+
result.result # => a User instance
|
|
164
163
|
```
|
|
165
164
|
|
|
166
|
-
|
|
165
|
+
Adding a message does not affect `successful?`.
|
|
167
166
|
|
|
168
|
-
##
|
|
167
|
+
## Rails service example
|
|
169
168
|
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
A service can use Active Record models, Rails helpers, and private methods like
|
|
170
|
+
any other Ruby object:
|
|
172
171
|
|
|
173
172
|
```ruby
|
|
174
173
|
module Billing
|
|
@@ -215,7 +214,7 @@ module Billing
|
|
|
215
214
|
end
|
|
216
215
|
```
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
Call the service with keyword arguments:
|
|
219
218
|
|
|
220
219
|
```ruby
|
|
221
220
|
result = Billing::DiscountedPriceService.call(
|
|
@@ -230,10 +229,10 @@ else
|
|
|
230
229
|
end
|
|
231
230
|
```
|
|
232
231
|
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
The service instance remains mutable during execution. Memoization with `||=`,
|
|
233
|
+
such as `@customer ||= ...`, therefore works as expected.
|
|
235
234
|
|
|
236
|
-
##
|
|
235
|
+
## Order creation example
|
|
237
236
|
|
|
238
237
|
```ruby
|
|
239
238
|
module Orders
|
|
@@ -298,14 +297,14 @@ else
|
|
|
298
297
|
end
|
|
299
298
|
```
|
|
300
299
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
The service returns the order in `result.result` even when errors were
|
|
301
|
+
appended. Use `result.successful?` to determine whether the execution was
|
|
302
|
+
successful.
|
|
304
303
|
|
|
305
|
-
##
|
|
304
|
+
## Calling a service from another service
|
|
306
305
|
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
A service can call another service. Check the returned `Service::Result` and
|
|
307
|
+
retrieve its business value explicitly:
|
|
309
308
|
|
|
310
309
|
```ruby
|
|
311
310
|
class CheckoutService < Service::Base
|
|
@@ -330,10 +329,10 @@ class CheckoutService < Service::Base
|
|
|
330
329
|
end
|
|
331
330
|
```
|
|
332
331
|
|
|
333
|
-
|
|
334
|
-
service
|
|
332
|
+
Errors from a nested service are not copied automatically to the calling
|
|
333
|
+
service. The example above propagates them explicitly.
|
|
335
334
|
|
|
336
|
-
##
|
|
335
|
+
## Using a service in a Rails controller
|
|
337
336
|
|
|
338
337
|
```ruby
|
|
339
338
|
class OrdersController < ApplicationController
|
|
@@ -357,8 +356,8 @@ end
|
|
|
357
356
|
|
|
358
357
|
## Callbacks
|
|
359
358
|
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
The block passed to `.call` or `.new` builds an immutable callback collection
|
|
360
|
+
provided by the `callback-collection` gem:
|
|
362
361
|
|
|
363
362
|
```ruby
|
|
364
363
|
class NotifyUserService < Service::Base
|
|
@@ -386,71 +385,71 @@ result = NotifyUserService.call(user_id: 42) do |callbacks|
|
|
|
386
385
|
end
|
|
387
386
|
```
|
|
388
387
|
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
Callbacks are not invoked automatically by `Service::Base`. The service
|
|
389
|
+
decides when to invoke them:
|
|
391
390
|
|
|
392
391
|
```ruby
|
|
393
|
-
@callbacks.respond_with(:
|
|
392
|
+
@callbacks.respond_with(:callback_name, argument)
|
|
394
393
|
```
|
|
395
394
|
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
The safe navigation operator (`&.`) makes the callback block optional. Without
|
|
396
|
+
it, the service must be called with the expected callback.
|
|
398
397
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
398
|
+
The collection is frozen after configuration. Invoking an unknown callback
|
|
399
|
+
raises `NoMethodError`, and exceptions raised inside callbacks propagate to
|
|
400
|
+
the caller.
|
|
402
401
|
|
|
403
|
-
|
|
404
|
-
[`callback-collection`](https://github.com/nicolasva/callback-collection)
|
|
405
|
-
|
|
402
|
+
See the
|
|
403
|
+
[`callback-collection`](https://github.com/nicolasva/callback-collection)
|
|
404
|
+
documentation for method-based callback registration and Ractor
|
|
405
|
+
compatibility.
|
|
406
406
|
|
|
407
|
-
##
|
|
407
|
+
## Manual instantiation
|
|
408
408
|
|
|
409
|
-
|
|
409
|
+
The recommended form is:
|
|
410
410
|
|
|
411
411
|
```ruby
|
|
412
412
|
result = MyService.call(argument: value)
|
|
413
413
|
```
|
|
414
414
|
|
|
415
|
-
|
|
415
|
+
It is equivalent to:
|
|
416
416
|
|
|
417
417
|
```ruby
|
|
418
418
|
service = MyService.new(argument: value)
|
|
419
419
|
result = service.execute
|
|
420
420
|
```
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
422
|
+
In both cases, `execute` creates a new execution context, invokes the business
|
|
423
|
+
`call` method, and builds a `Service::Result`.
|
|
424
424
|
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
Avoid reusing the same instance for multiple calls to `execute`.
|
|
426
|
+
`MyService.call(...)` creates a dedicated instance for each execution.
|
|
427
427
|
|
|
428
|
-
##
|
|
428
|
+
## Development
|
|
429
429
|
|
|
430
430
|
```sh
|
|
431
431
|
bundle install
|
|
432
432
|
bundle exec rake
|
|
433
433
|
```
|
|
434
434
|
|
|
435
|
-
|
|
435
|
+
The default task runs the test suite and then builds the gem in `pkg/`.
|
|
436
436
|
|
|
437
|
-
##
|
|
437
|
+
## Publishing to RubyGems
|
|
438
438
|
|
|
439
|
-
|
|
439
|
+
Releases use
|
|
440
440
|
[RubyGems Trusted Publishing](https://guides.rubygems.org/trusted-publishing/).
|
|
441
|
-
|
|
441
|
+
No RubyGems API key needs to be stored in GitHub secrets.
|
|
442
442
|
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
Configure a trusted publisher for the gem on RubyGems with the following
|
|
444
|
+
values:
|
|
445
445
|
|
|
446
|
-
- gem
|
|
447
|
-
-
|
|
448
|
-
-
|
|
449
|
-
- workflow
|
|
450
|
-
-
|
|
446
|
+
- gem: `base-service`;
|
|
447
|
+
- repository owner: `nicolasva`;
|
|
448
|
+
- repository: `base-service`;
|
|
449
|
+
- workflow: `release.yml`;
|
|
450
|
+
- GitHub environment: leave blank.
|
|
451
451
|
|
|
452
|
-
|
|
453
|
-
`Service::VERSION` :
|
|
452
|
+
Publish a version by creating a tag that exactly matches `Service::VERSION`:
|
|
454
453
|
|
|
455
454
|
```sh
|
|
456
455
|
VERSION=$(ruby -Ilib -rbase_service/version -e 'print Service::VERSION')
|
|
@@ -458,5 +457,5 @@ git tag "v${VERSION}"
|
|
|
458
457
|
git push origin "v${VERSION}"
|
|
459
458
|
```
|
|
460
459
|
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
GitHub Actions then builds and publishes the gem to RubyGems. RubyDoc
|
|
461
|
+
automatically generates documentation for the published version.
|
data/lib/base_service/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: base-service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -70,7 +70,7 @@ homepage: https://github.com/nicolasva/base-service
|
|
|
70
70
|
licenses: []
|
|
71
71
|
metadata:
|
|
72
72
|
rubygems_mfa_required: 'true'
|
|
73
|
-
documentation_uri: https://www.rubydoc.info/gems/base-service/0.1.
|
|
73
|
+
documentation_uri: https://www.rubydoc.info/gems/base-service/0.1.2
|
|
74
74
|
source_code_uri: https://github.com/nicolasva/base-service
|
|
75
75
|
rdoc_options: []
|
|
76
76
|
require_paths:
|