zen-service 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 +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +32 -0
- data/.travis.yml +4 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +284 -0
- data/Rakefile +12 -0
- data/bin/console +73 -0
- data/bin/setup +8 -0
- data/lib/zen/service.rb +15 -0
- data/lib/zen/service/plugins.rb +38 -0
- data/lib/zen/service/plugins/assertions.rb +17 -0
- data/lib/zen/service/plugins/attributes.rb +88 -0
- data/lib/zen/service/plugins/context.rb +53 -0
- data/lib/zen/service/plugins/executable.rb +179 -0
- data/lib/zen/service/plugins/execution_cache.rb +22 -0
- data/lib/zen/service/plugins/pluggable.rb +44 -0
- data/lib/zen/service/plugins/plugin.rb +29 -0
- data/lib/zen/service/plugins/policies.rb +68 -0
- data/lib/zen/service/plugins/rescue.rb +34 -0
- data/lib/zen/service/plugins/status.rb +65 -0
- data/lib/zen/service/plugins/validation.rb +59 -0
- data/lib/zen/service/spec_helpers.rb +60 -0
- data/lib/zen/service/version.rb +7 -0
- data/zen-service.gemspec +37 -0
- metadata +156 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: afe0cafc3b91d39c26b6eb9a24e4494b2310301ba94929dc1f49bced4f22b3d9
|
4
|
+
data.tar.gz: c57a4f0e6f0fb4b7cda8493be297134bc255f607fdab5d4841d12942a835eb2b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 242f523601adf176f3adeae447c3319123af3d6cf32afd5ee320d7b53c70932874e49e6fd30b92bd1c6c56bf9b675436d6022a165cf276a4c354de20d60996c0
|
7
|
+
data.tar.gz: 98ad8deaa278b44038b3adfe1284d92919eaa0acfe334cbba1e288844fbf1742dbf9cc43a2d1ba9a9adff5850e4ff680a3afa5055e975ae6c21fa28a963828ab
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: disable
|
3
|
+
TargetRubyVersion: 2.4
|
4
|
+
|
5
|
+
Style/StringLiterals:
|
6
|
+
EnforcedStyle: double_quotes
|
7
|
+
|
8
|
+
Style/AccessModifierDeclarations:
|
9
|
+
EnforcedStyle: inline
|
10
|
+
|
11
|
+
Style/Documentation:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Style/LambdaCall:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/ClassAndModuleChildren:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/DoubleNegation:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Metrics/MethodLength:
|
24
|
+
Max: 20
|
25
|
+
|
26
|
+
Metrics/BlockLength:
|
27
|
+
Exclude:
|
28
|
+
- 'spec/**/*.rb'
|
29
|
+
|
30
|
+
Layout/LineLength:
|
31
|
+
Exclude:
|
32
|
+
- 'spec/**/*.rb'
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Artem Kuzko
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
# Zen::Service
|
2
|
+
|
3
|
+
Flexible and highly extensible Service Objects for business logic organization.
|
4
|
+
|
5
|
+
[](http://travis-ci.org/akuzko/zen-service)
|
6
|
+
[](https://github.com/akuzko/zen-service/releases)
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'zen-service'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install zen-service
|
23
|
+
|
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
|
+
## Usage
|
43
|
+
|
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
|
+
The very basic usage of `Zen` services can be shown with following example:
|
49
|
+
|
50
|
+
```rb
|
51
|
+
# app/services/todos/update.rb
|
52
|
+
module Todos
|
53
|
+
class Update < Zen::Service
|
54
|
+
attributes :todo, :params
|
55
|
+
|
56
|
+
delegate :errors, to: :todo
|
57
|
+
|
58
|
+
def execute!
|
59
|
+
todo.update(params)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# app/controllers/todos/controller
|
65
|
+
class TodosController < ApplicationController
|
66
|
+
def update
|
67
|
+
service = Todos::Update.new(todo, params: todo_params)
|
68
|
+
|
69
|
+
if service.execute.success?
|
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
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### Service attributes
|
80
|
+
|
81
|
+
Read full version on [wiki](https://github.com/akuzko/zen-service/wiki#instantiating-service-with-attributes).
|
82
|
+
|
83
|
+
`Zen` services are initialized with _attributes_. To specify list of available attributes, use `attributes`
|
84
|
+
class method. All attributes are optional during service initialization. It is possible to omit keys during
|
85
|
+
initialization, and pass attributes as parameters - in this case attributes will be filled in correspondance
|
86
|
+
to the order they were defined. However, you cannot pass more attributes than declared attributes list, as
|
87
|
+
well as cannot pass single attribute multiple times (as parameter and as named attribute) or attributes that
|
88
|
+
were not declared with `attributes` class method.
|
89
|
+
|
90
|
+
```rb
|
91
|
+
class MyService < Zen::Service
|
92
|
+
attributes :foo, :bar
|
93
|
+
|
94
|
+
def execute!
|
95
|
+
# do something
|
96
|
+
end
|
97
|
+
|
98
|
+
def foo
|
99
|
+
super || 5
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
s1 = MyService.new
|
104
|
+
s1.foo # => 5
|
105
|
+
s1.bar # => nil
|
106
|
+
|
107
|
+
s2 = MyService.new(6)
|
108
|
+
s2.foo # => 6
|
109
|
+
|
110
|
+
s3 = s2.with_attributes(foo: 1, bar: 2)
|
111
|
+
s3.foo # => 1
|
112
|
+
s3.bar # => 2
|
113
|
+
```
|
114
|
+
|
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
|
+
### Service Extensions (Plugins)
|
154
|
+
|
155
|
+
Read full version on [wiki](https://github.com/akuzko/zen-service/wiki/Plugins).
|
156
|
+
|
157
|
+
`zen-service` is built with extensions in mind. Even core functionality is organized in plugins that are
|
158
|
+
used in base `Zen::Service` class. Bellow you can see a list of plugins with some description
|
159
|
+
and examples that are shipped with the gem:
|
160
|
+
|
161
|
+
- [`:status`](https://github.com/akuzko/zen-service/wiki/Plugins#status) - Adds `status` execution state
|
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:
|
167
|
+
|
168
|
+
```rb
|
169
|
+
class Posts::Update < Zen::Service
|
170
|
+
use :status,
|
171
|
+
success: [:ok],
|
172
|
+
failure: [:unprocessable_entity]
|
173
|
+
|
174
|
+
attributes :post, :params
|
175
|
+
|
176
|
+
delegate :errors, to: :post
|
177
|
+
|
178
|
+
def execute!
|
179
|
+
if post.update(params)
|
180
|
+
ok { post.as_json }
|
181
|
+
else
|
182
|
+
unprocessable_entity
|
183
|
+
end
|
184
|
+
end
|
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
|
+
```
|
193
|
+
|
194
|
+
Note that just like `success`, `failure`, or `result` methods, status helpers accept result value
|
195
|
+
as result of yielded block.
|
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.
|
199
|
+
|
200
|
+
```rb
|
201
|
+
# application_controller.rb
|
202
|
+
around_action :with_context
|
203
|
+
|
204
|
+
def with_context
|
205
|
+
Zen::Service.with_context(current_user: current_user) do
|
206
|
+
yield
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
```rb
|
212
|
+
class Posts::Archive < Zen::Service
|
213
|
+
use :context
|
214
|
+
|
215
|
+
attributes :post
|
216
|
+
|
217
|
+
def execute!
|
218
|
+
post.update(archived: true, archived_by: context[:current_user])
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
- [`:policies`](https://github.com/akuzko/zen-service/wiki/Plugins#policies) - Allows you to define permission
|
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.
|
229
|
+
|
230
|
+
```rb
|
231
|
+
class Posts::Policies < Zen::Service
|
232
|
+
use :policies
|
233
|
+
|
234
|
+
attributes :post, :user
|
235
|
+
|
236
|
+
deny_with :unauthorized do
|
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
|
246
|
+
|
247
|
+
deny_with :unprocessable_entity do
|
248
|
+
def delete?
|
249
|
+
# disallow to destroy posts that are older than 1 hour
|
250
|
+
(post.created_at + 1.hour).past?
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
policies = Posts::Policies.new(outdated_post, user)
|
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}
|
261
|
+
```
|
262
|
+
|
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
|
+
## Development
|
273
|
+
|
274
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
275
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
276
|
+
|
277
|
+
## Contributing
|
278
|
+
|
279
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko/zen-service.
|
280
|
+
|
281
|
+
## License
|
282
|
+
|
283
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
284
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "zen/service"
|
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
|
+
require "pry"
|
73
|
+
Pry.start
|