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
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
module Service::Plugins
|
5
|
+
module Plugin
|
6
|
+
def self.extended(plugin)
|
7
|
+
name = plugin.name.sub(/^.*::/, "").gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
8
|
+
|
9
|
+
Service::Plugins.register(name, plugin)
|
10
|
+
end
|
11
|
+
|
12
|
+
def register_as(name)
|
13
|
+
Service::Plugins.register(name, self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_options(options)
|
17
|
+
config[:default_options] = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def service_extension(extension)
|
21
|
+
::Zen::Service.send(:extend, extension)
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
@config ||= {}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
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
|
@@ -0,0 +1,34 @@
|
|
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
|
@@ -0,0 +1,65 @@
|
|
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
|
@@ -0,0 +1,59 @@
|
|
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
|
@@ -0,0 +1,60 @@
|
|
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
|
data/zen-service.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/zen/service/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "zen-service"
|
7
|
+
spec.version = Zen::Service::VERSION
|
8
|
+
spec.authors = ["Artem Kuzko"]
|
9
|
+
spec.email = ["a.kuzko@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Essence of service objects pattern"
|
12
|
+
spec.description = "Flexible and highly extensible Services for business logic organization"
|
13
|
+
spec.homepage = "https://github.com/akuzko/zen-service"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/akuzko/zen-service.git"
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_development_dependency "pry"
|
32
|
+
spec.add_development_dependency "pry-nav"
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
spec.add_development_dependency "rspec-its", "~> 1.2"
|
36
|
+
spec.add_development_dependency "rubocop", "~> 0.80"
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zen-service
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Artem Kuzko
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pry
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry-nav
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-its
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.80'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.80'
|
97
|
+
description: Flexible and highly extensible Services for business logic organization
|
98
|
+
email:
|
99
|
+
- a.kuzko@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".rubocop.yml"
|
107
|
+
- ".travis.yml"
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- bin/console
|
113
|
+
- bin/setup
|
114
|
+
- lib/zen/service.rb
|
115
|
+
- lib/zen/service/plugins.rb
|
116
|
+
- lib/zen/service/plugins/assertions.rb
|
117
|
+
- 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
|
121
|
+
- lib/zen/service/plugins/pluggable.rb
|
122
|
+
- 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
|
128
|
+
- lib/zen/service/version.rb
|
129
|
+
- zen-service.gemspec
|
130
|
+
homepage: https://github.com/akuzko/zen-service
|
131
|
+
licenses:
|
132
|
+
- MIT
|
133
|
+
metadata:
|
134
|
+
allowed_push_host: https://rubygems.org/
|
135
|
+
homepage_uri: https://github.com/akuzko/zen-service
|
136
|
+
source_code_uri: https://github.com/akuzko/zen-service.git
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 2.4.0
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
requirements: []
|
152
|
+
rubygems_version: 3.0.3
|
153
|
+
signing_key:
|
154
|
+
specification_version: 4
|
155
|
+
summary: Essence of service objects pattern
|
156
|
+
test_files: []
|