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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zen
4
+ class Service
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -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: []