excom 0.4.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 +4 -4
- data/README.md +110 -68
- data/bin/console +15 -8
- data/excom.gemspec +3 -3
- data/lib/excom.rb +1 -1
- data/lib/excom/plugins.rb +2 -2
- data/lib/excom/plugins/args.rb +14 -7
- data/lib/excom/plugins/assertions.rb +3 -3
- data/lib/excom/plugins/caching.rb +11 -4
- data/lib/excom/plugins/context.rb +4 -2
- data/lib/excom/plugins/dry_types.rb +6 -10
- data/lib/excom/plugins/errors.rb +38 -0
- data/lib/excom/plugins/executable.rb +112 -59
- data/lib/excom/plugins/failure_cause.rb +16 -0
- data/lib/excom/plugins/pluggable.rb +8 -1
- data/lib/excom/plugins/rescue.rb +9 -7
- data/lib/excom/plugins/sentry.rb +10 -9
- data/lib/excom/plugins/sentry/sentinel.rb +15 -15
- data/lib/excom/plugins/status.rb +57 -0
- data/lib/excom/{command.rb → service.rb} +1 -1
- data/lib/excom/version.rb +1 -1
- metadata +10 -9
- data/lib/excom/plugins/status_helpers.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20f76b20a337404d2adaa57942d2939e15c7af7d
|
4
|
+
data.tar.gz: 12b63ebcbc418abf3065ac971a94b2c1242411e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89f21c5133b8e03630d339465f05dfc90854fef955d0ec71c0bb22c22c626494a2f314e257e961726a622a273ee5117e7253ae4395a7d33d4e8ad684ee95533e
|
7
|
+
data.tar.gz: 7e7bb287d15468f03481e4b49cc2a7897ec5b63675e2a918965c758b896f50b3498c04292581c49122433b99b8a008b9d586888d4f3a54de8dd5b751e0dd32ad
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Excom
|
2
2
|
|
3
|
-
Flexible and highly extensible
|
3
|
+
Flexible and highly extensible Service Objects for business logic organization.
|
4
4
|
|
5
5
|
[](http://travis-ci.org/akuzko/excom)
|
6
6
|
[](https://github.com/akuzko/excom/releases)
|
@@ -21,26 +21,35 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
$ gem install excom
|
23
23
|
|
24
|
+
## Preface
|
25
|
+
|
26
|
+
`Excom` stands for **Ex**excutable **Com**and. Initially, `Excom::Command` was the main
|
27
|
+
class, provided by the gem. But it seems that "Service" name become more popular and
|
28
|
+
common for describing classes for business logic, so it was renamed in this gem too.
|
29
|
+
|
24
30
|
## Usage
|
25
31
|
|
26
|
-
General idea behind every `excom`
|
27
|
-
options (named arguments), and should define `
|
28
|
-
|
32
|
+
General idea behind every `excom` service is simple: each service can have arguments,
|
33
|
+
options (named arguments), and should define `execute!` method that is called during
|
34
|
+
service execution. Executed service has `status` and `result`.
|
29
35
|
|
30
|
-
The
|
36
|
+
The very basic usage of `Excom` services can be shown with following example:
|
31
37
|
|
32
38
|
```rb
|
33
|
-
# app/
|
39
|
+
# app/services/todos/update.rb
|
34
40
|
module Todos
|
35
|
-
class Update < Excom::
|
41
|
+
class Update < Excom::Service
|
42
|
+
# `use` class method adds a plugin to a service with specified options
|
43
|
+
use :status, success: [:ok], failure: [:unprocessable_entity]
|
44
|
+
|
36
45
|
args :todo
|
37
46
|
opts :params
|
38
47
|
|
39
|
-
def
|
48
|
+
def execute!
|
40
49
|
if todo.update(params)
|
41
|
-
|
50
|
+
ok todo.as_json
|
42
51
|
else
|
43
|
-
|
52
|
+
unprocessable_entity todo.errors
|
44
53
|
end
|
45
54
|
end
|
46
55
|
end
|
@@ -49,34 +58,30 @@ end
|
|
49
58
|
# app/controllers/todos/controller
|
50
59
|
class TodosController < ApplicationController
|
51
60
|
def update
|
52
|
-
|
61
|
+
service = Todos::Update.(todo, params: todo_params)
|
53
62
|
|
54
|
-
|
55
|
-
render json: todo.result
|
56
|
-
else
|
57
|
-
render json: todo.result, status: :unprocessable_entity
|
58
|
-
end
|
63
|
+
render json: todo.result, status: service.status
|
59
64
|
end
|
60
65
|
end
|
61
66
|
```
|
62
67
|
|
63
68
|
However, even this basic example can be highly optimized by using Excom extensions and helper methods.
|
64
69
|
|
65
|
-
###
|
70
|
+
### Service arguments and options
|
66
71
|
|
67
|
-
Read full version on [wiki](https://github.com/akuzko/excom/wiki#instantiating-
|
72
|
+
Read full version on [wiki](https://github.com/akuzko/excom/wiki#instantiating-service-with-arguments-and-options).
|
68
73
|
|
69
|
-
Excom
|
74
|
+
Excom services can be initialized with _arguments_ and _options_ (named arguments). To specify list
|
70
75
|
of available arguments and options, use `args` and `opts` class methods. All arguments and options
|
71
|
-
are optional during
|
76
|
+
are optional during service initialization. However, you cannot pass more arguments to service or
|
72
77
|
options that were not declared with `opts` method.
|
73
78
|
|
74
79
|
```rb
|
75
|
-
class
|
80
|
+
class MyService < Excom::Service
|
76
81
|
args :foo
|
77
82
|
opts :bar
|
78
83
|
|
79
|
-
def
|
84
|
+
def execute!
|
80
85
|
# do something
|
81
86
|
end
|
82
87
|
|
@@ -85,84 +90,91 @@ class MyCommand < Excom::Command
|
|
85
90
|
end
|
86
91
|
end
|
87
92
|
|
88
|
-
|
89
|
-
|
90
|
-
|
93
|
+
s1 = MyService.new
|
94
|
+
s1.foo # => 5
|
95
|
+
s1.bar # => nil
|
91
96
|
|
92
|
-
|
93
|
-
|
94
|
-
|
97
|
+
s2 = s1.with_args(1).with_opts(bar: 2)
|
98
|
+
s2.foo # => 1
|
99
|
+
s2.bar # => 2
|
95
100
|
```
|
96
101
|
|
97
|
-
###
|
102
|
+
### Service Execution
|
98
103
|
|
99
|
-
Read full version on [wiki](https://github.com/akuzko/excom/wiki#
|
104
|
+
Read full version on [wiki](https://github.com/akuzko/excom/wiki#service-execution).
|
100
105
|
|
101
|
-
At the core of each
|
102
|
-
`result` methods to set execution status and result. If none
|
103
|
-
will be set based on `
|
106
|
+
At the core of each service's execution lies `execute!` method. By default, you can use
|
107
|
+
`success`, `failure` and `result` methods to set execution status and result. If none
|
108
|
+
were used, result and status will be set based on `execute!` method's return value.
|
104
109
|
|
105
110
|
Example:
|
106
111
|
|
107
112
|
```rb
|
108
|
-
class
|
109
|
-
alias_success :ok
|
113
|
+
class MyService < Excom::Service
|
110
114
|
args :foo
|
111
115
|
|
112
|
-
def
|
116
|
+
def execute!
|
113
117
|
if foo > 2
|
114
|
-
|
118
|
+
success { foo * 2 }
|
115
119
|
else
|
116
|
-
|
120
|
+
failure { -1 }
|
117
121
|
end
|
118
122
|
end
|
119
123
|
end
|
120
124
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
command.result # => 6
|
125
|
+
service = MyService.new(3)
|
126
|
+
service.execute.success? # => true
|
127
|
+
service.result # => 6
|
125
128
|
```
|
126
129
|
|
127
130
|
### Core API
|
128
131
|
|
129
132
|
Please read about core API and available class and instance methods on [wiki](https://github.com/akuzko/excom/wiki#core-api)
|
130
133
|
|
131
|
-
###
|
134
|
+
### Service Extensions (Plugins)
|
132
135
|
|
133
136
|
Read full version on [wiki](https://github.com/akuzko/excom/wiki/Plugins).
|
134
137
|
|
135
138
|
Excom is built with extensions in mind. Even core functionality is organized in plugins that are
|
136
|
-
used in base `Excom::
|
139
|
+
used in base `Excom::Service` class. Bellow you can see a list of plugins with some description
|
137
140
|
and examples that are shipped with `excom`:
|
138
141
|
|
139
|
-
- [`:
|
140
|
-
|
141
|
-
|
142
|
+
- [`:status`](https://github.com/akuzko/excom/wiki/Plugins#status) - Adds `status` execution state
|
143
|
+
property to the service, as well as helper methods and behavior to set it. `status` property is not
|
144
|
+
bound to the "success" flag of execution state and can have any value depending on your needs. It
|
145
|
+
is up to you to setup which statuses correspond to successful execution and which are not. Generated
|
146
|
+
status helper methods allow to atomically and more explicitly assign both status and result at
|
147
|
+
the same time:
|
142
148
|
|
143
149
|
```rb
|
144
|
-
class
|
145
|
-
use :
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
150
|
+
class Posts::Update < Excom::Service
|
151
|
+
use :status,
|
152
|
+
success: [:ok],
|
153
|
+
failure: [:unprocessable_entity]
|
154
|
+
|
155
|
+
args :post, :params
|
156
|
+
|
157
|
+
def execute!
|
158
|
+
if post.update(params)
|
159
|
+
ok post.as_json
|
151
160
|
else
|
152
|
-
unprocessable_entity
|
161
|
+
unprocessable_entity post.errors
|
153
162
|
end
|
154
163
|
end
|
155
164
|
end
|
156
165
|
|
157
|
-
|
166
|
+
service = Posts::Update.(post, post_params)
|
158
167
|
# in case params were valid you will have:
|
159
|
-
|
160
|
-
|
161
|
-
|
168
|
+
service.success? # => true
|
169
|
+
service.status # => :ok
|
170
|
+
service.result # => {'id' => 1, ...}
|
162
171
|
```
|
163
172
|
|
173
|
+
Note that unlike `success`, `failure`, or `result` methods, status helpers accept result value
|
174
|
+
as its argument rather than yield to a block to get it.
|
175
|
+
|
164
176
|
- [`:context`](https://github.com/akuzko/excom/wiki/Plugins#context) - Allows you to set an execution
|
165
|
-
context for a block that will be available to any
|
177
|
+
context for a block that will be available to any service that uses this plugin via `context` method.
|
166
178
|
|
167
179
|
```rb
|
168
180
|
# application_controller.rb
|
@@ -176,30 +188,30 @@ end
|
|
176
188
|
```
|
177
189
|
|
178
190
|
```rb
|
179
|
-
class Posts::Archive < Excom::
|
191
|
+
class Posts::Archive < Excom::Service
|
180
192
|
use :context
|
181
193
|
args :post
|
182
194
|
|
183
|
-
def
|
195
|
+
def execute!
|
184
196
|
post.update(archived: true, archived_by: context[:current_user])
|
185
197
|
end
|
186
198
|
end
|
187
199
|
```
|
188
200
|
|
189
201
|
- [`:sentry`](https://github.com/akuzko/excom/wiki/Plugins#sentry) - Allows you to define sentry logic that
|
190
|
-
will allow or deny
|
202
|
+
will allow or deny service's execution or other related checks. This logic can be defined inline in service
|
191
203
|
classes or in dedicated Sentry classes. Much like [pundit](https://github.com/elabs/pundit) Policies, but
|
192
204
|
more. Where pundit governs only authorization logic, Excom's Sentries can deny execution with any reason
|
193
205
|
you find appropriate.
|
194
206
|
|
195
207
|
```rb
|
196
|
-
class Posts::Destroy < Excom::
|
208
|
+
class Posts::Destroy < Excom::Service
|
197
209
|
use :context
|
198
210
|
use :sentry
|
199
211
|
|
200
212
|
args :post
|
201
213
|
|
202
|
-
def
|
214
|
+
def execute!
|
203
215
|
post.destroy
|
204
216
|
end
|
205
217
|
|
@@ -222,16 +234,46 @@ end
|
|
222
234
|
```
|
223
235
|
|
224
236
|
- [`:assertions`](https://github.com/akuzko/excom/wiki/Plugins#assertions) - Provides `assert` method that
|
225
|
-
can be used for different logic checks during
|
237
|
+
can be used for different logic checks during service execution.
|
238
|
+
|
239
|
+
- [`:failure_cause`](https://github.com/akuzko/excom/wiki/Plugins#failure_cause) - A small helper plugin
|
240
|
+
that can be used to more explicit access to cause of service failure. You can use it if you feel that
|
241
|
+
failed service shouldn't have a result, but a cause of the failure instead. Example:
|
242
|
+
|
243
|
+
```rb
|
244
|
+
class Posts::Create < Excom::Service
|
245
|
+
use :status, success: [:ok], failure: [:unprocessable_entity]
|
246
|
+
use :failure_cause, cause_method_name: :errors
|
247
|
+
|
248
|
+
args :params
|
249
|
+
|
250
|
+
def execute!
|
251
|
+
if post.save
|
252
|
+
ok post.as_json
|
253
|
+
else
|
254
|
+
unprocessable_entity post.errors
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private def post
|
259
|
+
@post ||= Post.new(params)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
service = Posts::Create.(title: 'invalid')
|
264
|
+
service.success? # => false
|
265
|
+
service.result # => nil
|
266
|
+
service.errors # => {title: ["is invalid"]}
|
267
|
+
```
|
226
268
|
|
227
269
|
- [`:dry_types`](https://github.com/akuzko/excom/wiki/Plugins#dry-types) - Allows you to use
|
228
270
|
[dry-types](http://dry-rb.org/gems/dry-types/) attributes instead of default `args` and `opts`.
|
229
271
|
|
230
272
|
- [`:caching`](https://github.com/akuzko/excom/wiki/Plugins#caching) - Simple plugin that will prevent
|
231
|
-
re-execution of
|
273
|
+
re-execution of service if it already has been executed, and will immediately return result.
|
232
274
|
|
233
275
|
- [`:rescue`](https://github.com/akuzko/excom/wiki/Plugins#rescue) - Provides `:rescue` execution option.
|
234
|
-
If set to `true`, any error occurred during
|
276
|
+
If set to `true`, any error occurred during service execution will not be raised outside.
|
235
277
|
|
236
278
|
## Development
|
237
279
|
|
data/bin/console
CHANGED
@@ -5,14 +5,14 @@ require "excom"
|
|
5
5
|
|
6
6
|
Excom::Sentry.deny_with :unauthorized
|
7
7
|
|
8
|
-
class Show < Excom::
|
8
|
+
class Show < Excom::Service
|
9
9
|
use :sentry, class: 'MySentry'
|
10
10
|
use :caching
|
11
11
|
use :dry_types
|
12
12
|
|
13
|
-
attribute :foo, Dry::Types['
|
13
|
+
attribute :foo, Dry::Types['integer']
|
14
14
|
|
15
|
-
def
|
15
|
+
def execute!
|
16
16
|
{foo: foo}
|
17
17
|
end
|
18
18
|
end
|
@@ -27,22 +27,29 @@ class MySentry < Excom::Sentry
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
class
|
30
|
+
class MyErrors < Hash
|
31
|
+
def add(key, message)
|
32
|
+
(self[key] ||= []) << message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Save < Excom::Service
|
37
|
+
use :status
|
31
38
|
use :sentry, delegate: [:threshold]
|
32
39
|
use :assertions
|
40
|
+
use :errors, errors_class: MyErrors
|
33
41
|
|
34
42
|
args :foo
|
35
43
|
opts :bar, :baz
|
36
44
|
|
37
|
-
alias_success :ok
|
38
|
-
|
39
45
|
def foo
|
40
46
|
super || 6
|
41
47
|
end
|
42
48
|
|
43
|
-
def
|
44
|
-
result
|
49
|
+
def execute!
|
50
|
+
result { foo * 2 }
|
45
51
|
assert { foo > bar }
|
52
|
+
errors.add(:foo, 'invalid') if foo > 5
|
46
53
|
end
|
47
54
|
|
48
55
|
def threshold
|
data/excom.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Artem Kuzko"]
|
10
10
|
spec.email = ["a.kuzko@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{Flexible and highly extensible
|
13
|
-
spec.description = %q{Flexible and highly extensible
|
12
|
+
spec.summary = %q{Flexible and highly extensible Services for business logic organization}
|
13
|
+
spec.description = %q{Flexible and highly extensible Services for business logic organization}
|
14
14
|
spec.homepage = "https://github.com/akuzko/excom"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.11"
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
-
spec.add_development_dependency "dry-types", "~> 0.
|
26
|
+
spec.add_development_dependency "dry-types", "~> 0.13.0"
|
27
27
|
spec.add_development_dependency "dry-struct", "~> 0.4"
|
28
28
|
spec.add_development_dependency "rspec", "~> 3.0"
|
29
29
|
spec.add_development_dependency "rspec-its", "~> 1.2"
|
data/lib/excom.rb
CHANGED
data/lib/excom/plugins.rb
CHANGED
@@ -10,12 +10,12 @@ module Excom
|
|
10
10
|
def fetch(name)
|
11
11
|
require("excom/plugins/#{name}") unless plugins.key?(name)
|
12
12
|
|
13
|
-
plugins[name] ||
|
13
|
+
plugins[name] || raise("extension `#{name}` is not registered")
|
14
14
|
end
|
15
15
|
|
16
16
|
def register(name, extension, options = {})
|
17
17
|
if plugins.key?(name)
|
18
|
-
|
18
|
+
raise ArgumentError, "extension `#{name}` is already registered"
|
19
19
|
end
|
20
20
|
extension.singleton_class.send(:define_method, :excom_options) { options }
|
21
21
|
plugins[name] = extension
|
data/lib/excom/plugins/args.rb
CHANGED
@@ -10,6 +10,8 @@ module Excom
|
|
10
10
|
|
11
11
|
@args = args
|
12
12
|
@opts = opts
|
13
|
+
|
14
|
+
super()
|
13
15
|
end
|
14
16
|
|
15
17
|
def initialize_clone(*)
|
@@ -54,7 +56,7 @@ module Excom
|
|
54
56
|
allowed = self.class.args_list.length
|
55
57
|
|
56
58
|
if actual.length > allowed
|
57
|
-
|
59
|
+
raise ArgumentError, "wrong number of args (given #{actual.length}, expected 0..#{allowed})"
|
58
60
|
end
|
59
61
|
end
|
60
62
|
|
@@ -62,16 +64,17 @@ module Excom
|
|
62
64
|
unexpected = actual.keys - self.class.opts_list
|
63
65
|
|
64
66
|
if unexpected.any?
|
65
|
-
|
67
|
+
raise ArgumentError, "wrong opts #{unexpected} given"
|
66
68
|
end
|
67
69
|
end
|
68
70
|
|
69
71
|
module ClassMethods
|
70
|
-
def inherited(
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
def inherited(service_class)
|
73
|
+
service_class.const_set(:ArgMethods, Module.new)
|
74
|
+
service_class.send(:include, service_class::ArgMethods)
|
75
|
+
service_class.args_list.replace args_list.dup
|
76
|
+
service_class.opts_list.replace opts_list.dup
|
77
|
+
super
|
75
78
|
end
|
76
79
|
|
77
80
|
def arg_methods
|
@@ -83,6 +86,8 @@ module Excom
|
|
83
86
|
|
84
87
|
argz.each_with_index do |name, i|
|
85
88
|
arg_methods.send(:define_method, name){ @args[i] }
|
89
|
+
|
90
|
+
arg_methods.send(:define_method, "#{name}?"){ !!@args[i] }
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
@@ -91,6 +96,8 @@ module Excom
|
|
91
96
|
|
92
97
|
optz.each do |name|
|
93
98
|
arg_methods.send(:define_method, name){ @opts[name] }
|
99
|
+
|
100
|
+
arg_methods.send(:define_method, "#{name}?"){ !!@opts[name] }
|
94
101
|
end
|
95
102
|
end
|
96
103
|
|
@@ -2,11 +2,11 @@ module Excom
|
|
2
2
|
module Plugins::Assertions
|
3
3
|
Plugins.register :assertions, self
|
4
4
|
|
5
|
-
def assert
|
5
|
+
def assert
|
6
6
|
if yield
|
7
|
-
|
7
|
+
success! unless state.has_success?
|
8
8
|
else
|
9
|
-
failure!
|
9
|
+
failure!
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -1,11 +1,18 @@
|
|
1
1
|
module Excom
|
2
2
|
module Plugins::Caching
|
3
|
-
Plugins.register :caching, self
|
3
|
+
Plugins.register :caching, self
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
extend Extension
|
8
|
+
end
|
9
|
+
|
10
|
+
module Extension
|
11
|
+
def execute(*)
|
12
|
+
return super if block_given? || !executed?
|
7
13
|
|
8
|
-
|
14
|
+
self
|
15
|
+
end
|
9
16
|
end
|
10
17
|
end
|
11
18
|
end
|
@@ -5,11 +5,11 @@ module Excom
|
|
5
5
|
attr_accessor :attributes
|
6
6
|
protected :attributes=
|
7
7
|
|
8
|
-
def self.used(
|
8
|
+
def self.used(service_class, *)
|
9
9
|
require 'dry-types'
|
10
10
|
require 'dry-struct'
|
11
11
|
|
12
|
-
|
12
|
+
service_class.const_set(:Attributes, Class.new(Dry::Struct))
|
13
13
|
end
|
14
14
|
|
15
15
|
def initialize(attrs)
|
@@ -41,30 +41,26 @@ module Excom
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def with_args(*)
|
44
|
-
|
44
|
+
raise("`with_args' method is not available with :dry_types plugin. use `with_attributes' method instead")
|
45
45
|
end
|
46
46
|
|
47
47
|
def with_opts(*)
|
48
|
-
|
48
|
+
raise("`with_opts' method is not available with :dry_types plugin. use `with_attributes' method instead")
|
49
49
|
end
|
50
50
|
|
51
51
|
module ClassMethods
|
52
52
|
def args(*)
|
53
|
-
|
53
|
+
raise("`args' method is not available with :dry_types plugin. use `attribute' method instead")
|
54
54
|
end
|
55
55
|
|
56
56
|
def opts(*)
|
57
|
-
|
57
|
+
raise("`args' method is not available with :dry_types plugin. use `attribute' method instead")
|
58
58
|
end
|
59
59
|
|
60
60
|
def attribute(name, *args)
|
61
61
|
const_get(:Attributes).send(:attribute, name, *args)
|
62
62
|
arg_methods.send(:define_method, name){ @attributes.send(name) }
|
63
63
|
end
|
64
|
-
|
65
|
-
def constructor_type(*args)
|
66
|
-
const_get(:Attributes).send(:constructor_type, *args)
|
67
|
-
end
|
68
64
|
end
|
69
65
|
end
|
70
66
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Excom
|
2
|
+
module Plugins::Errors
|
3
|
+
Plugins.register :errors, self,
|
4
|
+
default_options: {errors_class: Hash, fail_if_present: true}
|
5
|
+
|
6
|
+
def self.used(service_class, *)
|
7
|
+
service_class.add_execution_prop(:errors)
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(*)
|
11
|
+
super
|
12
|
+
|
13
|
+
if self.class.plugins[:errors].options[:fail_if_present] && !errors.empty?
|
14
|
+
failure!
|
15
|
+
end
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
private def initialize(*)
|
21
|
+
super
|
22
|
+
state.errors = errors_class.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def errors
|
26
|
+
state.errors
|
27
|
+
end
|
28
|
+
|
29
|
+
private def errors_class
|
30
|
+
self.class.plugins[:errors].options[:errors_class]
|
31
|
+
end
|
32
|
+
|
33
|
+
private def clear_execution_state!
|
34
|
+
super
|
35
|
+
state.errors = errors_class.new
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,12 +1,59 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
1
3
|
module Excom
|
2
4
|
module Plugins::Executable
|
3
5
|
Plugins.register :executable, self
|
4
6
|
|
5
|
-
|
7
|
+
class State
|
8
|
+
def self.prop_names
|
9
|
+
@prop_names ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.add_prop(*props)
|
13
|
+
prop_names.push(*props)
|
14
|
+
props.each{ |prop| def_prop_accessor(prop) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.def_prop_accessor(name)
|
18
|
+
define_method(name) { @values[name] }
|
19
|
+
define_method("#{name}=") { |value| @values[name] = value }
|
20
|
+
define_method("has_#{name}?") { @values.key?(name) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(values = {})
|
24
|
+
@values = values
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear!
|
28
|
+
@values.clear
|
29
|
+
end
|
30
|
+
|
31
|
+
def prop_names
|
32
|
+
self.class.prop_names
|
33
|
+
end
|
34
|
+
|
35
|
+
def replace(other)
|
36
|
+
missing_props = prop_names - other.prop_names
|
37
|
+
|
38
|
+
unless missing_props.empty?
|
39
|
+
raise ArgumentError, "cannot accept execution state #{other} due to missing props: #{missing_props}"
|
40
|
+
end
|
41
|
+
|
42
|
+
prop_names.each do |prop|
|
43
|
+
@values[prop] = other.public_send(prop)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.used(service_class, *)
|
49
|
+
service_class.const_set(:State, Class.new(State))
|
50
|
+
service_class.add_execution_prop(:executed, :success, :result)
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :state
|
6
54
|
|
7
55
|
def initialize(*)
|
8
|
-
@
|
9
|
-
super
|
56
|
+
@state = self.class::State.new(executed: false)
|
10
57
|
end
|
11
58
|
|
12
59
|
def initialize_clone(*)
|
@@ -15,108 +62,114 @@ module Excom
|
|
15
62
|
|
16
63
|
def execute(*, &block)
|
17
64
|
clear_execution_state!
|
18
|
-
result =
|
19
|
-
result_with(result) unless
|
20
|
-
|
65
|
+
result = execute!(&block)
|
66
|
+
result_with(result) unless state.has_result?
|
67
|
+
state.executed = true
|
21
68
|
|
22
69
|
self
|
23
70
|
end
|
24
71
|
|
25
72
|
def executed?
|
26
|
-
|
73
|
+
state.executed
|
27
74
|
end
|
28
75
|
|
29
76
|
def ~@
|
30
|
-
|
77
|
+
state
|
31
78
|
end
|
32
79
|
|
33
|
-
private def
|
80
|
+
private def execute!
|
34
81
|
success!
|
35
82
|
end
|
36
83
|
|
37
84
|
private def clear_execution_state!
|
38
|
-
|
39
|
-
|
40
|
-
remove_instance_variable('@status'.freeze) if defined?(@status)
|
85
|
+
state.clear!
|
86
|
+
state.executed = false
|
41
87
|
end
|
42
88
|
|
43
|
-
def
|
44
|
-
|
89
|
+
private def success
|
90
|
+
assign_successful_state
|
91
|
+
assign_successful_result(yield)
|
92
|
+
end
|
45
93
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
94
|
+
private def failure
|
95
|
+
assign_failed_state
|
96
|
+
assign_failed_result(yield)
|
97
|
+
end
|
51
98
|
|
52
|
-
|
53
|
-
|
54
|
-
result_with(obj)
|
55
|
-
end
|
99
|
+
private def success!
|
100
|
+
assign_successful_state
|
56
101
|
end
|
57
102
|
|
58
|
-
private def
|
59
|
-
|
60
|
-
|
61
|
-
return @result
|
62
|
-
end
|
103
|
+
private def failure!
|
104
|
+
assign_failed_state
|
105
|
+
end
|
63
106
|
|
64
|
-
|
65
|
-
|
107
|
+
private def assign_successful_state
|
108
|
+
state.success = true
|
109
|
+
state.result = nil
|
66
110
|
end
|
67
111
|
|
68
|
-
def
|
69
|
-
|
112
|
+
private def assign_failed_state
|
113
|
+
state.success = false
|
114
|
+
state.result = nil
|
115
|
+
end
|
70
116
|
|
71
|
-
|
117
|
+
private def assign_successful_result(value)
|
118
|
+
state.result = value
|
72
119
|
end
|
73
120
|
|
74
|
-
def
|
75
|
-
|
121
|
+
private def assign_failed_result(value)
|
122
|
+
state.result = value
|
76
123
|
end
|
77
124
|
|
78
|
-
def
|
79
|
-
|
125
|
+
def result
|
126
|
+
return state.result unless block_given?
|
127
|
+
|
128
|
+
result_with(yield)
|
80
129
|
end
|
81
130
|
|
82
|
-
private def
|
83
|
-
|
84
|
-
|
131
|
+
private def result_with(obj)
|
132
|
+
if State === obj
|
133
|
+
return state.replace(obj)
|
134
|
+
end
|
135
|
+
|
136
|
+
state.success = !!obj
|
137
|
+
if state.success
|
138
|
+
assign_successful_result(obj)
|
139
|
+
else
|
140
|
+
assign_failed_result(obj)
|
141
|
+
end
|
85
142
|
end
|
86
143
|
|
87
|
-
|
88
|
-
|
144
|
+
def success?
|
145
|
+
state.success == true
|
89
146
|
end
|
90
147
|
|
91
|
-
|
92
|
-
|
148
|
+
def failure?
|
149
|
+
!success?
|
93
150
|
end
|
94
151
|
|
95
152
|
module ClassMethods
|
96
|
-
def
|
97
|
-
new(
|
98
|
-
|
99
|
-
|
100
|
-
def [](*args)
|
101
|
-
call(*args).result
|
153
|
+
def inherited(klass)
|
154
|
+
klass.const_set(:State, Class.new(self::State))
|
155
|
+
klass::State.prop_names.replace(self::State.prop_names.dup)
|
102
156
|
end
|
103
157
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
107
|
-
@fail_with = status
|
158
|
+
def add_execution_prop(*props)
|
159
|
+
self::State.add_prop(*props)
|
108
160
|
end
|
109
161
|
|
110
|
-
def
|
111
|
-
|
162
|
+
def call(*args)
|
163
|
+
new(*args).execute
|
112
164
|
end
|
165
|
+
alias_method :execute, :call
|
113
166
|
|
114
|
-
def
|
115
|
-
|
167
|
+
def [](*args)
|
168
|
+
call(*args).result
|
116
169
|
end
|
117
170
|
|
118
171
|
def method_added(name)
|
119
|
-
private :
|
172
|
+
private :execute! if name == :execute!
|
120
173
|
super if defined? super
|
121
174
|
end
|
122
175
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Excom
|
2
|
+
module Plugins::FailureCause
|
3
|
+
Plugins.register :failure_cause, self,
|
4
|
+
default_options: {cause_method_name: :cause}
|
5
|
+
|
6
|
+
def self.used(service_class, cause_method_name:)
|
7
|
+
service_class.add_execution_prop(:cause)
|
8
|
+
service_class.send(:define_method, cause_method_name) { state.cause }
|
9
|
+
end
|
10
|
+
|
11
|
+
private def assign_failed_result(value)
|
12
|
+
state.result = nil
|
13
|
+
state.cause = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -6,6 +6,9 @@ module Excom
|
|
6
6
|
method = extension.excom_options[:use_with] || :include
|
7
7
|
send(method, extension)
|
8
8
|
|
9
|
+
defaults = extension.excom_options[:default_options]
|
10
|
+
opts = defaults.merge(opts) unless defaults.nil?
|
11
|
+
|
9
12
|
if extension.const_defined?('ClassMethods')
|
10
13
|
extend extension::ClassMethods
|
11
14
|
end
|
@@ -19,10 +22,14 @@ module Excom
|
|
19
22
|
extension
|
20
23
|
end
|
21
24
|
|
25
|
+
def using?(name)
|
26
|
+
plugins.key?(name)
|
27
|
+
end
|
28
|
+
|
22
29
|
def plugins
|
23
30
|
@plugins ||= {}
|
24
31
|
end
|
25
|
-
alias
|
32
|
+
alias extensions plugins
|
26
33
|
|
27
34
|
Reflection = Struct.new(:extension, :options)
|
28
35
|
end
|
data/lib/excom/plugins/rescue.rb
CHANGED
@@ -2,11 +2,9 @@ module Excom
|
|
2
2
|
module Plugins::Rescue
|
3
3
|
Plugins.register :rescue, self
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
remove_instance_variable('@error') if defined?(@error)
|
9
|
-
super
|
5
|
+
def self.used(service_class, *)
|
6
|
+
service_class.use(:status) unless service_class.using?(:status)
|
7
|
+
service_class.add_execution_prop :error
|
10
8
|
end
|
11
9
|
|
12
10
|
def execute(**opts)
|
@@ -14,12 +12,16 @@ module Excom
|
|
14
12
|
super
|
15
13
|
rescue StandardError => error
|
16
14
|
clear_execution_state!
|
17
|
-
|
18
|
-
|
15
|
+
failure!(:error)
|
16
|
+
state.error = error
|
19
17
|
raise error unless rezcue
|
20
18
|
self
|
21
19
|
end
|
22
20
|
|
21
|
+
def error
|
22
|
+
state.error
|
23
|
+
end
|
24
|
+
|
23
25
|
def error?
|
24
26
|
status == :error
|
25
27
|
end
|
data/lib/excom/plugins/sentry.rb
CHANGED
@@ -4,14 +4,15 @@ module Excom
|
|
4
4
|
|
5
5
|
Plugins.register :sentry, self
|
6
6
|
|
7
|
-
def self.used(
|
7
|
+
def self.used(service_class, **opts)
|
8
8
|
klass = opts[:class]
|
9
9
|
|
10
|
-
|
10
|
+
service_class.use(:status) unless service_class.using?(:status)
|
11
|
+
service_class._sentry_class = klass if klass
|
11
12
|
end
|
12
13
|
|
13
14
|
def execute(*)
|
14
|
-
reason = why_cant(:execute)
|
15
|
+
reason = why_cant?(:execute)
|
15
16
|
|
16
17
|
return super if reason.nil?
|
17
18
|
|
@@ -21,10 +22,10 @@ module Excom
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def can?(action)
|
24
|
-
why_cant(action).nil?
|
25
|
+
why_cant?(action).nil?
|
25
26
|
end
|
26
27
|
|
27
|
-
def why_cant(action)
|
28
|
+
def why_cant?(action)
|
28
29
|
sentry.denial_reason(action)
|
29
30
|
end
|
30
31
|
|
@@ -39,9 +40,9 @@ module Excom
|
|
39
40
|
module ClassMethods
|
40
41
|
attr_writer :_sentry_class
|
41
42
|
|
42
|
-
def inherited(
|
43
|
+
def inherited(service_class)
|
43
44
|
super
|
44
|
-
|
45
|
+
service_class.sentry_class(_sentry_class)
|
45
46
|
end
|
46
47
|
|
47
48
|
def sentry_class(klass = UNDEFINED)
|
@@ -59,7 +60,7 @@ module Excom
|
|
59
60
|
_sentry_class
|
60
61
|
end
|
61
62
|
|
62
|
-
@sentry_class.
|
63
|
+
@sentry_class.service_class = self
|
63
64
|
@sentry_class
|
64
65
|
end
|
65
66
|
|
@@ -74,7 +75,7 @@ module Excom
|
|
74
75
|
const_get(:Sentry).class_eval(&block)
|
75
76
|
else
|
76
77
|
@_sentry_class = @sentry_class = Class.new(Sentry, &block)
|
77
|
-
@sentry_class.
|
78
|
+
@sentry_class.service_class = self
|
78
79
|
const_set(:Sentry, @_sentry_class)
|
79
80
|
end
|
80
81
|
end
|
@@ -9,13 +9,13 @@ module Excom
|
|
9
9
|
sentry_class.send(:include, sentry_class::Delegations)
|
10
10
|
end
|
11
11
|
|
12
|
-
def self.
|
13
|
-
sentinels.each{ |s| s.
|
14
|
-
@
|
12
|
+
def self.service_class=(klass)
|
13
|
+
sentinels.each{ |s| s.service_class = klass }
|
14
|
+
@service_class = klass
|
15
15
|
end
|
16
16
|
|
17
|
-
def self.
|
18
|
-
@
|
17
|
+
def self.service_class
|
18
|
+
@service_class
|
19
19
|
end
|
20
20
|
|
21
21
|
def self.deny_with(reason)
|
@@ -56,10 +56,10 @@ module Excom
|
|
56
56
|
const_get(:Delegations)
|
57
57
|
end
|
58
58
|
|
59
|
-
attr_reader :
|
59
|
+
attr_reader :service
|
60
60
|
|
61
|
-
def initialize(
|
62
|
-
@
|
61
|
+
def initialize(service)
|
62
|
+
@service = service
|
63
63
|
end
|
64
64
|
|
65
65
|
def denial_reason(action)
|
@@ -74,7 +74,7 @@ module Excom
|
|
74
74
|
|
75
75
|
def sentry(klass)
|
76
76
|
klass = derive_sentry_class(klass) unless Class === klass
|
77
|
-
klass.new(
|
77
|
+
klass.new(service)
|
78
78
|
end
|
79
79
|
|
80
80
|
def to_hash
|
@@ -93,7 +93,7 @@ module Excom
|
|
93
93
|
|
94
94
|
private def sentinels
|
95
95
|
@sentinels ||= self.class.sentinels.map do |klass|
|
96
|
-
klass.new(
|
96
|
+
klass.new(service)
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
@@ -104,7 +104,7 @@ module Excom
|
|
104
104
|
end
|
105
105
|
|
106
106
|
private def constantize(klass, sentry_name)
|
107
|
-
module_prefix = (inline? ? self.class.
|
107
|
+
module_prefix = (inline? ? self.class.service_class.name : self.class.name).sub(/[^:]+\Z/, ''.freeze)
|
108
108
|
|
109
109
|
klass_name = module_prefix + "_#{klass}".gsub!(/(_([a-z]))/){ $2.upcase } + sentry_name
|
110
110
|
|
@@ -114,15 +114,15 @@ module Excom
|
|
114
114
|
end
|
115
115
|
|
116
116
|
private def inline?
|
117
|
-
self.class.
|
117
|
+
self.class.service_class.const_defined?(:Sentry) && self.class.service_class::Sentry == self.class
|
118
118
|
end
|
119
119
|
|
120
120
|
private def define_delegations!
|
121
|
-
delegated_methods = self.class.
|
122
|
-
Array(self.class.
|
121
|
+
delegated_methods = self.class.service_class.arg_methods.instance_methods +
|
122
|
+
Array(self.class.service_class.plugins[:sentry].options[:delegate])
|
123
123
|
|
124
124
|
delegated_methods.each do |name|
|
125
|
-
self.class.delegations.send(:define_method, name) {
|
125
|
+
self.class.delegations.send(:define_method, name) { service.public_send(name) }
|
126
126
|
end
|
127
127
|
|
128
128
|
self.class.instance_variable_set('@delegations_defined'.freeze, true)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Excom
|
2
|
+
module Plugins::Status
|
3
|
+
Plugins.register :status, self,
|
4
|
+
default_options: {success: [], failure: []}
|
5
|
+
|
6
|
+
def self.used(service_class, success:, failure:)
|
7
|
+
service_class.add_execution_prop(:status)
|
8
|
+
|
9
|
+
helpers = Module.new do
|
10
|
+
success.each do |name|
|
11
|
+
define_method(name) do |result = nil|
|
12
|
+
success(name) { result }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
failure.each do |name|
|
17
|
+
define_method(name) do |result = nil|
|
18
|
+
failure(name) { result }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
service_class.const_set('StatusHelpers', helpers)
|
24
|
+
service_class.send(:include, helpers)
|
25
|
+
end
|
26
|
+
|
27
|
+
def status
|
28
|
+
state.status
|
29
|
+
end
|
30
|
+
|
31
|
+
private def success!(status = :success)
|
32
|
+
state.status = status
|
33
|
+
super()
|
34
|
+
end
|
35
|
+
|
36
|
+
private def success(status = :success, &block)
|
37
|
+
state.status = status
|
38
|
+
super(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
private def failure!(status = :failure)
|
42
|
+
state.status = status
|
43
|
+
super()
|
44
|
+
end
|
45
|
+
|
46
|
+
private def failure(status = :failure, &block)
|
47
|
+
super(&block).tap do
|
48
|
+
state.status = status
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private def result_with(*)
|
53
|
+
super
|
54
|
+
state.status ||= state.success ? :success : :failure
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/excom/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: excom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.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: 2019-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.13.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.13.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: dry-struct
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,8 +122,7 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
-
description: Flexible and highly extensible
|
126
|
-
logic
|
125
|
+
description: Flexible and highly extensible Services for business logic organization
|
127
126
|
email:
|
128
127
|
- a.kuzko@gmail.com
|
129
128
|
executables: []
|
@@ -141,19 +140,21 @@ files:
|
|
141
140
|
- bin/setup
|
142
141
|
- excom.gemspec
|
143
142
|
- lib/excom.rb
|
144
|
-
- lib/excom/command.rb
|
145
143
|
- lib/excom/plugins.rb
|
146
144
|
- lib/excom/plugins/args.rb
|
147
145
|
- lib/excom/plugins/assertions.rb
|
148
146
|
- lib/excom/plugins/caching.rb
|
149
147
|
- lib/excom/plugins/context.rb
|
150
148
|
- lib/excom/plugins/dry_types.rb
|
149
|
+
- lib/excom/plugins/errors.rb
|
151
150
|
- lib/excom/plugins/executable.rb
|
151
|
+
- lib/excom/plugins/failure_cause.rb
|
152
152
|
- lib/excom/plugins/pluggable.rb
|
153
153
|
- lib/excom/plugins/rescue.rb
|
154
154
|
- lib/excom/plugins/sentry.rb
|
155
155
|
- lib/excom/plugins/sentry/sentinel.rb
|
156
|
-
- lib/excom/plugins/
|
156
|
+
- lib/excom/plugins/status.rb
|
157
|
+
- lib/excom/service.rb
|
157
158
|
- lib/excom/version.rb
|
158
159
|
homepage: https://github.com/akuzko/excom
|
159
160
|
licenses:
|
@@ -178,5 +179,5 @@ rubyforge_project:
|
|
178
179
|
rubygems_version: 2.6.8
|
179
180
|
signing_key:
|
180
181
|
specification_version: 4
|
181
|
-
summary: Flexible and highly extensible
|
182
|
+
summary: Flexible and highly extensible Services for business logic organization
|
182
183
|
test_files: []
|
@@ -1,25 +0,0 @@
|
|
1
|
-
module Excom
|
2
|
-
module Plugins::StatusHelpers
|
3
|
-
Plugins.register :status_helpers, self
|
4
|
-
|
5
|
-
def self.used(klass, success: [], failure: [])
|
6
|
-
klass.alias_success(*success)
|
7
|
-
|
8
|
-
helpers = Module.new do
|
9
|
-
(success + failure).each do |name|
|
10
|
-
define_method(name) do |result = nil|
|
11
|
-
@status = name
|
12
|
-
@result = result
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
klass.const_set('StatusHelpers', helpers)
|
18
|
-
klass.send(:include, helpers)
|
19
|
-
end
|
20
|
-
|
21
|
-
def success?
|
22
|
-
super || self.class.success_aliases.include?(status)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|