zen-service 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .rspec_status
12
+ .ruby-version
13
+ .ruby-gemset
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.0
4
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem"s dependencies in zen-service.gemspec
6
+ gemspec
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
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