servactory 1.6.0 → 1.6.1
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 +6 -431
- data/lib/servactory/configuration/factory.rb +4 -0
- data/lib/servactory/configuration/setup.rb +4 -1
- data/lib/servactory/methods/dsl.rb +14 -0
- data/lib/servactory/methods/shortcuts/collection.rb +17 -0
- data/lib/servactory/methods/workbench.rb +1 -1
- data/lib/servactory/version.rb +1 -1
- metadata +3 -3
- data/lib/servactory/context/configuration.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77500bca8d6ee7ee302e77b8b64eb840a2e004bece8743a2b246e49a49a42b24
|
4
|
+
data.tar.gz: dfff784e9b2787ac349d5e33f84f75a318814ecf64ac0df7915a500b2073f6ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16091e0330a74cda1246f0a3a4ccdbc9ed572cba9c29d485e306c95c2405792e12bd9d03cbf13e1e17cf0c40bd4839ea997d75eac7f5f26a199eb8bc757af01a
|
7
|
+
data.tar.gz: 2d67bd5beff3c5a4d73a28a70662b3df52e61757fadc37f15751e745e28f9a3b18cd3af43d84684339debd58b6231ca70bdcbcc8b2af8c20ef76f9eb9e68c883
|
data/README.md
CHANGED
@@ -5,439 +5,14 @@ A set of tools for building reliable services of any complexity.
|
|
5
5
|
[](https://rubygems.org/gems/servactory)
|
6
6
|
[](https://github.com/afuno/servactory/releases)
|
7
7
|
|
8
|
-
##
|
8
|
+
## Documentation
|
9
9
|
|
10
|
-
|
11
|
-
- [Getting started](#getting-started)
|
12
|
-
- [Conventions](#conventions)
|
13
|
-
- [Installation](#installation)
|
14
|
-
- [Preparation](#preparation)
|
15
|
-
- [Usage](#usage)
|
16
|
-
- [Minimal example](#minimal-example)
|
17
|
-
- [Call](#call)
|
18
|
-
- [Result](#result)
|
19
|
-
- [Input](#input)
|
20
|
-
- [Type](#option-type)
|
21
|
-
- [Required](#option-required)
|
22
|
-
- [Internal](#option-internal)
|
23
|
-
- [Internal name](#option-as)
|
24
|
-
- [Array](#option-array)
|
25
|
-
- [Inclusion](#option-inclusion)
|
26
|
-
- [Must](#option-must)
|
27
|
-
- [Output](#output)
|
28
|
-
- [Internal](#internal)
|
29
|
-
- [Make](#make)
|
30
|
-
- [Failures](#failures)
|
31
|
-
- [Inheritance](#inheritance)
|
32
|
-
- [I18n](#i18n)
|
33
|
-
- [Testing](#testing)
|
34
|
-
- [Thanks](#thanks)
|
35
|
-
- [Contributing](#contributing)
|
10
|
+
See [servactory.com](https://servactory.com) for documentation.
|
36
11
|
|
37
|
-
##
|
38
|
-
|
39
|
-
- Ruby >= 2.7
|
40
|
-
|
41
|
-
## Getting started
|
42
|
-
|
43
|
-
### Conventions
|
44
|
-
|
45
|
-
- Services are subclasses of `Servactory::Base` and are located in the `app/services` directory. It is common practice to create and inherit from `ApplicationService::Base`, which is a subclass of `Servactory::Base`.
|
46
|
-
- Name services by what they do, not by what they accept. Try to use verbs in names. For example, `UsersService::Create` instead of `UsersService::Creation`.
|
47
|
-
|
48
|
-
### Installation
|
49
|
-
|
50
|
-
Add this to `Gemfile`:
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
gem "servactory"
|
54
|
-
```
|
55
|
-
|
56
|
-
And execute:
|
57
|
-
|
58
|
-
```shell
|
59
|
-
bundle install
|
60
|
-
```
|
61
|
-
|
62
|
-
### Preparation
|
63
|
-
|
64
|
-
As a first step, it is recommended to prepare the base class for further inheritance.
|
65
|
-
|
66
|
-
#### ApplicationService::Errors
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
# app/services/application_service/errors.rb
|
70
|
-
|
71
|
-
module ApplicationService
|
72
|
-
module Errors
|
73
|
-
class InputError < Servactory::Errors::InputError; end
|
74
|
-
class OutputError < Servactory::Errors::OutputError; end
|
75
|
-
class InternalError < Servactory::Errors::InternalError; end
|
76
|
-
|
77
|
-
class Failure < Servactory::Errors::Failure; end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
```
|
81
|
-
|
82
|
-
#### ApplicationService::Base
|
83
|
-
|
84
|
-
```ruby
|
85
|
-
# app/services/application_service/base.rb
|
86
|
-
|
87
|
-
module ApplicationService
|
88
|
-
class Base < Servactory::Base
|
89
|
-
configuration do
|
90
|
-
input_error_class ApplicationService::Errors::InputError
|
91
|
-
output_error_class ApplicationService::Errors::OutputError
|
92
|
-
internal_error_class ApplicationService::Errors::InternalError
|
93
|
-
|
94
|
-
failure_class ApplicationService::Errors::Failure
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
```
|
99
|
-
|
100
|
-
## Usage
|
101
|
-
|
102
|
-
### Minimal example
|
103
|
-
|
104
|
-
```ruby
|
105
|
-
class MinimalService < ApplicationService::Base
|
106
|
-
make :call
|
107
|
-
|
108
|
-
private
|
109
|
-
|
110
|
-
def call
|
111
|
-
# ...
|
112
|
-
end
|
113
|
-
end
|
114
|
-
```
|
115
|
-
|
116
|
-
[More examples](https://github.com/afuno/servactory/tree/main/examples/usual)
|
117
|
-
|
118
|
-
### Call
|
119
|
-
|
120
|
-
Services can only be called via `.call` and `.call!` methods.
|
121
|
-
|
122
|
-
The `.call` method will only fail if it catches an exception in the input arguments.
|
123
|
-
Internal and output attributes, as well as methods for failures - all this will be collected in the result.
|
124
|
-
|
125
|
-
The `.call!` method will fail if it catches any exception.
|
126
|
-
|
127
|
-
#### Via .call
|
128
|
-
|
129
|
-
```ruby
|
130
|
-
UsersService::Accept.call(user: User.first)
|
131
|
-
```
|
132
|
-
|
133
|
-
#### Via .call!
|
134
|
-
|
135
|
-
```ruby
|
136
|
-
UsersService::Accept.call!(user: User.first)
|
137
|
-
```
|
138
|
-
|
139
|
-
### Result
|
140
|
-
|
141
|
-
All services have the result of their work. For example, in case of success this call:
|
142
|
-
|
143
|
-
```ruby
|
144
|
-
service_result = UsersService::Accept.call!(user: User.first)
|
145
|
-
```
|
146
|
-
|
147
|
-
Will return this:
|
148
|
-
|
149
|
-
```ruby
|
150
|
-
#<Servactory::Result:0x0000000107ad9e88 @user="...">
|
151
|
-
```
|
152
|
-
|
153
|
-
And then you can work with this result, for example, in this way:
|
154
|
-
|
155
|
-
```ruby
|
156
|
-
Notification::SendJob.perform_later(service_result.user.id)
|
157
|
-
```
|
158
|
-
|
159
|
-
### Input
|
160
|
-
|
161
|
-
#### Option `type`
|
162
|
-
|
163
|
-
Always required to specify. May contain one or more classes.
|
164
|
-
|
165
|
-
```ruby
|
166
|
-
class UsersService::Accept < ApplicationService::Base
|
167
|
-
input :user, type: User
|
168
|
-
|
169
|
-
# ...
|
170
|
-
end
|
171
|
-
```
|
172
|
-
|
173
|
-
```ruby
|
174
|
-
class ToggleService < ApplicationService::Base
|
175
|
-
input :flag, type: [TrueClass, FalseClass]
|
176
|
-
|
177
|
-
# ...
|
178
|
-
end
|
179
|
-
```
|
180
|
-
|
181
|
-
#### Option `required`
|
182
|
-
|
183
|
-
By default, `required` is set to `true`.
|
184
|
-
|
185
|
-
```ruby
|
186
|
-
class UsersService::Create < ApplicationService::Base
|
187
|
-
input :first_name, type: String
|
188
|
-
input :middle_name, type: String, required: false
|
189
|
-
input :last_name, type: String
|
190
|
-
|
191
|
-
# ...
|
192
|
-
end
|
193
|
-
```
|
194
|
-
|
195
|
-
#### Option `internal`
|
196
|
-
|
197
|
-
By default, `internal` is set to `false`.
|
198
|
-
|
199
|
-
```ruby
|
200
|
-
class UsersService::Accept < ApplicationService::Base
|
201
|
-
input :user, type: User
|
202
|
-
|
203
|
-
make :accept!
|
204
|
-
|
205
|
-
private
|
206
|
-
|
207
|
-
def accept!
|
208
|
-
inputs.user.accept!
|
209
|
-
end
|
210
|
-
end
|
211
|
-
```
|
212
|
-
|
213
|
-
```ruby
|
214
|
-
class UsersService::Accept < ApplicationService::Base
|
215
|
-
input :user, type: User, internal: true
|
216
|
-
|
217
|
-
make :accept!
|
218
|
-
|
219
|
-
private
|
220
|
-
|
221
|
-
def accept!
|
222
|
-
user.accept!
|
223
|
-
end
|
224
|
-
end
|
225
|
-
```
|
226
|
-
|
227
|
-
#### Option `as`
|
228
|
-
|
229
|
-
This option changes the name of the input within the service.
|
230
|
-
|
231
|
-
```ruby
|
232
|
-
class NotificationService::Create < ApplicationService::Base
|
233
|
-
input :customer, as: :user, type: User
|
234
|
-
|
235
|
-
output :notification, type: Notification
|
236
|
-
|
237
|
-
make :create_notification!
|
238
|
-
|
239
|
-
private
|
240
|
-
|
241
|
-
def create_notification!
|
242
|
-
self.notification = Notification.create!(user: inputs.user)
|
243
|
-
end
|
244
|
-
end
|
245
|
-
```
|
246
|
-
|
247
|
-
#### Option `array`
|
248
|
-
|
249
|
-
Using this option will mean that the input argument is an array, each element of which has the specified type.
|
250
|
-
|
251
|
-
```ruby
|
252
|
-
class PymentsService::Send < ApplicationService::Base
|
253
|
-
input :invoice_numbers, type: String, array: true
|
254
|
-
|
255
|
-
# ...
|
256
|
-
end
|
257
|
-
```
|
258
|
-
|
259
|
-
#### Option `inclusion`
|
260
|
-
|
261
|
-
```ruby
|
262
|
-
class EventService::Send < ApplicationService::Base
|
263
|
-
input :event_name, type: String, inclusion: %w[created rejected approved]
|
264
|
-
|
265
|
-
# ...
|
266
|
-
end
|
267
|
-
```
|
268
|
-
|
269
|
-
#### Option `must`
|
270
|
-
|
271
|
-
Sometimes there are cases that require the implementation of a specific input attribute check. In such cases `must` can help.
|
272
|
-
|
273
|
-
```ruby
|
274
|
-
class PymentsService::Send < ApplicationService::Base
|
275
|
-
input :invoice_numbers,
|
276
|
-
type: String,
|
277
|
-
array: true,
|
278
|
-
must: {
|
279
|
-
be_6_characters: {
|
280
|
-
is: ->(value:) { value.all? { |id| id.size == 6 } }
|
281
|
-
}
|
282
|
-
}
|
283
|
-
|
284
|
-
# ...
|
285
|
-
end
|
286
|
-
```
|
287
|
-
|
288
|
-
### Output
|
289
|
-
|
290
|
-
```ruby
|
291
|
-
class NotificationService::Create < ApplicationService::Base
|
292
|
-
input :user, type: User
|
293
|
-
|
294
|
-
output :notification, type: Notification
|
295
|
-
|
296
|
-
make :create_notification!
|
297
|
-
|
298
|
-
private
|
299
|
-
|
300
|
-
def create_notification!
|
301
|
-
self.notification = Notification.create!(user: inputs.user)
|
302
|
-
end
|
303
|
-
end
|
304
|
-
```
|
305
|
-
|
306
|
-
### Internal
|
307
|
-
|
308
|
-
```ruby
|
309
|
-
class NotificationService::Create < ApplicationService::Base
|
310
|
-
input :user, type: User
|
311
|
-
|
312
|
-
internal :inviter, type: User
|
313
|
-
|
314
|
-
output :notification, type: Notification
|
315
|
-
|
316
|
-
make :assign_inviter
|
317
|
-
make :create_notification!
|
318
|
-
|
319
|
-
private
|
320
|
-
|
321
|
-
def assign_inviter
|
322
|
-
self.inviter = user.inviter
|
323
|
-
end
|
324
|
-
|
325
|
-
def create_notification!
|
326
|
-
self.notification = Notification.create!(user: inputs.user, inviter:)
|
327
|
-
end
|
328
|
-
end
|
329
|
-
```
|
330
|
-
|
331
|
-
### Make
|
332
|
-
|
333
|
-
#### Minimal example
|
334
|
-
|
335
|
-
```ruby
|
336
|
-
make :something
|
337
|
-
|
338
|
-
def something
|
339
|
-
# ...
|
340
|
-
end
|
341
|
-
```
|
342
|
-
|
343
|
-
#### Condition
|
344
|
-
|
345
|
-
```ruby
|
346
|
-
make :something, if: -> { Settings.something.enabled }
|
347
|
-
|
348
|
-
def something
|
349
|
-
# ...
|
350
|
-
end
|
351
|
-
```
|
352
|
-
|
353
|
-
#### Several
|
354
|
-
|
355
|
-
```ruby
|
356
|
-
make :assign_api_model
|
357
|
-
make :perform_api_request
|
358
|
-
make :process_result
|
359
|
-
|
360
|
-
def assign_api_model
|
361
|
-
self.api_model = APIModel.new
|
362
|
-
end
|
363
|
-
|
364
|
-
def perform_api_request
|
365
|
-
self.response = APIClient.resource.create(api_model)
|
366
|
-
end
|
367
|
-
|
368
|
-
def process_result
|
369
|
-
ARModel.create!(response)
|
370
|
-
end
|
371
|
-
```
|
372
|
-
|
373
|
-
### Failures
|
374
|
-
|
375
|
-
The methods that are used in `make` may fail. In order to more informatively provide information about this outside the service, the following methods were prepared.
|
376
|
-
|
377
|
-
#### Fail
|
378
|
-
|
379
|
-
```ruby
|
380
|
-
make :check!
|
381
|
-
|
382
|
-
def check!
|
383
|
-
return if inputs.invoice_number.start_with?("AA")
|
384
|
-
|
385
|
-
fail!(message: "Invalid invoice number")
|
386
|
-
end
|
387
|
-
```
|
388
|
-
|
389
|
-
#### Fail for input
|
390
|
-
|
391
|
-
```ruby
|
392
|
-
make :check!
|
393
|
-
|
394
|
-
def check!
|
395
|
-
return if inputs.invoice_number.start_with?("AA")
|
396
|
-
|
397
|
-
fail_input!(:invoice_number, message: "Invalid invoice number")
|
398
|
-
end
|
399
|
-
```
|
400
|
-
|
401
|
-
#### Metadata
|
402
|
-
|
403
|
-
```ruby
|
404
|
-
fail!(
|
405
|
-
message: "Invalid invoice number",
|
406
|
-
meta: {
|
407
|
-
invoice_number: inputs.invoice_number
|
408
|
-
}
|
409
|
-
)
|
410
|
-
```
|
411
|
-
|
412
|
-
```ruby
|
413
|
-
exception.detailed_message # => Invalid invoice number (ApplicationService::Errors::Failure)
|
414
|
-
exception.message # => Invalid invoice number
|
415
|
-
exception.type # => :fail
|
416
|
-
exception.meta # => {:invoice_number=>"BB-7650AE"}
|
417
|
-
```
|
418
|
-
|
419
|
-
### Inheritance
|
420
|
-
|
421
|
-
Inheritance between services is also supported.
|
422
|
-
|
423
|
-
## I18n
|
424
|
-
|
425
|
-
All texts are stored in the localization file. All texts can be changed or supplemented by new locales.
|
426
|
-
|
427
|
-
[See en.yml file](https://github.com/afuno/servactory/tree/main/config/locales/en.yml)
|
428
|
-
|
429
|
-
## Testing
|
430
|
-
|
431
|
-
Testing Servactory services is the same as testing regular Ruby classes.
|
432
|
-
|
433
|
-
## Thanks
|
12
|
+
## Contributing
|
434
13
|
|
435
|
-
|
14
|
+
This project is intended to be a safe, welcoming space for collaboration. Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. We recommend reading the [contributing guide](./docs/pages/CONTRIBUTING.md) as well.
|
436
15
|
|
437
|
-
##
|
16
|
+
## License
|
438
17
|
|
439
|
-
|
440
|
-
2. Create your feature branch (`git checkout -b my-new-feature`);
|
441
|
-
3. Commit your changes (`git commit -am "Add some feature"`);
|
442
|
-
4. Push to the branch (`git push origin my-new-feature`);
|
443
|
-
5. Create a new Pull Request.
|
18
|
+
ViewComponent is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -18,6 +18,10 @@ module Servactory
|
|
18
18
|
def failure_class(failure_class)
|
19
19
|
Servactory.configuration.failure_class = failure_class
|
20
20
|
end
|
21
|
+
|
22
|
+
def method_shortcuts(method_shortcuts)
|
23
|
+
Servactory.configuration.method_shortcuts.merge(method_shortcuts)
|
24
|
+
end
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
@@ -6,7 +6,8 @@ module Servactory
|
|
6
6
|
attr_accessor :input_error_class,
|
7
7
|
:internal_error_class,
|
8
8
|
:output_error_class,
|
9
|
-
:failure_class
|
9
|
+
:failure_class,
|
10
|
+
:method_shortcuts
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@input_error_class = Servactory::Errors::InputError
|
@@ -14,6 +15,8 @@ module Servactory
|
|
14
15
|
@output_error_class = Servactory::Errors::OutputError
|
15
16
|
|
16
17
|
@failure_class = Servactory::Errors::Failure
|
18
|
+
|
19
|
+
@method_shortcuts = Servactory::Methods::Shortcuts::Collection.new
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
@@ -20,6 +20,20 @@ module Servactory
|
|
20
20
|
collection_of_methods << Method.new(name, **options)
|
21
21
|
end
|
22
22
|
|
23
|
+
def method_missing(shortcut_name, *args, &block)
|
24
|
+
return super unless Servactory.configuration.method_shortcuts.include?(shortcut_name)
|
25
|
+
|
26
|
+
method_options = args.last.is_a?(Hash) ? args.pop : {}
|
27
|
+
|
28
|
+
args.each do |method_name|
|
29
|
+
make(:"#{shortcut_name}_#{method_name}", **method_options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to_missing?(shortcut_name, *)
|
34
|
+
Servactory.configuration.method_shortcuts.include?(shortcut_name) || super
|
35
|
+
end
|
36
|
+
|
23
37
|
def collection_of_methods
|
24
38
|
@collection_of_methods ||= Collection.new
|
25
39
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Servactory
|
4
|
+
module Methods
|
5
|
+
module Shortcuts
|
6
|
+
class Collection
|
7
|
+
# NOTE: http://words.steveklabnik.com/beware-subclassing-ruby-core-classes
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@collection, :<<, :each, :merge, :include?
|
10
|
+
|
11
|
+
def initialize(*)
|
12
|
+
@collection = Set.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/servactory/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: servactory
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.6.
|
4
|
+
version: 1.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anton Sokolov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-05-
|
11
|
+
date: 2023-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -194,7 +194,6 @@ files:
|
|
194
194
|
- lib/servactory/configuration/factory.rb
|
195
195
|
- lib/servactory/configuration/setup.rb
|
196
196
|
- lib/servactory/context/callable.rb
|
197
|
-
- lib/servactory/context/configuration.rb
|
198
197
|
- lib/servactory/context/dsl.rb
|
199
198
|
- lib/servactory/context/store.rb
|
200
199
|
- lib/servactory/context/workspace.rb
|
@@ -235,6 +234,7 @@ files:
|
|
235
234
|
- lib/servactory/methods/collection.rb
|
236
235
|
- lib/servactory/methods/dsl.rb
|
237
236
|
- lib/servactory/methods/method.rb
|
237
|
+
- lib/servactory/methods/shortcuts/collection.rb
|
238
238
|
- lib/servactory/methods/workbench.rb
|
239
239
|
- lib/servactory/outputs/checks/base.rb
|
240
240
|
- lib/servactory/outputs/checks/type.rb
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Servactory
|
4
|
-
module Context
|
5
|
-
class Configuration
|
6
|
-
def input_error_class(input_error_class)
|
7
|
-
Servactory.configuration.input_error_class = input_error_class
|
8
|
-
end
|
9
|
-
|
10
|
-
def output_error_class(output_error_class)
|
11
|
-
Servactory.configuration.output_error_class = output_error_class
|
12
|
-
end
|
13
|
-
|
14
|
-
def internal_error_class(internal_error_class)
|
15
|
-
Servactory.configuration.internal_error_class = internal_error_class
|
16
|
-
end
|
17
|
-
|
18
|
-
def failure_class(failure_class)
|
19
|
-
Servactory.configuration.failure_class = failure_class
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|