zen-service 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![build status](https://secure.travis-ci.org/akuzko/zen-service.png)](http://travis-ci.org/akuzko/zen-service)
|
6
|
+
[![github release](https://img.shields.io/github/release/akuzko/zen-service.svg)](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
|