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
data/bin/setup
ADDED
data/lib/zen/service.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "service/version"
|
4
|
+
require_relative "service/plugins"
|
5
|
+
|
6
|
+
module Zen
|
7
|
+
class Service
|
8
|
+
autoload :SpecHelpers, "zen/service/spec_helpers"
|
9
|
+
|
10
|
+
extend Plugins::Pluggable
|
11
|
+
|
12
|
+
use :executable
|
13
|
+
use :attributes
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
module Service::Plugins
|
5
|
+
def self.fetch(name)
|
6
|
+
require("zen/service/plugins/#{name}") unless plugins.key?(name)
|
7
|
+
|
8
|
+
plugins[name] || raise("extension `#{name}` is not registered")
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.register(name, extension)
|
12
|
+
raise(ArgumentError, "extension `#{name}` is already registered") if plugins.key?(name)
|
13
|
+
|
14
|
+
plugins[name] =
|
15
|
+
if (old_name = plugins.key(extension))
|
16
|
+
plugins.delete(old_name)
|
17
|
+
else
|
18
|
+
extension
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.plugins
|
23
|
+
@plugins ||= {}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require_relative "plugins/plugin"
|
28
|
+
require_relative "plugins/pluggable"
|
29
|
+
require_relative "plugins/executable"
|
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"
|
38
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
module Service::Plugins
|
5
|
+
module Attributes
|
6
|
+
extend Plugin
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
@attributes = assert_valid_attributes!(resolve_args!(args))
|
10
|
+
|
11
|
+
super()
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize_clone(*)
|
15
|
+
super
|
16
|
+
@attributes = @attributes.dup unless @attributes.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_attributes(attributes)
|
20
|
+
clone.tap { |copy| copy.attributes.merge!(attributes) }
|
21
|
+
end
|
22
|
+
|
23
|
+
protected def attributes
|
24
|
+
@attributes
|
25
|
+
end
|
26
|
+
|
27
|
+
private def resolve_args!(args) # rubocop:disable Metrics/AbcSize
|
28
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
29
|
+
attributes = {}
|
30
|
+
allowed_length = self.class.attributes_list.length
|
31
|
+
|
32
|
+
if args.length > allowed_length
|
33
|
+
raise ArgumentError, "wrong number of attributes (given #{args.length}, expected 0..#{allowed_length})"
|
34
|
+
end
|
35
|
+
|
36
|
+
args.each_with_index do |value, i|
|
37
|
+
attributes[self.class.attributes_list[i]] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.each do |name, value|
|
41
|
+
raise(ArgumentError, "attribute #{name} has already been provided as parameter") if attributes.key?(name)
|
42
|
+
|
43
|
+
attributes[name] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
attributes
|
47
|
+
end
|
48
|
+
|
49
|
+
private def assert_valid_attributes!(actual)
|
50
|
+
unexpected = actual.keys - self.class.attributes_list
|
51
|
+
|
52
|
+
raise(ArgumentError, "wrong attributes #{unexpected} given") if unexpected.any?
|
53
|
+
|
54
|
+
actual
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
def inherited(service_class)
|
59
|
+
service_class.const_set(:AttributeMethods, Module.new)
|
60
|
+
service_class.send(:include, service_class::AttributeMethods)
|
61
|
+
service_class.attributes_list.replace attributes_list.dup
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
def attribute_methods
|
66
|
+
const_get(:AttributeMethods)
|
67
|
+
end
|
68
|
+
|
69
|
+
def attributes(*attrs)
|
70
|
+
attributes_list.concat(attrs)
|
71
|
+
|
72
|
+
attrs.each do |name|
|
73
|
+
attribute_methods.send(:define_method, name) { @attributes[name] }
|
74
|
+
attribute_methods.send(:define_method, "#{name}?") { !!@attributes[name] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def attributes_list
|
79
|
+
@attributes_list ||= []
|
80
|
+
end
|
81
|
+
|
82
|
+
def from(service)
|
83
|
+
new(service.send(:attributes))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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
|
@@ -0,0 +1,179 @@
|
|
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
|
@@ -0,0 +1,22 @@
|
|
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
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
module Service::Plugins
|
5
|
+
module Pluggable
|
6
|
+
def use(name, **opts)
|
7
|
+
extension = Service::Plugins.fetch(name)
|
8
|
+
|
9
|
+
defaults = extension.config[:default_options]
|
10
|
+
opts = defaults.merge(opts) unless defaults.nil?
|
11
|
+
|
12
|
+
if using?(name)
|
13
|
+
extension.configure(self, **opts) if extension.respond_to?(:configure)
|
14
|
+
return extension
|
15
|
+
end
|
16
|
+
|
17
|
+
use_extension(extension, name, **opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
private def use_extension(extension, name, **opts)
|
21
|
+
include extension
|
22
|
+
extend extension::ClassMethods if extension.const_defined?(:ClassMethods)
|
23
|
+
|
24
|
+
extension.used(self, **opts) if extension.respond_to?(:used)
|
25
|
+
extension.configure(self, **opts) if extension.respond_to?(:configure)
|
26
|
+
|
27
|
+
plugins[name] = Reflection.new(extension, opts)
|
28
|
+
|
29
|
+
extension
|
30
|
+
end
|
31
|
+
|
32
|
+
def using?(name)
|
33
|
+
plugins.key?(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def plugins
|
37
|
+
@plugins ||= {}
|
38
|
+
end
|
39
|
+
alias extensions plugins
|
40
|
+
|
41
|
+
Reflection = Struct.new(:extension, :options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|