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.
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,17 @@
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
@@ -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