zen-service 1.1.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4c9b04897987c5d00fe1d73e2c4e4151be3292467ce87ed00317bae09c9d9f2
4
- data.tar.gz: 4300a61cfe877b2c7212f7a8cf77da13b7ea0936bc033e0eab6f8f21e4df9138
3
+ metadata.gz: 64770c881ffd916d2ae348ea1e0e1b2f50f0ae1ddfe77e4cb33488894800a241
4
+ data.tar.gz: 1cf89ac8571af9af101ddfe2458a0a09e371a40e6d1e6bab9a58be0c0eec1445
5
5
  SHA512:
6
- metadata.gz: 5e679eada81a3ab7fe338efc42c55e0ee9f8e3fda73e3b7a84ddd9b47e3b83a0ea598931730cf09790167463bcec4310894a6402898817032a280f94df2d2298
7
- data.tar.gz: 0ce64d370187a540947d4931c780b3c2fa46b4408aaf229376f988111f26682dc8eeeea1287901fa94a49c4ff50aa49c4c49d9d7fce3bd180a7d2f05439e0531
6
+ metadata.gz: 6467ffcd8497764c8974f45cef7106865d3109c3a794a2b7cbefd6342a3d69599392f56a83386be728ecca753fdf247a554ebd8b20c726ccc723b767618f54eb
7
+ data.tar.gz: '034914344b92311d62df7eec453791f87b39111a1d6f29c5344761718468c6f103a012a8981a37b0e358f612ab581ec29d4bb2a65f2b86f3a1f88ce303628157'
data/.rubocop.yml CHANGED
@@ -1,12 +1,12 @@
1
1
  AllCops:
2
2
  NewCops: disable
3
- TargetRubyVersion: 2.4
3
+ TargetRubyVersion: 3.2
4
4
 
5
5
  Style/StringLiterals:
6
6
  EnforcedStyle: double_quotes
7
7
 
8
8
  Style/AccessModifierDeclarations:
9
- EnforcedStyle: inline
9
+ EnforcedStyle: group
10
10
 
11
11
  Style/Documentation:
12
12
  Enabled: false
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.2.2
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  Flexible and highly extensible Service Objects for business logic organization.
4
4
 
5
- [![build status](https://secure.travis-ci.org/akuzko/zen-service.png)](http://travis-ci.org/akuzko/zen-service)
6
5
  [![github release](https://img.shields.io/github/release/akuzko/zen-service.svg)](https://github.com/akuzko/zen-service/releases)
7
6
 
8
7
  ## Installation
@@ -21,42 +20,22 @@ Or install it yourself as:
21
20
 
22
21
  $ gem install zen-service
23
22
 
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
23
  ## Usage
43
24
 
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
25
  The very basic usage of `Zen` services can be shown with following example:
49
26
 
50
27
  ```rb
51
28
  # app/services/todos/update.rb
52
29
  module Todos
53
- class Update < Zen::Service
30
+ class Update < ApplicationService # base class for app services, inherits from Zen::Service
54
31
  attributes :todo, :params
55
32
 
56
- delegate :errors, to: :todo
57
-
58
- def execute!
59
- todo.update(params)
33
+ def call
34
+ if todo.update(params)
35
+ [:ok, todo]
36
+ else
37
+ [:error, todo.errors.messages]
38
+ end
60
39
  end
61
40
  end
62
41
  end
@@ -64,13 +43,9 @@ end
64
43
  # app/controllers/todos/controller
65
44
  class TodosController < ApplicationController
66
45
  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
46
+ case Todos::Update.call(todo, params: todo_params)
47
+ in [:ok, todo] then render json: Todos::Show.call(todo)
48
+ in [:error, errors] then render json: errors, status: :unprocessable_content
74
49
  end
75
50
  end
76
51
  end
@@ -91,7 +66,7 @@ were not declared with `attributes` class method.
91
66
  class MyService < Zen::Service
92
67
  attributes :foo, :bar
93
68
 
94
- def execute!
69
+ def call
95
70
  # do something
96
71
  end
97
72
 
@@ -112,163 +87,79 @@ s3.foo # => 1
112
87
  s3.bar # => 2
113
88
  ```
114
89
 
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
90
  ### Service Extensions (Plugins)
154
91
 
155
92
  Read full version on [wiki](https://github.com/akuzko/zen-service/wiki/Plugins).
156
93
 
157
94
  `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:
95
+ used in base `Zen::Service` class. Version 2.0.0 drops majority of built-in plugins for sake of
96
+ simplicity.
160
97
 
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:
98
+ However, `zen-service` still provides a couple of helpfull plugins out-of-the-box:
167
99
 
168
- ```rb
169
- class Posts::Update < Zen::Service
170
- use :status,
171
- success: [:ok],
172
- failure: [:unprocessable_entity]
173
-
174
- attributes :post, :params
100
+ - `:persisted_result` - provides `#result` method that returns value of the latest `#call`
101
+ method call. Also provides `#called?` helper method.
175
102
 
176
- delegate :errors, to: :post
103
+ - `:result_yielding` - can be used in junction with nested service calls to result with
104
+ block-provided value instead of nested service `call` return value. For example:
177
105
 
178
- def execute!
179
- if post.update(params)
180
- ok { post.as_json }
181
- else
182
- unprocessable_entity
106
+ ```rb
107
+ def call
108
+ logger.call do # logger uses `:result_yielding` plugin
109
+ todo.update!(params)
110
+ [:ok, todo]
111
+ rescue ActiveRecord::RecordInvalid
112
+ [:error, todo.errors.messages]
113
+ end
183
114
  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
- ```
115
+ ```
193
116
 
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.
117
+ Bellow you can see sample implementation of a plugin that transforms resulting objects
118
+ to camel-case notation (relying on ActiveSupport's core extensions)
199
119
 
200
120
  ```rb
201
- # application_controller.rb
202
- around_action :with_context
121
+ module CamelizeResult
122
+ extend Zen::Service::Plugin
203
123
 
204
- def with_context
205
- Zen::Service.with_context(current_user: current_user) do
206
- yield
124
+ def self.used(service_class)
125
+ service_class.prepend(Extension)
207
126
  end
208
- end
209
- ```
210
127
 
211
- ```rb
212
- class Posts::Archive < Zen::Service
213
- use :context
214
-
215
- attributes :post
128
+ def self.camelize(obj)
129
+ case obj
130
+ when Array then obj.map { camelize(_1) }
131
+ when Hash then obj.deep_transform_keys { _1.to_s.camelize(:lower).to_sym }
132
+ else obj
133
+ end
134
+ end
216
135
 
217
- def execute!
218
- post.update(archived: true, archived_by: context[:current_user])
136
+ module Extension
137
+ def call
138
+ CamelizeResult.camelize(super)
139
+ end
219
140
  end
220
141
  end
221
142
  ```
222
143
 
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.
144
+ and then
229
145
 
230
146
  ```rb
231
- class Posts::Policies < Zen::Service
232
- use :policies
233
-
234
- attributes :post, :user
147
+ class Todos::Show < Zen::Service
148
+ attributes :todo
235
149
 
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
150
+ use :camelize_result
246
151
 
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
152
+ def call
153
+ {
154
+ id: todo.id,
155
+ is_completed: todo.completed?
156
+ }
252
157
  end
253
158
  end
254
159
 
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}
160
+ Todos::Show[todo] # => { id: 1, isCompleted: true }
261
161
  ```
262
162
 
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
163
  ## Development
273
164
 
274
165
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -281,4 +172,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko
281
172
  ## License
282
173
 
283
174
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
284
-
data/bin/console CHANGED
@@ -4,70 +4,5 @@
4
4
  require "bundler/setup"
5
5
  require "zen/service"
6
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
7
  require "pry"
73
8
  Pry.start
@@ -5,8 +5,8 @@ module Zen
5
5
  module Attributes
6
6
  extend Plugin
7
7
 
8
- def initialize(*args)
9
- @attributes = assert_valid_attributes!(resolve_args!(args))
8
+ def initialize(*args, **kwargs)
9
+ @attributes = assert_valid_attributes!(resolve_args!(args, kwargs))
10
10
 
11
11
  super()
12
12
  end
@@ -20,24 +20,28 @@ module Zen
20
20
  clone.tap { |copy| copy.attributes.merge!(attributes) }
21
21
  end
22
22
 
23
- protected def attributes
23
+ protected
24
+
25
+ def attributes
24
26
  @attributes
25
27
  end
26
28
 
27
- private def resolve_args!(args) # rubocop:disable Metrics/AbcSize
28
- opts = args.last.is_a?(Hash) ? args.pop : {}
29
+ private
30
+
31
+ def resolve_args!(args, kwargs) # rubocop:disable Metrics/AbcSize
29
32
  attributes = {}
33
+ total_length = args.length + kwargs.length
30
34
  allowed_length = self.class.attributes_list.length
31
35
 
32
- if args.length > allowed_length
33
- raise ArgumentError, "wrong number of attributes (given #{args.length}, expected 0..#{allowed_length})"
36
+ if total_length > allowed_length
37
+ raise ArgumentError, "wrong number of attributes (given #{total_length}, expected 0..#{allowed_length})"
34
38
  end
35
39
 
36
40
  args.each_with_index do |value, i|
37
41
  attributes[self.class.attributes_list[i]] = value
38
42
  end
39
43
 
40
- opts.each do |name, value|
44
+ kwargs.each do |name, value|
41
45
  raise(ArgumentError, "attribute #{name} has already been provided as parameter") if attributes.key?(name)
42
46
 
43
47
  attributes[name] = value
@@ -46,7 +50,7 @@ module Zen
46
50
  attributes
47
51
  end
48
52
 
49
- private def assert_valid_attributes!(actual)
53
+ def assert_valid_attributes!(actual)
50
54
  unexpected = actual.keys - self.class.attributes_list
51
55
 
52
56
  raise(ArgumentError, "wrong attributes #{unexpected} given") if unexpected.any?
@@ -58,7 +62,7 @@ module Zen
58
62
  def inherited(service_class)
59
63
  service_class.const_set(:AttributeMethods, Module.new)
60
64
  service_class.send(:include, service_class::AttributeMethods)
61
- service_class.attributes_list.replace attributes_list.dup
65
+ service_class.attributes_list.replace(attributes_list.dup)
62
66
  super
63
67
  end
64
68
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module Zen
6
+ module Service::Plugins
7
+ module Callable
8
+ extend Plugin
9
+
10
+ def call
11
+ # No-op by default
12
+ end
13
+
14
+ module ClassMethods
15
+ def call(...)
16
+ new(...).call
17
+ end
18
+ alias [] call
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module Zen
6
+ module Service::Plugins
7
+ module PersistedResult
8
+ extend Plugin
9
+
10
+ module Extension
11
+ def call
12
+ @result = super
13
+ end
14
+ end
15
+
16
+ attr_reader :result
17
+
18
+ def initialize(*, **)
19
+ super
20
+ extend(Extension)
21
+ end
22
+
23
+ def called?
24
+ defined?(@result)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -3,6 +3,8 @@
3
3
  module Zen
4
4
  module Service::Plugins
5
5
  module Pluggable
6
+ Reflection = Struct.new(:extension, :options)
7
+
6
8
  def use(name, **opts, &block)
7
9
  extension = Service::Plugins.fetch(name)
8
10
 
@@ -17,18 +19,6 @@ module Zen
17
19
  use_extension(extension, name, **opts, &block)
18
20
  end
19
21
 
20
- private def use_extension(extension, name, **opts, &block)
21
- include extension
22
- extend extension::ClassMethods if extension.const_defined?(:ClassMethods)
23
-
24
- extension.used(self, **opts, &block) if extension.respond_to?(:used)
25
- extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
26
-
27
- plugins[name] = Reflection.new(extension, opts.merge(block: block))
28
-
29
- extension
30
- end
31
-
32
22
  def using?(name)
33
23
  plugins.key?(name)
34
24
  end
@@ -38,7 +28,19 @@ module Zen
38
28
  end
39
29
  alias extensions plugins
40
30
 
41
- Reflection = Struct.new(:extension, :options)
31
+ private
32
+
33
+ def use_extension(extension, name, **opts, &block)
34
+ include extension
35
+ extend extension::ClassMethods if extension.const_defined?(:ClassMethods)
36
+
37
+ extension.used(self, **opts, &block) if extension.respond_to?(:used)
38
+ extension.configure(self, **opts, &block) if extension.respond_to?(:configure)
39
+
40
+ plugins[name] = Reflection.new(extension, opts.merge(block:))
41
+
42
+ extension
43
+ end
42
44
  end
43
45
  end
44
46
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module Zen
6
+ module Service::Plugins
7
+ module ResultYielding
8
+ extend Plugin
9
+
10
+ module Extension
11
+ def call
12
+ return super unless block_given?
13
+
14
+ result = nil
15
+ super do
16
+ result = yield
17
+ end
18
+ result
19
+ end
20
+ end
21
+
22
+ def self.used(service_class)
23
+ service_class.prepend(Extension)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -26,13 +26,8 @@ module Zen
26
26
 
27
27
  require_relative "plugins/plugin"
28
28
  require_relative "plugins/pluggable"
29
- require_relative "plugins/executable"
29
+ require_relative "plugins/callable"
30
30
  require_relative "plugins/attributes"
31
- require_relative "plugins/assertions"
32
- require_relative "plugins/context"
33
- require_relative "plugins/execution_cache"
34
- require_relative "plugins/policies"
35
- require_relative "plugins/rescue"
36
- require_relative "plugins/status"
37
- require_relative "plugins/validation"
31
+ require_relative "plugins/persisted_result"
32
+ require_relative "plugins/result_yielding"
38
33
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Zen
4
4
  class Service
5
- VERSION = "1.1.0"
5
+ VERSION = "2.0.0"
6
6
  end
7
7
  end
data/lib/zen/service.rb CHANGED
@@ -9,7 +9,7 @@ module Zen
9
9
 
10
10
  extend Plugins::Pluggable
11
11
 
12
- use :executable
12
+ use :callable
13
13
  use :attributes
14
14
  end
15
15
  end
data/zen-service.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Flexible and highly extensible Services for business logic organization"
13
13
  spec.homepage = "https://github.com/akuzko/zen-service"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org/"
18
18
 
@@ -33,5 +33,5 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency "rake", "~> 13.0"
34
34
  spec.add_development_dependency "rspec", "~> 3.0"
35
35
  spec.add_development_dependency "rspec-its", "~> 1.2"
36
- spec.add_development_dependency "rubocop", "~> 0.80"
36
+ spec.add_development_dependency "rubocop", "~> 1.81"
37
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zen-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.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: 2023-10-09 00:00:00.000000000 Z
11
+ date: 2025-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.80'
89
+ version: '1.81'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.80'
96
+ version: '1.81'
97
97
  description: Flexible and highly extensible Services for business logic organization
98
98
  email:
99
99
  - a.kuzko@gmail.com
@@ -104,7 +104,7 @@ files:
104
104
  - ".gitignore"
105
105
  - ".rspec"
106
106
  - ".rubocop.yml"
107
- - ".travis.yml"
107
+ - ".tool-versions"
108
108
  - Gemfile
109
109
  - LICENSE.txt
110
110
  - README.md
@@ -113,18 +113,12 @@ files:
113
113
  - bin/setup
114
114
  - lib/zen/service.rb
115
115
  - lib/zen/service/plugins.rb
116
- - lib/zen/service/plugins/assertions.rb
117
116
  - lib/zen/service/plugins/attributes.rb
118
- - lib/zen/service/plugins/context.rb
119
- - lib/zen/service/plugins/executable.rb
120
- - lib/zen/service/plugins/execution_cache.rb
117
+ - lib/zen/service/plugins/callable.rb
118
+ - lib/zen/service/plugins/persisted_result.rb
121
119
  - lib/zen/service/plugins/pluggable.rb
122
120
  - lib/zen/service/plugins/plugin.rb
123
- - lib/zen/service/plugins/policies.rb
124
- - lib/zen/service/plugins/rescue.rb
125
- - lib/zen/service/plugins/status.rb
126
- - lib/zen/service/plugins/validation.rb
127
- - lib/zen/service/spec_helpers.rb
121
+ - lib/zen/service/plugins/result_yielding.rb
128
122
  - lib/zen/service/version.rb
129
123
  - zen-service.gemspec
130
124
  homepage: https://github.com/akuzko/zen-service
@@ -142,7 +136,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
136
  requirements:
143
137
  - - ">="
144
138
  - !ruby/object:Gem::Version
145
- version: 2.4.0
139
+ version: 3.2.0
146
140
  required_rubygems_version: !ruby/object:Gem::Requirement
147
141
  requirements:
148
142
  - - ">="
data/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.4.0
4
- before_install: gem install bundler -v 1.16.0
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module Assertions
6
- extend Plugin
7
-
8
- private def assert
9
- if yield
10
- success! unless state.has_success?
11
- else
12
- failure!
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module Context
6
- extend Plugin
7
-
8
- attr_accessor :local_context
9
- protected :local_context, :local_context=
10
-
11
- def context
12
- global_context = ::Zen::Service.context
13
- return global_context if local_context.nil?
14
-
15
- if global_context.respond_to?(:merge)
16
- global_context.merge(local_context)
17
- else
18
- local_context
19
- end
20
- end
21
-
22
- def with_context(ctx)
23
- clone.tap do |copy|
24
- copy.local_context =
25
- copy.local_context.respond_to?(:merge) ? copy.local_context.merge(ctx) : ctx
26
- end
27
- end
28
-
29
- def execute(*)
30
- ::Zen::Service.with_context(context) do
31
- super
32
- end
33
- end
34
-
35
- module ServiceMethods
36
- def with_context(ctx)
37
- current = context
38
- Thread.current[:zen_service_context] = context.respond_to?(:merge) ? context.merge(ctx) : ctx
39
-
40
- yield
41
- ensure
42
- Thread.current[:zen_service_context] = current
43
- end
44
-
45
- def context
46
- Thread.current[:zen_service_context]
47
- end
48
- end
49
-
50
- service_extension ServiceMethods
51
- end
52
- end
53
- end
@@ -1,179 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ostruct"
4
-
5
- module Zen
6
- module Service::Plugins
7
- module Executable
8
- extend Plugin
9
-
10
- class State
11
- def self.prop_names
12
- @prop_names ||= []
13
- end
14
-
15
- def self.add_prop(*props)
16
- prop_names.push(*props)
17
- props.each { |prop| def_prop_accessor(prop) }
18
- end
19
-
20
- def self.def_prop_accessor(name)
21
- define_method(name) { @values[name] }
22
- define_method("#{name}=") { |value| @values[name] = value }
23
- define_method("has_#{name}?") { @values.key?(name) }
24
- end
25
-
26
- def initialize(values = {})
27
- @values = values
28
- end
29
-
30
- def clear!
31
- @values.clear
32
- end
33
-
34
- def prop_names
35
- self.class.prop_names
36
- end
37
-
38
- def replace(other)
39
- missing_props = prop_names - other.prop_names
40
-
41
- unless missing_props.empty?
42
- raise ArgumentError, "cannot accept execution state #{other} due to missing props: #{missing_props}"
43
- end
44
-
45
- prop_names.each do |prop|
46
- @values[prop] = other.public_send(prop)
47
- end
48
- end
49
- end
50
-
51
- def self.used(service_class, *)
52
- service_class.const_set(:State, Class.new(State))
53
- service_class.add_execution_prop(:executed, :success, :result)
54
- end
55
-
56
- attr_reader :state
57
-
58
- def initialize(*)
59
- @state = self.class::State.new(executed: false)
60
- end
61
-
62
- def initialize_clone(*)
63
- clear_execution_state!
64
- end
65
-
66
- def execute(*, &block)
67
- clear_execution_state!
68
- result = execute!(&block)
69
- result_with(result) unless state.has_result?
70
- state.executed = true
71
-
72
- self
73
- end
74
-
75
- def executed?
76
- state.executed
77
- end
78
-
79
- def ~@
80
- state
81
- end
82
-
83
- private def execute!
84
- success!
85
- end
86
-
87
- private def clear_execution_state!
88
- state.clear!
89
- state.executed = false
90
- end
91
-
92
- private def success(**)
93
- assign_successful_state
94
- assign_successful_result(yield)
95
- end
96
-
97
- private def failure(**)
98
- assign_failed_state
99
- assign_failed_result(yield)
100
- end
101
-
102
- private def success!(**)
103
- assign_successful_state
104
- end
105
-
106
- private def failure!(**)
107
- assign_failed_state
108
- end
109
-
110
- private def assign_successful_state
111
- state.success = true
112
- state.result = nil
113
- end
114
-
115
- private def assign_failed_state
116
- state.success = false
117
- state.result = nil
118
- end
119
-
120
- private def assign_successful_result(value)
121
- state.result = value
122
- end
123
-
124
- private def assign_failed_result(value)
125
- state.result = value
126
- end
127
-
128
- def result
129
- return state.result unless block_given?
130
-
131
- result_with(yield)
132
- end
133
-
134
- private def result_with(obj)
135
- return state.replace(obj) if obj.is_a?(State)
136
-
137
- state.success = !!obj
138
- if state.success
139
- assign_successful_result(obj)
140
- else
141
- assign_failed_result(obj)
142
- end
143
- end
144
-
145
- def success?
146
- state.success == true
147
- end
148
-
149
- def failure?
150
- !success?
151
- end
152
-
153
- module ClassMethods
154
- def inherited(klass)
155
- klass.const_set(:State, Class.new(self::State))
156
- klass::State.prop_names.replace(self::State.prop_names.dup)
157
- end
158
-
159
- def add_execution_prop(*props)
160
- self::State.add_prop(*props)
161
- end
162
-
163
- def call(*args)
164
- new(*args).execute
165
- end
166
- alias execute call
167
-
168
- def [](*args)
169
- call(*args).result
170
- end
171
-
172
- def method_added(name)
173
- private :execute! if name == :execute!
174
- super if defined? super
175
- end
176
- end
177
- end
178
- end
179
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module ExecutionCache
6
- extend Plugin
7
-
8
- def initialize(*)
9
- super
10
- extend Extension
11
- end
12
-
13
- module Extension
14
- def execute(*)
15
- return super if block_given? || !executed?
16
-
17
- self
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module Policies
6
- extend Plugin
7
-
8
- GuardViolationError = Class.new(StandardError)
9
-
10
- def self.used(service_class, *)
11
- service_class.partials = []
12
- end
13
-
14
- private def execute!
15
- partials.each_with_object({}) do |partial, permissions|
16
- partial.public_methods(false).grep(/\?$/).each do |action_check|
17
- key = action_check.to_s[0...-1]
18
- can = partial.public_send(action_check)
19
-
20
- permissions[key] = permissions.key?(key) ? permissions[key] && can : can
21
- end
22
- end
23
- end
24
-
25
- def can?(action)
26
- why_cant?(action).nil?
27
- end
28
-
29
- def guard!(action)
30
- reason = why_cant?(action)
31
-
32
- return if reason.nil?
33
-
34
- raise(reason) if (reason.is_a?(Class) ? reason : reason.class) < Exception
35
-
36
- raise(GuardViolationError, reason)
37
- end
38
-
39
- def why_cant?(action)
40
- action_check = "#{action}?"
41
-
42
- reason =
43
- partials
44
- .find { |p| p.respond_to?(action_check) && !p.public_send(action_check) }
45
- &.class
46
- &.denial_reason
47
-
48
- reason.is_a?(Proc) ? instance_exec(&reason) : reason
49
- end
50
-
51
- private def partials
52
- @partials ||= self.class.partials.map do |klass|
53
- klass.from(self)
54
- end
55
- end
56
-
57
- module ClassMethods
58
- attr_accessor :partials, :denial_reason
59
-
60
- def deny_with(reason, &block)
61
- partial = Class.new(self, &block)
62
- partial.denial_reason = reason
63
- partials << partial
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module Rescue
6
- extend Plugin
7
-
8
- def self.used(service_class, *)
9
- service_class.use(:status) unless service_class.using?(:status)
10
- service_class.add_execution_prop(:error)
11
- end
12
-
13
- def execute(**opts)
14
- rezcue = opts.delete(:rescue)
15
- super
16
- rescue StandardError => e
17
- clear_execution_state!
18
- failure!(status: :error)
19
- state.error = e
20
- raise e unless rezcue
21
-
22
- self
23
- end
24
-
25
- def error
26
- state.error
27
- end
28
-
29
- def error?
30
- status == :error
31
- end
32
- end
33
- end
34
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module Status
6
- extend Plugin
7
-
8
- default_options(success: [], failure: [])
9
-
10
- def self.used(service_class, **)
11
- service_class.add_execution_prop(:status)
12
-
13
- helpers = Module.new
14
- service_class.const_set(:StatusHelpers, helpers)
15
- service_class.send(:include, helpers)
16
- end
17
-
18
- def self.configure(service_class, success:, failure:)
19
- service_class::StatusHelpers.module_eval do
20
- success.each do |name|
21
- define_method(name) do |**opts, &block|
22
- success(status: name, **opts, &block)
23
- end
24
- end
25
-
26
- failure.each do |name|
27
- define_method(name) do |**opts, &block|
28
- failure(status: name, **opts, &block)
29
- end
30
- end
31
- end
32
- end
33
-
34
- def status
35
- state.status
36
- end
37
-
38
- private def success!(status: :success, **)
39
- state.status = status
40
- super
41
- end
42
-
43
- private def success(status: :success, **)
44
- state.status = status
45
- super
46
- end
47
-
48
- private def failure!(status: :failure, **)
49
- state.status = status
50
- super
51
- end
52
-
53
- private def failure(status: :failure, **)
54
- super.tap do
55
- state.status = status
56
- end
57
- end
58
-
59
- private def result_with(*)
60
- super
61
- state.status ||= state.success ? :success : :failure
62
- end
63
- end
64
- end
65
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::Plugins
5
- module Validation
6
- extend Plugin
7
-
8
- class Errors < Hash
9
- def add(key, message)
10
- (self[key] ||= []).push(message)
11
- end
12
- end
13
-
14
- default_options(errors_class: Errors)
15
-
16
- def self.used(service_class, *)
17
- service_class.add_execution_prop(:errors)
18
- end
19
-
20
- private def initialize(*)
21
- super
22
- state.errors = errors_class.new
23
- end
24
-
25
- def execute(*)
26
- return super if valid?
27
-
28
- failure!(status: :invalid)
29
-
30
- self
31
- end
32
-
33
- def errors
34
- state.errors
35
- end
36
-
37
- private def errors_class
38
- self.class.plugins[:validation].options[:errors_class]
39
- end
40
-
41
- private def validate!
42
- errors.clear
43
- validate
44
- end
45
-
46
- def validate; end
47
-
48
- def valid?
49
- validate!
50
- errors.empty?
51
- end
52
-
53
- private def clear_execution_state!
54
- super
55
- state.errors = errors_class.new
56
- end
57
- end
58
- end
59
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Zen
4
- module Service::SpecHelpers
5
- def self.included(target)
6
- target.extend(ClassMethods)
7
- end
8
-
9
- # Example:
10
- # stub_service(MyService)
11
- # .with_atributes(foo: 'foo')
12
- # .with_stubs(result: 'bar', success: true)
13
- # .service
14
- def stub_service(service)
15
- ServiceMocker.new(self).stub_service(service)
16
- end
17
-
18
- module ClassMethods
19
- def service_context(&block)
20
- around do |example|
21
- ::Zen::Service.with_context(instance_exec(&block)) do
22
- example.run
23
- end
24
- end
25
- end
26
- end
27
-
28
- class ServiceMocker < SimpleDelegator
29
- attr_reader :service_class, :service
30
-
31
- def stub_service(service_class) # rubocop:disable Metrics/AbcSize
32
- @service_class = service_class
33
- @service = double(service_class.name)
34
-
35
- allow(service_class).to receive(:new).and_return(service)
36
- allow(service).to receive(:execute).and_return(service)
37
- allow(service).to receive(:executed?).and_return(true)
38
-
39
- self
40
- end
41
-
42
- def with_attributes(*attributes)
43
- expect(service_class).to receive(:new).with(*attributes).and_return(service)
44
-
45
- self
46
- end
47
-
48
- def with_stubs(stubs)
49
- stubs[:success?] = !!stubs[:result] unless stubs.key?(:success)
50
- stubs[:failure?] = !stubs[:success?]
51
-
52
- stubs.each do |name, value|
53
- allow(service).to receive(name).and_return(value)
54
- end
55
-
56
- self
57
- end
58
- end
59
- end
60
- end