zen-service 1.1.0 → 2.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 +4 -4
- data/.rubocop.yml +2 -2
- data/.tool-versions +1 -0
- data/README.md +54 -164
- data/bin/console +0 -65
- data/lib/zen/service/plugins/attributes.rb +14 -10
- data/lib/zen/service/plugins/callable.rb +22 -0
- data/lib/zen/service/plugins/persisted_result.rb +28 -0
- data/lib/zen/service/plugins/pluggable.rb +15 -13
- data/lib/zen/service/plugins/result_yielding.rb +27 -0
- data/lib/zen/service/plugins.rb +3 -8
- data/lib/zen/service/version.rb +1 -1
- data/lib/zen/service.rb +1 -1
- data/zen-service.gemspec +2 -2
- metadata +9 -15
- data/.travis.yml +0 -4
- data/lib/zen/service/plugins/assertions.rb +0 -17
- data/lib/zen/service/plugins/context.rb +0 -53
- data/lib/zen/service/plugins/executable.rb +0 -179
- data/lib/zen/service/plugins/execution_cache.rb +0 -22
- data/lib/zen/service/plugins/policies.rb +0 -68
- data/lib/zen/service/plugins/rescue.rb +0 -34
- data/lib/zen/service/plugins/status.rb +0 -65
- data/lib/zen/service/plugins/validation.rb +0 -59
- data/lib/zen/service/spec_helpers.rb +0 -60
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64770c881ffd916d2ae348ea1e0e1b2f50f0ae1ddfe77e4cb33488894800a241
|
|
4
|
+
data.tar.gz: 1cf89ac8571af9af101ddfe2458a0a09e371a40e6d1e6bab9a58be0c0eec1445
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6467ffcd8497764c8974f45cef7106865d3109c3a794a2b7cbefd6342a3d69599392f56a83386be728ecca753fdf247a554ebd8b20c726ccc723b767618f54eb
|
|
7
|
+
data.tar.gz: '034914344b92311d62df7eec453791f87b39111a1d6f29c5344761718468c6f103a012a8981a37b0e358f612ab581ec29d4bb2a65f2b86f3a1f88ce303628157'
|
data/.rubocop.yml
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
NewCops: disable
|
|
3
|
-
TargetRubyVersion: 2
|
|
3
|
+
TargetRubyVersion: 3.2
|
|
4
4
|
|
|
5
5
|
Style/StringLiterals:
|
|
6
6
|
EnforcedStyle: double_quotes
|
|
7
7
|
|
|
8
8
|
Style/AccessModifierDeclarations:
|
|
9
|
-
EnforcedStyle:
|
|
9
|
+
EnforcedStyle: group
|
|
10
10
|
|
|
11
11
|
Style/Documentation:
|
|
12
12
|
Enabled: false
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.2.2
|
data/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Flexible and highly extensible Service Objects for business logic organization.
|
|
4
4
|
|
|
5
|
-
[](http://travis-ci.org/akuzko/zen-service)
|
|
6
5
|
[](https://github.com/akuzko/zen-service/releases)
|
|
7
6
|
|
|
8
7
|
## Installation
|
|
@@ -21,42 +20,22 @@ Or install it yourself as:
|
|
|
21
20
|
|
|
22
21
|
$ gem install zen-service
|
|
23
22
|
|
|
24
|
-
## Preface
|
|
25
|
-
|
|
26
|
-
From the beginning of Rails times, proper business logic code organization has always
|
|
27
|
-
been a problem. Some code was placed in models, some in controllers, and complexity of
|
|
28
|
-
both made applications hard to maintain. Then, patterns like "decorators", "facades" and
|
|
29
|
-
"presenters" appeared to take care of certain part of logic. Finally, multiple service
|
|
30
|
-
object solutions were proposed by many developers. This gem is one of such solutions, but
|
|
31
|
-
with a significant difference.
|
|
32
|
-
|
|
33
|
-
`Zen` services are aimed to take care of *all* business logic in your application, no
|
|
34
|
-
matter what it is aimed for, and how complicated it is. From simplest cases of managing
|
|
35
|
-
single model, to the most complicated logic related with external requests, `Zen` services
|
|
36
|
-
got you covered. They are highly extendable due to plugin-based approach, composable and
|
|
37
|
-
debuggable.
|
|
38
|
-
|
|
39
|
-
Side note: as can be seen from commit history, this gem was initially called as `excom`,
|
|
40
|
-
which stood for **Ex**ecutable **Com**and.
|
|
41
|
-
|
|
42
23
|
## Usage
|
|
43
24
|
|
|
44
|
-
General idea behind every `Zen` service is simple: each service can have optional attributes,
|
|
45
|
-
and should define `execute!` method that is called during service execution. Executed service
|
|
46
|
-
responds to `success?` and has `result`.
|
|
47
|
-
|
|
48
25
|
The very basic usage of `Zen` services can be shown with following example:
|
|
49
26
|
|
|
50
27
|
```rb
|
|
51
28
|
# app/services/todos/update.rb
|
|
52
29
|
module Todos
|
|
53
|
-
class Update < Zen::Service
|
|
30
|
+
class Update < ApplicationService # base class for app services, inherits from Zen::Service
|
|
54
31
|
attributes :todo, :params
|
|
55
32
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
33
|
+
def call
|
|
34
|
+
if todo.update(params)
|
|
35
|
+
[:ok, todo]
|
|
36
|
+
else
|
|
37
|
+
[:error, todo.errors.messages]
|
|
38
|
+
end
|
|
60
39
|
end
|
|
61
40
|
end
|
|
62
41
|
end
|
|
@@ -64,13 +43,9 @@ end
|
|
|
64
43
|
# app/controllers/todos/controller
|
|
65
44
|
class TodosController < ApplicationController
|
|
66
45
|
def update
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# class .[] method initializes service with passed arguments, executes it and returns it's result
|
|
71
|
-
render json: Todos::Show[service.result]
|
|
72
|
-
else
|
|
73
|
-
render json: service.errors, status: service.status
|
|
46
|
+
case Todos::Update.call(todo, params: todo_params)
|
|
47
|
+
in [:ok, todo] then render json: Todos::Show.call(todo)
|
|
48
|
+
in [:error, errors] then render json: errors, status: :unprocessable_content
|
|
74
49
|
end
|
|
75
50
|
end
|
|
76
51
|
end
|
|
@@ -91,7 +66,7 @@ were not declared with `attributes` class method.
|
|
|
91
66
|
class MyService < Zen::Service
|
|
92
67
|
attributes :foo, :bar
|
|
93
68
|
|
|
94
|
-
def
|
|
69
|
+
def call
|
|
95
70
|
# do something
|
|
96
71
|
end
|
|
97
72
|
|
|
@@ -112,163 +87,79 @@ s3.foo # => 1
|
|
|
112
87
|
s3.bar # => 2
|
|
113
88
|
```
|
|
114
89
|
|
|
115
|
-
### Service Execution
|
|
116
|
-
|
|
117
|
-
Read full version on [wiki](https://github.com/akuzko/zen-service/wiki#service-execution).
|
|
118
|
-
|
|
119
|
-
At the core of each service's execution lies `execute!` method. By default, you can use
|
|
120
|
-
`success`, `failure` and `result` methods to set execution success flag and result. If none
|
|
121
|
-
were used, result and success flag will be set based on `execute!` method's return value.
|
|
122
|
-
|
|
123
|
-
Example:
|
|
124
|
-
|
|
125
|
-
```rb
|
|
126
|
-
class Users::Create < Zen::Service
|
|
127
|
-
attributes :params
|
|
128
|
-
|
|
129
|
-
def execute!
|
|
130
|
-
result { User.create(params) } # explicit result assignment
|
|
131
|
-
|
|
132
|
-
send_invitation_email if success?
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
class Users::Update < Zen::Service
|
|
137
|
-
attributes :user, :params
|
|
138
|
-
|
|
139
|
-
def execute!
|
|
140
|
-
user.update(params) # implicit result assignment
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
service = Users::Create.new(valid_params)
|
|
145
|
-
service.execute.success? # => true
|
|
146
|
-
service.result # => instance of User
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Core API
|
|
150
|
-
|
|
151
|
-
Please read about core API and available class and instance methods on [wiki](https://github.com/akuzko/zen-service/wiki#core-api)
|
|
152
|
-
|
|
153
90
|
### Service Extensions (Plugins)
|
|
154
91
|
|
|
155
92
|
Read full version on [wiki](https://github.com/akuzko/zen-service/wiki/Plugins).
|
|
156
93
|
|
|
157
94
|
`zen-service` is built with extensions in mind. Even core functionality is organized in plugins that are
|
|
158
|
-
used in base `Zen::Service` class.
|
|
159
|
-
|
|
95
|
+
used in base `Zen::Service` class. Version 2.0.0 drops majority of built-in plugins for sake of
|
|
96
|
+
simplicity.
|
|
160
97
|
|
|
161
|
-
|
|
162
|
-
property to the service, as well as helper methods and behavior to set it. `status` property is not
|
|
163
|
-
bound to the "success" flag of execution state and can have any value depending on your needs. It
|
|
164
|
-
is up to you to setup which statuses correspond to successful execution and which are not. Generated
|
|
165
|
-
status helper methods allow to atomically and more explicitly assign both status and result at
|
|
166
|
-
the same time:
|
|
98
|
+
However, `zen-service` still provides a couple of helpfull plugins out-of-the-box:
|
|
167
99
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
use :status,
|
|
171
|
-
success: [:ok],
|
|
172
|
-
failure: [:unprocessable_entity]
|
|
173
|
-
|
|
174
|
-
attributes :post, :params
|
|
100
|
+
- `:persisted_result` - provides `#result` method that returns value of the latest `#call`
|
|
101
|
+
method call. Also provides `#called?` helper method.
|
|
175
102
|
|
|
176
|
-
|
|
103
|
+
- `:result_yielding` - can be used in junction with nested service calls to result with
|
|
104
|
+
block-provided value instead of nested service `call` return value. For example:
|
|
177
105
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
106
|
+
```rb
|
|
107
|
+
def call
|
|
108
|
+
logger.call do # logger uses `:result_yielding` plugin
|
|
109
|
+
todo.update!(params)
|
|
110
|
+
[:ok, todo]
|
|
111
|
+
rescue ActiveRecord::RecordInvalid
|
|
112
|
+
[:error, todo.errors.messages]
|
|
113
|
+
end
|
|
183
114
|
end
|
|
184
|
-
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
service = Posts::Update.(post, post_params)
|
|
188
|
-
# in case params were valid you will have:
|
|
189
|
-
service.success? # => true
|
|
190
|
-
service.status # => :ok
|
|
191
|
-
service.result # => {'id' => 1, ...}
|
|
192
|
-
```
|
|
115
|
+
```
|
|
193
116
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- [`:context`](https://github.com/akuzko/zen-service/wiki/Plugins#context) - Allows you to set an execution
|
|
198
|
-
context for a block that will be available to any service that uses this plugin via `context` method.
|
|
117
|
+
Bellow you can see sample implementation of a plugin that transforms resulting objects
|
|
118
|
+
to camel-case notation (relying on ActiveSupport's core extensions)
|
|
199
119
|
|
|
200
120
|
```rb
|
|
201
|
-
|
|
202
|
-
|
|
121
|
+
module CamelizeResult
|
|
122
|
+
extend Zen::Service::Plugin
|
|
203
123
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
yield
|
|
124
|
+
def self.used(service_class)
|
|
125
|
+
service_class.prepend(Extension)
|
|
207
126
|
end
|
|
208
|
-
end
|
|
209
|
-
```
|
|
210
127
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
128
|
+
def self.camelize(obj)
|
|
129
|
+
case obj
|
|
130
|
+
when Array then obj.map { camelize(_1) }
|
|
131
|
+
when Hash then obj.deep_transform_keys { _1.to_s.camelize(:lower).to_sym }
|
|
132
|
+
else obj
|
|
133
|
+
end
|
|
134
|
+
end
|
|
216
135
|
|
|
217
|
-
|
|
218
|
-
|
|
136
|
+
module Extension
|
|
137
|
+
def call
|
|
138
|
+
CamelizeResult.camelize(super)
|
|
139
|
+
end
|
|
219
140
|
end
|
|
220
141
|
end
|
|
221
142
|
```
|
|
222
143
|
|
|
223
|
-
|
|
224
|
-
checks within a service that can be used in other services for checks and guard violations. Much like
|
|
225
|
-
[pundit](https://github.com/elabs/pundit) Policies (hence the name), but more. Where pundit governs only
|
|
226
|
-
authorization logic, `zen-service`'s "policy" services can have any denial reason you find appropriate, and declare
|
|
227
|
-
logic for different denial reasons in single place. It also defines `#execute!` method that will result in
|
|
228
|
-
hash with all permission checks.
|
|
144
|
+
and then
|
|
229
145
|
|
|
230
146
|
```rb
|
|
231
|
-
class
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
attributes :post, :user
|
|
147
|
+
class Todos::Show < Zen::Service
|
|
148
|
+
attributes :todo
|
|
235
149
|
|
|
236
|
-
|
|
237
|
-
def publish?
|
|
238
|
-
# only author can publish a post
|
|
239
|
-
post.author_id == user.id
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def delete?
|
|
243
|
-
publish?
|
|
244
|
-
end
|
|
245
|
-
end
|
|
150
|
+
use :camelize_result
|
|
246
151
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
152
|
+
def call
|
|
153
|
+
{
|
|
154
|
+
id: todo.id,
|
|
155
|
+
is_completed: todo.completed?
|
|
156
|
+
}
|
|
252
157
|
end
|
|
253
158
|
end
|
|
254
159
|
|
|
255
|
-
|
|
256
|
-
policies.can?(:publish) # => true
|
|
257
|
-
policies.can?(:delete) # => false
|
|
258
|
-
policies.why_cant?(:delete) # => :unprocessable_entity
|
|
259
|
-
policies.guard!(:delete) # => raises Zen::Service::Plugins::Policies::GuardViolationError, :unprocessable_entity
|
|
260
|
-
policies.execute.result # => {'publish' => true, 'delete' => false}
|
|
160
|
+
Todos::Show[todo] # => { id: 1, isCompleted: true }
|
|
261
161
|
```
|
|
262
162
|
|
|
263
|
-
- [`:assertions`](https://github.com/akuzko/zen-service/wiki/Plugins#assertions) - Provides `assert` method that
|
|
264
|
-
can be used for different logic checks during service execution.
|
|
265
|
-
|
|
266
|
-
- [`:execution_cache`](https://github.com/akuzko/zen-service/wiki/Plugins#execution_cache) - Simple plugin that will prevent
|
|
267
|
-
re-execution of service if it already has been executed, and will immediately return result.
|
|
268
|
-
|
|
269
|
-
- [`:rescue`](https://github.com/akuzko/zen-service/wiki/Plugins#rescue) - Provides `:rescue` execution option.
|
|
270
|
-
If set to `true`, any error occurred during service execution will not be raised outside.
|
|
271
|
-
|
|
272
163
|
## Development
|
|
273
164
|
|
|
274
165
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
@@ -281,4 +172,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko
|
|
|
281
172
|
## License
|
|
282
173
|
|
|
283
174
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
284
|
-
|
data/bin/console
CHANGED
|
@@ -4,70 +4,5 @@
|
|
|
4
4
|
require "bundler/setup"
|
|
5
5
|
require "zen/service"
|
|
6
6
|
|
|
7
|
-
class Show < Zen::Service
|
|
8
|
-
use :execution_cache
|
|
9
|
-
|
|
10
|
-
attributes :foo
|
|
11
|
-
|
|
12
|
-
def execute!
|
|
13
|
-
sleep(1)
|
|
14
|
-
{ foo: foo }
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
class MyErrors < Hash
|
|
19
|
-
def add(key, message)
|
|
20
|
-
(self[key] ||= []) << message
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
class Policies < Zen::Service
|
|
25
|
-
use :policies
|
|
26
|
-
|
|
27
|
-
attributes :foo, :bar, :threshold
|
|
28
|
-
|
|
29
|
-
deny_with :unauthorized do
|
|
30
|
-
def save?
|
|
31
|
-
foo != 6
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
deny_with :unprocessable_entity do
|
|
36
|
-
def save?
|
|
37
|
-
foo != 7
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def bar?
|
|
41
|
-
bar && bar > threshold
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
class Save < Zen::Service
|
|
47
|
-
use :status
|
|
48
|
-
use :validation, errors_class: MyErrors
|
|
49
|
-
use :assertions
|
|
50
|
-
|
|
51
|
-
attributes :foo, :bar
|
|
52
|
-
|
|
53
|
-
def foo
|
|
54
|
-
super || 6
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def execute!
|
|
58
|
-
result { foo * 2 }
|
|
59
|
-
assert { foo > bar }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private def validate
|
|
63
|
-
errors.add(:foo, "invalid") unless policies.can?(:save)
|
|
64
|
-
errors.add(:bar, "too small") unless policies.can?(:bar)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private def policies
|
|
68
|
-
@policies ||= Policies.new(foo: foo, bar: bar, threshold: 0)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
7
|
require "pry"
|
|
73
8
|
Pry.start
|
|
@@ -5,8 +5,8 @@ module Zen
|
|
|
5
5
|
module Attributes
|
|
6
6
|
extend Plugin
|
|
7
7
|
|
|
8
|
-
def initialize(*args)
|
|
9
|
-
@attributes = assert_valid_attributes!(resolve_args!(args))
|
|
8
|
+
def initialize(*args, **kwargs)
|
|
9
|
+
@attributes = assert_valid_attributes!(resolve_args!(args, kwargs))
|
|
10
10
|
|
|
11
11
|
super()
|
|
12
12
|
end
|
|
@@ -20,24 +20,28 @@ module Zen
|
|
|
20
20
|
clone.tap { |copy| copy.attributes.merge!(attributes) }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
protected
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def attributes
|
|
24
26
|
@attributes
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def resolve_args!(args, kwargs) # rubocop:disable Metrics/AbcSize
|
|
29
32
|
attributes = {}
|
|
33
|
+
total_length = args.length + kwargs.length
|
|
30
34
|
allowed_length = self.class.attributes_list.length
|
|
31
35
|
|
|
32
|
-
if
|
|
33
|
-
raise ArgumentError, "wrong number of attributes (given #{
|
|
36
|
+
if total_length > allowed_length
|
|
37
|
+
raise ArgumentError, "wrong number of attributes (given #{total_length}, expected 0..#{allowed_length})"
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
args.each_with_index do |value, i|
|
|
37
41
|
attributes[self.class.attributes_list[i]] = value
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
kwargs.each do |name, value|
|
|
41
45
|
raise(ArgumentError, "attribute #{name} has already been provided as parameter") if attributes.key?(name)
|
|
42
46
|
|
|
43
47
|
attributes[name] = value
|
|
@@ -46,7 +50,7 @@ module Zen
|
|
|
46
50
|
attributes
|
|
47
51
|
end
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
def assert_valid_attributes!(actual)
|
|
50
54
|
unexpected = actual.keys - self.class.attributes_list
|
|
51
55
|
|
|
52
56
|
raise(ArgumentError, "wrong attributes #{unexpected} given") if unexpected.any?
|
|
@@ -58,7 +62,7 @@ module Zen
|
|
|
58
62
|
def inherited(service_class)
|
|
59
63
|
service_class.const_set(:AttributeMethods, Module.new)
|
|
60
64
|
service_class.send(:include, service_class::AttributeMethods)
|
|
61
|
-
service_class.attributes_list.replace
|
|
65
|
+
service_class.attributes_list.replace(attributes_list.dup)
|
|
62
66
|
super
|
|
63
67
|
end
|
|
64
68
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Zen
|
|
6
|
+
module Service::Plugins
|
|
7
|
+
module Callable
|
|
8
|
+
extend Plugin
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
# No-op by default
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
def call(...)
|
|
16
|
+
new(...).call
|
|
17
|
+
end
|
|
18
|
+
alias [] call
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Zen
|
|
6
|
+
module Service::Plugins
|
|
7
|
+
module PersistedResult
|
|
8
|
+
extend Plugin
|
|
9
|
+
|
|
10
|
+
module Extension
|
|
11
|
+
def call
|
|
12
|
+
@result = super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :result
|
|
17
|
+
|
|
18
|
+
def initialize(*, **)
|
|
19
|
+
super
|
|
20
|
+
extend(Extension)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def called?
|
|
24
|
+
defined?(@result)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Zen
|
|
4
4
|
module Service::Plugins
|
|
5
5
|
module Pluggable
|
|
6
|
+
Reflection = Struct.new(:extension, :options)
|
|
7
|
+
|
|
6
8
|
def use(name, **opts, &block)
|
|
7
9
|
extension = Service::Plugins.fetch(name)
|
|
8
10
|
|
|
@@ -17,18 +19,6 @@ module Zen
|
|
|
17
19
|
use_extension(extension, name, **opts, &block)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
private def use_extension(extension, name, **opts, &block)
|
|
21
|
-
include extension
|
|
22
|
-
extend extension::ClassMethods if extension.const_defined?(:ClassMethods)
|
|
23
|
-
|
|
24
|
-
extension.used(self, **opts, &block) if extension.respond_to?(:used)
|
|
25
|
-
extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
|
|
26
|
-
|
|
27
|
-
plugins[name] = Reflection.new(extension, opts.merge(block: block))
|
|
28
|
-
|
|
29
|
-
extension
|
|
30
|
-
end
|
|
31
|
-
|
|
32
22
|
def using?(name)
|
|
33
23
|
plugins.key?(name)
|
|
34
24
|
end
|
|
@@ -38,7 +28,19 @@ module Zen
|
|
|
38
28
|
end
|
|
39
29
|
alias extensions plugins
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def use_extension(extension, name, **opts, &block)
|
|
34
|
+
include extension
|
|
35
|
+
extend extension::ClassMethods if extension.const_defined?(:ClassMethods)
|
|
36
|
+
|
|
37
|
+
extension.used(self, **opts, &block) if extension.respond_to?(:used)
|
|
38
|
+
extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
|
|
39
|
+
|
|
40
|
+
plugins[name] = Reflection.new(extension, opts.merge(block:))
|
|
41
|
+
|
|
42
|
+
extension
|
|
43
|
+
end
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
|
|
5
|
+
module Zen
|
|
6
|
+
module Service::Plugins
|
|
7
|
+
module ResultYielding
|
|
8
|
+
extend Plugin
|
|
9
|
+
|
|
10
|
+
module Extension
|
|
11
|
+
def call
|
|
12
|
+
return super unless block_given?
|
|
13
|
+
|
|
14
|
+
result = nil
|
|
15
|
+
super do
|
|
16
|
+
result = yield
|
|
17
|
+
end
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.used(service_class)
|
|
23
|
+
service_class.prepend(Extension)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/zen/service/plugins.rb
CHANGED
|
@@ -26,13 +26,8 @@ module Zen
|
|
|
26
26
|
|
|
27
27
|
require_relative "plugins/plugin"
|
|
28
28
|
require_relative "plugins/pluggable"
|
|
29
|
-
require_relative "plugins/
|
|
29
|
+
require_relative "plugins/callable"
|
|
30
30
|
require_relative "plugins/attributes"
|
|
31
|
-
require_relative "plugins/
|
|
32
|
-
require_relative "plugins/
|
|
33
|
-
require_relative "plugins/execution_cache"
|
|
34
|
-
require_relative "plugins/policies"
|
|
35
|
-
require_relative "plugins/rescue"
|
|
36
|
-
require_relative "plugins/status"
|
|
37
|
-
require_relative "plugins/validation"
|
|
31
|
+
require_relative "plugins/persisted_result"
|
|
32
|
+
require_relative "plugins/result_yielding"
|
|
38
33
|
end
|
data/lib/zen/service/version.rb
CHANGED
data/lib/zen/service.rb
CHANGED
data/zen-service.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = "Flexible and highly extensible Services for business logic organization"
|
|
13
13
|
spec.homepage = "https://github.com/akuzko/zen-service"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
16
16
|
|
|
17
17
|
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
|
|
18
18
|
|
|
@@ -33,5 +33,5 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
34
34
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
35
35
|
spec.add_development_dependency "rspec-its", "~> 1.2"
|
|
36
|
-
spec.add_development_dependency "rubocop", "~>
|
|
36
|
+
spec.add_development_dependency "rubocop", "~> 1.81"
|
|
37
37
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zen-service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artem Kuzko
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-12-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pry
|
|
@@ -86,14 +86,14 @@ dependencies:
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
89
|
+
version: '1.81'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
96
|
+
version: '1.81'
|
|
97
97
|
description: Flexible and highly extensible Services for business logic organization
|
|
98
98
|
email:
|
|
99
99
|
- a.kuzko@gmail.com
|
|
@@ -104,7 +104,7 @@ files:
|
|
|
104
104
|
- ".gitignore"
|
|
105
105
|
- ".rspec"
|
|
106
106
|
- ".rubocop.yml"
|
|
107
|
-
- ".
|
|
107
|
+
- ".tool-versions"
|
|
108
108
|
- Gemfile
|
|
109
109
|
- LICENSE.txt
|
|
110
110
|
- README.md
|
|
@@ -113,18 +113,12 @@ files:
|
|
|
113
113
|
- bin/setup
|
|
114
114
|
- lib/zen/service.rb
|
|
115
115
|
- lib/zen/service/plugins.rb
|
|
116
|
-
- lib/zen/service/plugins/assertions.rb
|
|
117
116
|
- lib/zen/service/plugins/attributes.rb
|
|
118
|
-
- lib/zen/service/plugins/
|
|
119
|
-
- lib/zen/service/plugins/
|
|
120
|
-
- lib/zen/service/plugins/execution_cache.rb
|
|
117
|
+
- lib/zen/service/plugins/callable.rb
|
|
118
|
+
- lib/zen/service/plugins/persisted_result.rb
|
|
121
119
|
- lib/zen/service/plugins/pluggable.rb
|
|
122
120
|
- lib/zen/service/plugins/plugin.rb
|
|
123
|
-
- lib/zen/service/plugins/
|
|
124
|
-
- lib/zen/service/plugins/rescue.rb
|
|
125
|
-
- lib/zen/service/plugins/status.rb
|
|
126
|
-
- lib/zen/service/plugins/validation.rb
|
|
127
|
-
- lib/zen/service/spec_helpers.rb
|
|
121
|
+
- lib/zen/service/plugins/result_yielding.rb
|
|
128
122
|
- lib/zen/service/version.rb
|
|
129
123
|
- zen-service.gemspec
|
|
130
124
|
homepage: https://github.com/akuzko/zen-service
|
|
@@ -142,7 +136,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
142
136
|
requirements:
|
|
143
137
|
- - ">="
|
|
144
138
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: 2.
|
|
139
|
+
version: 3.2.0
|
|
146
140
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
141
|
requirements:
|
|
148
142
|
- - ">="
|
data/.travis.yml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::Plugins
|
|
5
|
-
module Context
|
|
6
|
-
extend Plugin
|
|
7
|
-
|
|
8
|
-
attr_accessor :local_context
|
|
9
|
-
protected :local_context, :local_context=
|
|
10
|
-
|
|
11
|
-
def context
|
|
12
|
-
global_context = ::Zen::Service.context
|
|
13
|
-
return global_context if local_context.nil?
|
|
14
|
-
|
|
15
|
-
if global_context.respond_to?(:merge)
|
|
16
|
-
global_context.merge(local_context)
|
|
17
|
-
else
|
|
18
|
-
local_context
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def with_context(ctx)
|
|
23
|
-
clone.tap do |copy|
|
|
24
|
-
copy.local_context =
|
|
25
|
-
copy.local_context.respond_to?(:merge) ? copy.local_context.merge(ctx) : ctx
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def execute(*)
|
|
30
|
-
::Zen::Service.with_context(context) do
|
|
31
|
-
super
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
module ServiceMethods
|
|
36
|
-
def with_context(ctx)
|
|
37
|
-
current = context
|
|
38
|
-
Thread.current[:zen_service_context] = context.respond_to?(:merge) ? context.merge(ctx) : ctx
|
|
39
|
-
|
|
40
|
-
yield
|
|
41
|
-
ensure
|
|
42
|
-
Thread.current[:zen_service_context] = current
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def context
|
|
46
|
-
Thread.current[:zen_service_context]
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
service_extension ServiceMethods
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ostruct"
|
|
4
|
-
|
|
5
|
-
module Zen
|
|
6
|
-
module Service::Plugins
|
|
7
|
-
module Executable
|
|
8
|
-
extend Plugin
|
|
9
|
-
|
|
10
|
-
class State
|
|
11
|
-
def self.prop_names
|
|
12
|
-
@prop_names ||= []
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.add_prop(*props)
|
|
16
|
-
prop_names.push(*props)
|
|
17
|
-
props.each { |prop| def_prop_accessor(prop) }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.def_prop_accessor(name)
|
|
21
|
-
define_method(name) { @values[name] }
|
|
22
|
-
define_method("#{name}=") { |value| @values[name] = value }
|
|
23
|
-
define_method("has_#{name}?") { @values.key?(name) }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def initialize(values = {})
|
|
27
|
-
@values = values
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def clear!
|
|
31
|
-
@values.clear
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def prop_names
|
|
35
|
-
self.class.prop_names
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def replace(other)
|
|
39
|
-
missing_props = prop_names - other.prop_names
|
|
40
|
-
|
|
41
|
-
unless missing_props.empty?
|
|
42
|
-
raise ArgumentError, "cannot accept execution state #{other} due to missing props: #{missing_props}"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
prop_names.each do |prop|
|
|
46
|
-
@values[prop] = other.public_send(prop)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.used(service_class, *)
|
|
52
|
-
service_class.const_set(:State, Class.new(State))
|
|
53
|
-
service_class.add_execution_prop(:executed, :success, :result)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
attr_reader :state
|
|
57
|
-
|
|
58
|
-
def initialize(*)
|
|
59
|
-
@state = self.class::State.new(executed: false)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def initialize_clone(*)
|
|
63
|
-
clear_execution_state!
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def execute(*, &block)
|
|
67
|
-
clear_execution_state!
|
|
68
|
-
result = execute!(&block)
|
|
69
|
-
result_with(result) unless state.has_result?
|
|
70
|
-
state.executed = true
|
|
71
|
-
|
|
72
|
-
self
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def executed?
|
|
76
|
-
state.executed
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def ~@
|
|
80
|
-
state
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
private def execute!
|
|
84
|
-
success!
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
private def clear_execution_state!
|
|
88
|
-
state.clear!
|
|
89
|
-
state.executed = false
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
private def success(**)
|
|
93
|
-
assign_successful_state
|
|
94
|
-
assign_successful_result(yield)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
private def failure(**)
|
|
98
|
-
assign_failed_state
|
|
99
|
-
assign_failed_result(yield)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private def success!(**)
|
|
103
|
-
assign_successful_state
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
private def failure!(**)
|
|
107
|
-
assign_failed_state
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
private def assign_successful_state
|
|
111
|
-
state.success = true
|
|
112
|
-
state.result = nil
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
private def assign_failed_state
|
|
116
|
-
state.success = false
|
|
117
|
-
state.result = nil
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private def assign_successful_result(value)
|
|
121
|
-
state.result = value
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private def assign_failed_result(value)
|
|
125
|
-
state.result = value
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def result
|
|
129
|
-
return state.result unless block_given?
|
|
130
|
-
|
|
131
|
-
result_with(yield)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
private def result_with(obj)
|
|
135
|
-
return state.replace(obj) if obj.is_a?(State)
|
|
136
|
-
|
|
137
|
-
state.success = !!obj
|
|
138
|
-
if state.success
|
|
139
|
-
assign_successful_result(obj)
|
|
140
|
-
else
|
|
141
|
-
assign_failed_result(obj)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def success?
|
|
146
|
-
state.success == true
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def failure?
|
|
150
|
-
!success?
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
module ClassMethods
|
|
154
|
-
def inherited(klass)
|
|
155
|
-
klass.const_set(:State, Class.new(self::State))
|
|
156
|
-
klass::State.prop_names.replace(self::State.prop_names.dup)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def add_execution_prop(*props)
|
|
160
|
-
self::State.add_prop(*props)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def call(*args)
|
|
164
|
-
new(*args).execute
|
|
165
|
-
end
|
|
166
|
-
alias execute call
|
|
167
|
-
|
|
168
|
-
def [](*args)
|
|
169
|
-
call(*args).result
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def method_added(name)
|
|
173
|
-
private :execute! if name == :execute!
|
|
174
|
-
super if defined? super
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::Plugins
|
|
5
|
-
module ExecutionCache
|
|
6
|
-
extend Plugin
|
|
7
|
-
|
|
8
|
-
def initialize(*)
|
|
9
|
-
super
|
|
10
|
-
extend Extension
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
module Extension
|
|
14
|
-
def execute(*)
|
|
15
|
-
return super if block_given? || !executed?
|
|
16
|
-
|
|
17
|
-
self
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::Plugins
|
|
5
|
-
module Policies
|
|
6
|
-
extend Plugin
|
|
7
|
-
|
|
8
|
-
GuardViolationError = Class.new(StandardError)
|
|
9
|
-
|
|
10
|
-
def self.used(service_class, *)
|
|
11
|
-
service_class.partials = []
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private def execute!
|
|
15
|
-
partials.each_with_object({}) do |partial, permissions|
|
|
16
|
-
partial.public_methods(false).grep(/\?$/).each do |action_check|
|
|
17
|
-
key = action_check.to_s[0...-1]
|
|
18
|
-
can = partial.public_send(action_check)
|
|
19
|
-
|
|
20
|
-
permissions[key] = permissions.key?(key) ? permissions[key] && can : can
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def can?(action)
|
|
26
|
-
why_cant?(action).nil?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def guard!(action)
|
|
30
|
-
reason = why_cant?(action)
|
|
31
|
-
|
|
32
|
-
return if reason.nil?
|
|
33
|
-
|
|
34
|
-
raise(reason) if (reason.is_a?(Class) ? reason : reason.class) < Exception
|
|
35
|
-
|
|
36
|
-
raise(GuardViolationError, reason)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def why_cant?(action)
|
|
40
|
-
action_check = "#{action}?"
|
|
41
|
-
|
|
42
|
-
reason =
|
|
43
|
-
partials
|
|
44
|
-
.find { |p| p.respond_to?(action_check) && !p.public_send(action_check) }
|
|
45
|
-
&.class
|
|
46
|
-
&.denial_reason
|
|
47
|
-
|
|
48
|
-
reason.is_a?(Proc) ? instance_exec(&reason) : reason
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private def partials
|
|
52
|
-
@partials ||= self.class.partials.map do |klass|
|
|
53
|
-
klass.from(self)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
module ClassMethods
|
|
58
|
-
attr_accessor :partials, :denial_reason
|
|
59
|
-
|
|
60
|
-
def deny_with(reason, &block)
|
|
61
|
-
partial = Class.new(self, &block)
|
|
62
|
-
partial.denial_reason = reason
|
|
63
|
-
partials << partial
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::Plugins
|
|
5
|
-
module Rescue
|
|
6
|
-
extend Plugin
|
|
7
|
-
|
|
8
|
-
def self.used(service_class, *)
|
|
9
|
-
service_class.use(:status) unless service_class.using?(:status)
|
|
10
|
-
service_class.add_execution_prop(:error)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def execute(**opts)
|
|
14
|
-
rezcue = opts.delete(:rescue)
|
|
15
|
-
super
|
|
16
|
-
rescue StandardError => e
|
|
17
|
-
clear_execution_state!
|
|
18
|
-
failure!(status: :error)
|
|
19
|
-
state.error = e
|
|
20
|
-
raise e unless rezcue
|
|
21
|
-
|
|
22
|
-
self
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def error
|
|
26
|
-
state.error
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def error?
|
|
30
|
-
status == :error
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::Plugins
|
|
5
|
-
module Status
|
|
6
|
-
extend Plugin
|
|
7
|
-
|
|
8
|
-
default_options(success: [], failure: [])
|
|
9
|
-
|
|
10
|
-
def self.used(service_class, **)
|
|
11
|
-
service_class.add_execution_prop(:status)
|
|
12
|
-
|
|
13
|
-
helpers = Module.new
|
|
14
|
-
service_class.const_set(:StatusHelpers, helpers)
|
|
15
|
-
service_class.send(:include, helpers)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.configure(service_class, success:, failure:)
|
|
19
|
-
service_class::StatusHelpers.module_eval do
|
|
20
|
-
success.each do |name|
|
|
21
|
-
define_method(name) do |**opts, &block|
|
|
22
|
-
success(status: name, **opts, &block)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
failure.each do |name|
|
|
27
|
-
define_method(name) do |**opts, &block|
|
|
28
|
-
failure(status: name, **opts, &block)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def status
|
|
35
|
-
state.status
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private def success!(status: :success, **)
|
|
39
|
-
state.status = status
|
|
40
|
-
super
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private def success(status: :success, **)
|
|
44
|
-
state.status = status
|
|
45
|
-
super
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private def failure!(status: :failure, **)
|
|
49
|
-
state.status = status
|
|
50
|
-
super
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private def failure(status: :failure, **)
|
|
54
|
-
super.tap do
|
|
55
|
-
state.status = status
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private def result_with(*)
|
|
60
|
-
super
|
|
61
|
-
state.status ||= state.success ? :success : :failure
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::Plugins
|
|
5
|
-
module Validation
|
|
6
|
-
extend Plugin
|
|
7
|
-
|
|
8
|
-
class Errors < Hash
|
|
9
|
-
def add(key, message)
|
|
10
|
-
(self[key] ||= []).push(message)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
default_options(errors_class: Errors)
|
|
15
|
-
|
|
16
|
-
def self.used(service_class, *)
|
|
17
|
-
service_class.add_execution_prop(:errors)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private def initialize(*)
|
|
21
|
-
super
|
|
22
|
-
state.errors = errors_class.new
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def execute(*)
|
|
26
|
-
return super if valid?
|
|
27
|
-
|
|
28
|
-
failure!(status: :invalid)
|
|
29
|
-
|
|
30
|
-
self
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def errors
|
|
34
|
-
state.errors
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private def errors_class
|
|
38
|
-
self.class.plugins[:validation].options[:errors_class]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private def validate!
|
|
42
|
-
errors.clear
|
|
43
|
-
validate
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def validate; end
|
|
47
|
-
|
|
48
|
-
def valid?
|
|
49
|
-
validate!
|
|
50
|
-
errors.empty?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private def clear_execution_state!
|
|
54
|
-
super
|
|
55
|
-
state.errors = errors_class.new
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Zen
|
|
4
|
-
module Service::SpecHelpers
|
|
5
|
-
def self.included(target)
|
|
6
|
-
target.extend(ClassMethods)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
# Example:
|
|
10
|
-
# stub_service(MyService)
|
|
11
|
-
# .with_atributes(foo: 'foo')
|
|
12
|
-
# .with_stubs(result: 'bar', success: true)
|
|
13
|
-
# .service
|
|
14
|
-
def stub_service(service)
|
|
15
|
-
ServiceMocker.new(self).stub_service(service)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
module ClassMethods
|
|
19
|
-
def service_context(&block)
|
|
20
|
-
around do |example|
|
|
21
|
-
::Zen::Service.with_context(instance_exec(&block)) do
|
|
22
|
-
example.run
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class ServiceMocker < SimpleDelegator
|
|
29
|
-
attr_reader :service_class, :service
|
|
30
|
-
|
|
31
|
-
def stub_service(service_class) # rubocop:disable Metrics/AbcSize
|
|
32
|
-
@service_class = service_class
|
|
33
|
-
@service = double(service_class.name)
|
|
34
|
-
|
|
35
|
-
allow(service_class).to receive(:new).and_return(service)
|
|
36
|
-
allow(service).to receive(:execute).and_return(service)
|
|
37
|
-
allow(service).to receive(:executed?).and_return(true)
|
|
38
|
-
|
|
39
|
-
self
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def with_attributes(*attributes)
|
|
43
|
-
expect(service_class).to receive(:new).with(*attributes).and_return(service)
|
|
44
|
-
|
|
45
|
-
self
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def with_stubs(stubs)
|
|
49
|
-
stubs[:success?] = !!stubs[:result] unless stubs.key?(:success)
|
|
50
|
-
stubs[:failure?] = !stubs[:success?]
|
|
51
|
-
|
|
52
|
-
stubs.each do |name, value|
|
|
53
|
-
allow(service).to receive(name).and_return(value)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
self
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|