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 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