simple-service 0.1.6 → 0.2.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 +4 -4
- data/.rubocop.yml +7 -2
- data/Gemfile +3 -1
- data/VERSION +1 -1
- data/lib/simple/service/action/comment.rb +1 -3
- data/lib/simple/service/action.rb +4 -3
- data/lib/simple/service/errors.rb +4 -3
- data/lib/simple/service.rb +19 -35
- data/lib/simple/workflow/context.rb +105 -0
- data/lib/simple/workflow/current_context.rb +33 -0
- data/lib/simple/workflow/reloader.rb +84 -0
- data/lib/simple/workflow/rspec_helper.rb +15 -0
- data/lib/simple/workflow.rb +96 -0
- data/lib/simple-workflow.rb +3 -0
- data/scripts/test +2 -0
- data/simple-service.gemspec +1 -0
- data/spec/simple/service/action_invoke3_spec.rb +0 -8
- data/spec/simple/service/action_invoke_spec.rb +0 -9
- data/spec/simple/service/service_spec.rb +14 -59
- data/spec/simple/workflow/context_spec.rb +90 -0
- data/spec/simple/workflow/current_context_spec.rb +41 -0
- data/spec/simple/workflow/reloader_spec/example1.rb +10 -0
- data/spec/simple/workflow/reloader_spec/example2.rb +7 -0
- data/spec/simple/workflow/reloader_spec.rb +48 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/support/spec_services.rb +1 -3
- metadata +40 -6
- data/lib/simple/service/context.rb +0 -94
- data/spec/simple/service/context_spec.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 706a23842071b121c2b6298b7bdaec0b43c4b110125c2c09011537541c4f5b9b
|
4
|
+
data.tar.gz: 9ad938ece973392233c554053aa1906da0ea799722d527cb15870913ef108070
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68563b2c8d9c955049814f90d31b765a30c077553d7199287c4d0117d7d1ed0eb2ce71674195820d482de52bd2f53c70dbb798549d2bf6a7c35ef2f149de3b56
|
7
|
+
data.tar.gz: ccb9fc69a58b3a9f7225e60e1b8120dced8ec58802483ee4151cb6a0fc71c20b5b3f9b4393b6b9c09de2ec18d3c09bfbdac16f341669ac055b3daa9b31b98ae0
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
AllCops:
|
2
|
-
|
2
|
+
NewCops: enable
|
3
|
+
TargetRubyVersion: 2.7
|
3
4
|
Exclude:
|
4
5
|
- 'spec/**/*'
|
5
6
|
- 'test/**/*'
|
@@ -14,7 +15,7 @@ Metrics/BlockLength:
|
|
14
15
|
Exclude:
|
15
16
|
- 'spec/**/*'
|
16
17
|
|
17
|
-
|
18
|
+
Layout/LineLength:
|
18
19
|
Max: 140
|
19
20
|
|
20
21
|
Metrics/MethodLength:
|
@@ -101,3 +102,7 @@ Style/ParallelAssignment:
|
|
101
102
|
|
102
103
|
Style/CommentedKeyword:
|
103
104
|
Enabled: false
|
105
|
+
|
106
|
+
Style/AccessorGrouping:
|
107
|
+
Enabled: false
|
108
|
+
|
data/Gemfile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# rubocop:disable Metrics/AbcSize
|
2
|
-
|
3
1
|
# returns the comment for an action
|
4
2
|
class ::Simple::Service::Action::Comment # @private
|
5
3
|
attr_reader :short
|
@@ -8,7 +6,7 @@ class ::Simple::Service::Action::Comment # @private
|
|
8
6
|
def self.extract(action:)
|
9
7
|
file, line = action.source_location
|
10
8
|
lines = Extractor.extract_comment_lines(file: file, before_line: line)
|
11
|
-
full = lines[2
|
9
|
+
full = lines[2..].join("\n") if lines.length >= 2
|
12
10
|
new short: lines[0], full: full
|
13
11
|
end
|
14
12
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module Simple::Service
|
2
|
+
# rubocop:disable Lint/EmptyClass
|
2
3
|
class Action
|
3
4
|
end
|
4
5
|
end
|
@@ -114,7 +115,7 @@ module Simple::Service
|
|
114
115
|
|
115
116
|
# Note that +keys+ now only contains names of keyword arguments that actually exist.
|
116
117
|
# This is therefore not a way to DOS this process.
|
117
|
-
|
118
|
+
keys.map(&:to_sym).zip(values).to_h
|
118
119
|
end
|
119
120
|
|
120
121
|
def variadic_parameter
|
@@ -164,7 +165,7 @@ module Simple::Service
|
|
164
165
|
# we otherwise raise a ExtraArguments exception.
|
165
166
|
case ary.length <=> positional_names.length
|
166
167
|
when 1 # i.e. ary.length > positional_names.length
|
167
|
-
extra_arguments = ary[positional_names.length
|
168
|
+
extra_arguments = ary[positional_names.length..]
|
168
169
|
ary = ary[0..positional_names.length]
|
169
170
|
|
170
171
|
if !extra_arguments.empty? && !variadic_parameter
|
@@ -179,7 +180,7 @@ module Simple::Service
|
|
179
180
|
end
|
180
181
|
|
181
182
|
# Build a hash with the existing_positional_names and the values from the array.
|
182
|
-
hsh =
|
183
|
+
hsh = existing_positional_names.zip(ary).to_h
|
183
184
|
|
184
185
|
# Add the variadic_parameter, if any.
|
185
186
|
hsh[variadic_parameter.name] = extra_arguments if variadic_parameter
|
@@ -2,8 +2,10 @@ module Simple::Service
|
|
2
2
|
# Will be raised by ::Simple::Service.action.
|
3
3
|
class NoSuchAction < ::ArgumentError
|
4
4
|
attr_reader :service, :name
|
5
|
+
|
5
6
|
def initialize(service, name)
|
6
7
|
@service, @name = service, name
|
8
|
+
super()
|
7
9
|
end
|
8
10
|
|
9
11
|
def to_s
|
@@ -22,6 +24,7 @@ module Simple::Service
|
|
22
24
|
|
23
25
|
def initialize(action, parameters)
|
24
26
|
@action, @parameters = action, parameters
|
27
|
+
super()
|
25
28
|
end
|
26
29
|
|
27
30
|
def to_s
|
@@ -35,6 +38,7 @@ module Simple::Service
|
|
35
38
|
|
36
39
|
def initialize(action, arguments)
|
37
40
|
@action, @arguments = action, arguments
|
41
|
+
super()
|
38
42
|
end
|
39
43
|
|
40
44
|
def to_s
|
@@ -43,9 +47,6 @@ module Simple::Service
|
|
43
47
|
end
|
44
48
|
end
|
45
49
|
|
46
|
-
class ContextMissingError < ::StandardError
|
47
|
-
end
|
48
|
-
|
49
50
|
class ContextReadOnlyError < ::StandardError
|
50
51
|
def initialize(key)
|
51
52
|
super "Cannot overwrite existing context setting #{key.inspect}"
|
data/lib/simple/service.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
module Simple # @private
|
2
2
|
end
|
3
3
|
|
4
|
+
module Simple::Service # @private
|
5
|
+
end
|
6
|
+
|
4
7
|
require "expectation"
|
5
8
|
require "logger"
|
6
9
|
|
7
10
|
require_relative "service/errors"
|
8
11
|
require_relative "service/action"
|
9
|
-
require_relative "service/context"
|
10
12
|
require_relative "service/version"
|
11
13
|
|
12
14
|
# <b>The Simple::Service interface</b>
|
@@ -40,11 +42,9 @@ require_relative "service/version"
|
|
40
42
|
# TODO: why a Hash? It feels much better if Simple::Service.actions returns an array of names.
|
41
43
|
#
|
42
44
|
#
|
43
|
-
# 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>.
|
45
|
+
# 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>.
|
44
46
|
#
|
45
|
-
# Simple::Service.
|
46
|
-
# Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
|
47
|
-
# end
|
47
|
+
# Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
|
48
48
|
# => 42
|
49
49
|
#
|
50
50
|
module Simple::Service
|
@@ -61,28 +61,16 @@ module Simple::Service
|
|
61
61
|
klass.include ServiceExpectations
|
62
62
|
end
|
63
63
|
|
64
|
-
|
65
|
-
@logger ||= ::Logger.new(STDOUT)
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.logger=(logger)
|
69
|
-
@logger = logger
|
70
|
-
end
|
71
|
-
|
72
|
-
# returns true if the passed in object is a service module.
|
73
|
-
#
|
74
|
-
# A service must be a module, and it must include the Simple::Service module.
|
75
|
-
def self.service?(service)
|
76
|
-
verify_service! service
|
77
|
-
true
|
78
|
-
rescue ::ArgumentError
|
79
|
-
false
|
80
|
-
end
|
81
|
-
|
82
|
-
# Raises an error if the passed in object is not a service
|
64
|
+
# Raises an error if the passed in object is not a Simple::Service
|
83
65
|
def self.verify_service!(service) # @private
|
84
|
-
|
85
|
-
|
66
|
+
expect! service => Module
|
67
|
+
|
68
|
+
# rubocop:disable Style/GuardClause
|
69
|
+
unless service.include?(::Simple::Service)
|
70
|
+
raise ::ArgumentError,
|
71
|
+
"#{service.name} is not a Simple::Service, did you 'include Simple::Service'"
|
72
|
+
end
|
73
|
+
# rubocop:enable Style/GuardClause
|
86
74
|
end
|
87
75
|
|
88
76
|
# returns a Hash with all actions in the +service+ module
|
@@ -94,6 +82,8 @@ module Simple::Service
|
|
94
82
|
|
95
83
|
# returns the action with the given name.
|
96
84
|
def self.action(service, name)
|
85
|
+
expect! name => Symbol
|
86
|
+
|
97
87
|
actions = self.actions(service)
|
98
88
|
actions[name] || begin
|
99
89
|
raise ::Simple::Service::NoSuchAction.new(service, name)
|
@@ -107,10 +97,8 @@ module Simple::Service
|
|
107
97
|
#
|
108
98
|
# As the main purpose of this module is to call services with outside data,
|
109
99
|
# the +.invoke+ action is usually preferred.
|
110
|
-
#
|
111
|
-
# *Note:* You cannot call this method if the context is not set.
|
112
100
|
def self.invoke3(service, name, *args, **flags)
|
113
|
-
flags = flags.
|
101
|
+
flags = flags.transform_keys(&:to_s)
|
114
102
|
invoke service, name, args: args, flags: flags
|
115
103
|
end
|
116
104
|
|
@@ -154,14 +142,10 @@ module Simple::Service
|
|
154
142
|
# by the method we raise an ArgumentError.
|
155
143
|
#
|
156
144
|
# Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
|
157
|
-
|
158
|
-
# <b>Note:</b> You cannot call this method if the context is not set.
|
159
145
|
def self.invoke(service, name, args: {}, flags: {})
|
160
|
-
raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke3" unless context
|
161
|
-
|
162
146
|
expect! args => [Hash, Array], flags: Hash
|
163
|
-
args.
|
164
|
-
flags.
|
147
|
+
args.each_key { |key| expect! key => String } if args.is_a?(Hash)
|
148
|
+
flags.each_key { |key| expect! key => String }
|
165
149
|
|
166
150
|
action(service, name).invoke(args: args, flags: flags)
|
167
151
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "simple/immutable"
|
2
|
+
|
3
|
+
module Simple::Workflow
|
4
|
+
# A context object
|
5
|
+
#
|
6
|
+
# Each service executes with a current context. The system manages a stack of
|
7
|
+
# contexts; whenever a service execution is done the current context is reverted
|
8
|
+
# to its previous value.
|
9
|
+
#
|
10
|
+
# A context object can store a large number of values; the only way to set or
|
11
|
+
# access a value is via getters and setters. These are implemented via
|
12
|
+
# +method_missing(..)+.
|
13
|
+
#
|
14
|
+
# Also, once a value is set in the context it is not possible to change or
|
15
|
+
# unset it.
|
16
|
+
class Context < Simple::Immutable
|
17
|
+
SELF = self
|
18
|
+
|
19
|
+
# returns a new Context object.
|
20
|
+
#
|
21
|
+
# Parameters:
|
22
|
+
#
|
23
|
+
# - hsh (Hash or nil): sets values for this context
|
24
|
+
# - previous_context (Context or nil): if +previous_context+ is provided,
|
25
|
+
# values that are not defined in the \a +hsh+ argument are read from the
|
26
|
+
# +previous_context+ instead (or the previous context's +previous_context+,
|
27
|
+
# etc.)
|
28
|
+
def initialize(hsh, previous_context = nil)
|
29
|
+
expect! hsh => [Hash, nil]
|
30
|
+
expect! previous_context => [SELF, nil]
|
31
|
+
|
32
|
+
@previous_context = previous_context
|
33
|
+
super(hsh || {})
|
34
|
+
end
|
35
|
+
|
36
|
+
def reload!(a_module)
|
37
|
+
if @previous_context
|
38
|
+
@previous_context.reload!(a_module)
|
39
|
+
return a_module
|
40
|
+
end
|
41
|
+
|
42
|
+
@reloaded_modules ||= []
|
43
|
+
return if @reloaded_modules.include?(a_module)
|
44
|
+
|
45
|
+
::Simple::Workflow::Reloader.reload(a_module)
|
46
|
+
@reloaded_modules << a_module
|
47
|
+
a_module
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_attribute!(sym, raise_when_missing:)
|
51
|
+
unless @previous_context
|
52
|
+
return super(sym, raise_when_missing: raise_when_missing)
|
53
|
+
end
|
54
|
+
|
55
|
+
first_error = nil
|
56
|
+
|
57
|
+
# check this context first. We catch any NameError, to be able to look up
|
58
|
+
# the attribute also in the previous_context.
|
59
|
+
begin
|
60
|
+
return super(sym, raise_when_missing: true)
|
61
|
+
rescue NameError => e
|
62
|
+
first_error = e
|
63
|
+
end
|
64
|
+
|
65
|
+
# check previous_context
|
66
|
+
begin
|
67
|
+
return @previous_context.fetch_attribute!(sym, raise_when_missing: raise_when_missing)
|
68
|
+
rescue NameError
|
69
|
+
:nop
|
70
|
+
end
|
71
|
+
|
72
|
+
# Not in +self+, not in +previous_context+, and +raise_when_missing+ is true:
|
73
|
+
raise(first_error)
|
74
|
+
end
|
75
|
+
|
76
|
+
# def inspect
|
77
|
+
# if @previous_context
|
78
|
+
# "#{object_id} [" + @hsh.keys.map(&:inspect).join(", ") + "; #{@previous_context.inspect}]"
|
79
|
+
# else
|
80
|
+
# "#{object_id} [" + @hsh.keys.map(&:inspect).join(", ") + "]"
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
IDENTIFIER = "[a-z_][a-z0-9_]*" # @private
|
87
|
+
|
88
|
+
def method_missing(sym, *args, &block)
|
89
|
+
raise ArgumentError, "#{self.class.name}##{sym}: Block given" if block
|
90
|
+
raise ArgumentError, "#{self.class.name}##{sym}: Extra args #{args.inspect}" unless args.empty?
|
91
|
+
|
92
|
+
if sym !~ /\A(#{IDENTIFIER})(\?)?\z/
|
93
|
+
raise ArgumentError, "#{self.class.name}: Invalid context key '#{sym}'"
|
94
|
+
end
|
95
|
+
|
96
|
+
# rubocop:disable Lint/OutOfRangeRegexpRef
|
97
|
+
fetch_attribute!($1, raise_when_missing: $2.nil?)
|
98
|
+
# rubocop:enable Lint/OutOfRangeRegexpRef
|
99
|
+
end
|
100
|
+
|
101
|
+
def respond_to_missing?(sym, include_private = false)
|
102
|
+
super || @previous_context&.respond_to_missing?(sym, include_private)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Simple::Workflow
|
2
|
+
module CurrentContext
|
3
|
+
# Returns the current context.
|
4
|
+
#
|
5
|
+
# This method never returns nil - it raises a ContextMissingError exception if
|
6
|
+
# the context was not initialized (via <tt>Simple::Workflow.with_context</tt>).
|
7
|
+
def current_context
|
8
|
+
Thread.current[:"Simple::Workflow.current_context"] || raise(ContextMissingError)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns a logger
|
12
|
+
#
|
13
|
+
# Returns a logger if a context is set and contains a logger.
|
14
|
+
def logger
|
15
|
+
current_context = Thread.current[:"Simple::Workflow.current_context"]
|
16
|
+
current_context&.logger?
|
17
|
+
end
|
18
|
+
|
19
|
+
# yields a block with a given context, and restores the previous context
|
20
|
+
# object afterwards.
|
21
|
+
def with_context(ctx = nil, &block)
|
22
|
+
old_ctx = Thread.current[:"Simple::Workflow.current_context"]
|
23
|
+
|
24
|
+
Thread.current[:"Simple::Workflow.current_context"] = Context.new(ctx, old_ctx)
|
25
|
+
|
26
|
+
block.call
|
27
|
+
ensure
|
28
|
+
Thread.current[:"Simple::Workflow.current_context"] = old_ctx
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
extend CurrentContext
|
33
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# The Simple::Workflow::Reloader provides a way to locate and reload a module
|
2
|
+
module Simple::Workflow::Reloader
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def reload(a_module)
|
6
|
+
source_paths = locate(a_module)
|
7
|
+
if source_paths.empty?
|
8
|
+
logger&.warn "#{a_module}: cannot reload module: cannot find sources"
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
source_paths.each do |source_path|
|
13
|
+
logger&.debug "#{a_module}: reload #{source_path}"
|
14
|
+
end
|
15
|
+
|
16
|
+
logger&.info "#{a_module}: reloaded module"
|
17
|
+
end
|
18
|
+
|
19
|
+
# This method tries to identify source files for a module's functions.
|
20
|
+
def locate(a_module)
|
21
|
+
expect! a_module => Module
|
22
|
+
|
23
|
+
@registered_source_paths ||= {}
|
24
|
+
@registered_source_paths[a_module.name] ||= locate_source_paths(a_module)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def logger
|
30
|
+
::Simple::Workflow.logger
|
31
|
+
end
|
32
|
+
|
33
|
+
def locate_source_paths(a_module)
|
34
|
+
source_paths = []
|
35
|
+
|
36
|
+
source_paths.concat locate_by_instance_methods(a_module)
|
37
|
+
source_paths.concat locate_by_methods(a_module)
|
38
|
+
source_paths.concat locate_by_name(a_module)
|
39
|
+
|
40
|
+
source_paths.uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
def locate_by_instance_methods(a_module)
|
44
|
+
method_names = a_module.instance_methods(false)
|
45
|
+
methods = method_names.map { |sym| a_module.instance_method(sym) }
|
46
|
+
methods.map(&:source_location).map(&:first)
|
47
|
+
end
|
48
|
+
|
49
|
+
def locate_by_methods(a_module)
|
50
|
+
method_names = a_module.methods(false)
|
51
|
+
methods = method_names.map { |sym| a_module.method(sym) }
|
52
|
+
methods.map(&:source_location).map(&:first)
|
53
|
+
end
|
54
|
+
|
55
|
+
def locate_by_name(a_module)
|
56
|
+
source_file = "#{underscore(a_module.name)}.rb"
|
57
|
+
|
58
|
+
$LOAD_PATH.each do |dir|
|
59
|
+
full_path = File.join(dir, source_file)
|
60
|
+
return [full_path] if File.exist?(full_path)
|
61
|
+
end
|
62
|
+
|
63
|
+
[]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Makes an underscored, lowercase form from the expression in the string.
|
67
|
+
#
|
68
|
+
# Changes '::' to '/' to convert namespaces to paths.
|
69
|
+
#
|
70
|
+
# This is copied and slightly changed (we don't support any custom
|
71
|
+
# inflections) from activesupport's lib/active_support/inflector/methods.rb
|
72
|
+
#
|
73
|
+
def underscore(camel_cased_word)
|
74
|
+
return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
|
75
|
+
|
76
|
+
word = camel_cased_word.to_s.gsub("::", "/")
|
77
|
+
|
78
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
79
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
80
|
+
word.tr!("-", "_")
|
81
|
+
word.downcase!
|
82
|
+
word
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ::Simple::Workflow::RSpecHelper
|
2
|
+
def self.included(base)
|
3
|
+
base.let(:current_context) { {} }
|
4
|
+
|
5
|
+
base.around do |example|
|
6
|
+
if (ctx = current_context)
|
7
|
+
::Simple::Workflow.with_context(ctx) do
|
8
|
+
example.run
|
9
|
+
end
|
10
|
+
else
|
11
|
+
example.run
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "simple/service"
|
2
|
+
|
3
|
+
require_relative "workflow/context"
|
4
|
+
require_relative "workflow/current_context"
|
5
|
+
require_relative "workflow/reloader"
|
6
|
+
|
7
|
+
if defined?(RSpec)
|
8
|
+
require_relative "workflow/rspec_helper"
|
9
|
+
end
|
10
|
+
|
11
|
+
module Simple::Workflow
|
12
|
+
class ContextMissingError < ::StandardError
|
13
|
+
def to_s
|
14
|
+
"Simple::Workflow.current_context not initialized; remember to call Simple::Workflow.with_context/1"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module HelperMethods
|
19
|
+
def invoke(*args, **kwargs)
|
20
|
+
Simple::Workflow.invoke(self, *args, **kwargs)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceHelperMethods
|
25
|
+
private
|
26
|
+
|
27
|
+
def current_context
|
28
|
+
Simple::Workflow.current_context
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ModuleMethods
|
33
|
+
def register_workflow(mod)
|
34
|
+
expect! mod => Module
|
35
|
+
|
36
|
+
mod.extend ::Simple::Workflow::HelperMethods
|
37
|
+
mod.include ::Simple::Workflow::InstanceHelperMethods
|
38
|
+
mod.extend mod
|
39
|
+
mod.include Simple::Service
|
40
|
+
end
|
41
|
+
|
42
|
+
def reload_on_invocation?
|
43
|
+
!!@reload_on_invocation
|
44
|
+
end
|
45
|
+
|
46
|
+
def reload_on_invocation!
|
47
|
+
@reload_on_invocation = true
|
48
|
+
end
|
49
|
+
|
50
|
+
def invoke(workflow, *args, **kwargs)
|
51
|
+
# This call to Simple::Workflow.current_context raises a ContextMissingError
|
52
|
+
# if the context is not set.
|
53
|
+
_ = ::Simple::Workflow.current_context
|
54
|
+
|
55
|
+
expect! workflow => [Module, String]
|
56
|
+
|
57
|
+
workflow_module = lookup_workflow!(workflow)
|
58
|
+
|
59
|
+
# We will reload workflow modules only once per invocation.
|
60
|
+
if Simple::Workflow.reload_on_invocation?
|
61
|
+
Simple::Workflow.current_context.reload!(workflow_module)
|
62
|
+
end
|
63
|
+
|
64
|
+
Simple::Service.invoke(workflow_module, :call, args: args, flags: kwargs.transform_keys(&:to_s))
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def lookup_workflow!(workflow)
|
70
|
+
workflow_module = lookup_workflow(workflow)
|
71
|
+
|
72
|
+
verify_workflow! workflow_module
|
73
|
+
|
74
|
+
workflow_module
|
75
|
+
end
|
76
|
+
|
77
|
+
def lookup_workflow(workflow)
|
78
|
+
case workflow
|
79
|
+
when Module
|
80
|
+
workflow
|
81
|
+
when String
|
82
|
+
Object.const_get workflow
|
83
|
+
else
|
84
|
+
expect! workflow => [Module, String]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def verify_workflow!(workflow_module)
|
89
|
+
return if Simple::Service.actions(workflow_module).key?(:call)
|
90
|
+
|
91
|
+
raise ArgumentError, "#{workflow_module.name} is not a Simple::Workflow"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
extend ModuleMethods
|
96
|
+
end
|
data/scripts/test
ADDED
data/simple-service.gemspec
CHANGED
@@ -3,14 +3,6 @@
|
|
3
3
|
require "spec_helper"
|
4
4
|
|
5
5
|
describe "Simple::Service.invoke3" do
|
6
|
-
# the context to use in the around hook below. By default this is nil -
|
7
|
-
# which gives us an empty context.
|
8
|
-
let(:context) { nil }
|
9
|
-
|
10
|
-
around do |example|
|
11
|
-
::Simple::Service.with_context(context) { example.run }
|
12
|
-
end
|
13
|
-
|
14
6
|
let(:service) { InvokeTestService }
|
15
7
|
let(:action) { nil }
|
16
8
|
|
@@ -1,16 +1,7 @@
|
|
1
1
|
# rubocop:disable Style/WordArray
|
2
|
-
|
3
2
|
require "spec_helper"
|
4
3
|
|
5
4
|
describe "Simple::Service.invoke" do
|
6
|
-
# the context to use in the around hook below. By default this is nil -
|
7
|
-
# which gives us an empty context.
|
8
|
-
let(:context) { nil }
|
9
|
-
|
10
|
-
around do |example|
|
11
|
-
::Simple::Service.with_context(context) { example.run }
|
12
|
-
end
|
13
|
-
|
14
5
|
let(:service) { InvokeTestService }
|
15
6
|
let(:action) { nil }
|
16
7
|
|
@@ -6,12 +6,6 @@ describe "Simple::Service" do
|
|
6
6
|
context "when running against a NoService module" do
|
7
7
|
let(:service) { NoServiceModule }
|
8
8
|
|
9
|
-
describe ".service?" do
|
10
|
-
it "returns false on a NoService module" do
|
11
|
-
expect(Simple::Service.service?(service)).to eq(false)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
9
|
describe ".actions" do
|
16
10
|
it "raises an argument error" do
|
17
11
|
expect { Simple::Service.actions(service) }.to raise_error(ArgumentError)
|
@@ -26,9 +20,7 @@ describe "Simple::Service" do
|
|
26
20
|
|
27
21
|
describe ".invoke3" do
|
28
22
|
it "raises an argument error" do
|
29
|
-
|
30
|
-
expect { Simple::Service.invoke3(service, :service1, {}, {}, context: nil) }.to raise_error(::ArgumentError)
|
31
|
-
end
|
23
|
+
expect { Simple::Service.invoke3(service, :service1, {}, {}, context: nil) }.to raise_error(::ArgumentError)
|
32
24
|
end
|
33
25
|
end
|
34
26
|
end
|
@@ -36,12 +28,6 @@ describe "Simple::Service" do
|
|
36
28
|
# running against a proper service module
|
37
29
|
let(:service) { SpecService }
|
38
30
|
|
39
|
-
describe ".service?" do
|
40
|
-
it "returns true" do
|
41
|
-
expect(Simple::Service.service?(service)).to eq(true)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
31
|
describe ".actions" do
|
46
32
|
it "returns a Hash of actions on a Service module" do
|
47
33
|
actions = Simple::Service.actions(service)
|
@@ -83,26 +69,11 @@ describe "Simple::Service" do
|
|
83
69
|
Simple::Service.invoke3(service, :service1, "my_a", "my_b", d: "my_d")
|
84
70
|
end
|
85
71
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
expect(action).not_to receive(:invoke)
|
90
|
-
|
91
|
-
expect do
|
92
|
-
invoke3
|
93
|
-
end.to raise_error(::Simple::Service::ContextMissingError)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
context "when context is set" do
|
98
|
-
it "calls Action#invoke with the right arguments" do
|
99
|
-
action = Simple::Service.actions(service)[:service1]
|
100
|
-
expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" })
|
72
|
+
it "calls Action#invoke with the right arguments" do
|
73
|
+
action = Simple::Service.actions(service)[:service1]
|
74
|
+
expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" })
|
101
75
|
|
102
|
-
|
103
|
-
invoke3
|
104
|
-
end
|
105
|
-
end
|
76
|
+
invoke3
|
106
77
|
end
|
107
78
|
end
|
108
79
|
|
@@ -112,26 +83,11 @@ describe "Simple::Service" do
|
|
112
83
|
Simple::Service.invoke(service, :service1, args: ["my_a", "my_b"], flags: { "d" => "my_d" })
|
113
84
|
end
|
114
85
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
expect(action).not_to receive(:invoke)
|
119
|
-
|
120
|
-
expect do
|
121
|
-
invoke
|
122
|
-
end.to raise_error(::Simple::Service::ContextMissingError)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
context "when context is set" do
|
127
|
-
it "calls Action#invoke with the right arguments" do
|
128
|
-
action = Simple::Service.actions(service)[:service1]
|
129
|
-
expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" }).and_call_original
|
86
|
+
it "calls Action#invoke with the right arguments" do
|
87
|
+
action = Simple::Service.actions(service)[:service1]
|
88
|
+
expect(action).to receive(:invoke).with(args: ["my_a", "my_b"], flags: { "d" => "my_d" }).and_call_original
|
130
89
|
|
131
|
-
|
132
|
-
invoke
|
133
|
-
end
|
134
|
-
end
|
90
|
+
invoke
|
135
91
|
end
|
136
92
|
end
|
137
93
|
end
|
@@ -147,12 +103,11 @@ describe "Simple::Service" do
|
|
147
103
|
|
148
104
|
it "calls Action#invoke with the right arguments" do
|
149
105
|
expected = ["bar-value", "baz-value"]
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
end
|
106
|
+
|
107
|
+
expect(invoke3("bar-value", baz: "baz-value")).to eq(expected)
|
108
|
+
expect(invoke3(bar: "bar-value", baz: "baz-value")).to eq(expected)
|
109
|
+
expect(invoke(args: ["bar-value"], flags: { "baz" => "baz-value" })).to eq(expected)
|
110
|
+
expect(invoke(args: { "bar" => "bar-value", "baz" => "baz-value" })).to eq(expected)
|
156
111
|
end
|
157
112
|
end
|
158
113
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Simple::Workflow::Context do
|
4
|
+
RSpec.shared_examples "context requesting" do
|
5
|
+
it "inherits from Simple::Immutable" do
|
6
|
+
expect(context).to be_a(Simple::Immutable)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "checking for identifier" do
|
10
|
+
it "raises an ArgumentError if the key is invalid" do
|
11
|
+
expect { context.oneTwoThree? }.to raise_error(ArgumentError)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns nil if the key is not set" do
|
15
|
+
expect(context.two?).to be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "returns the value if the key is set" do
|
19
|
+
expect(context.one?).to eq 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "fetching identifier" do
|
24
|
+
it "raises an ArgumentError if the identifier is invalid" do
|
25
|
+
expect { context.one! }.to raise_error(ArgumentError)
|
26
|
+
expect { context.oneTwoThree }.to raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "raises a NoMethodError if the key is not set" do
|
30
|
+
expect { context.two }.to raise_error(NameError)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns the value if the key is set" do
|
34
|
+
expect(context.one).to eq 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#merge" do
|
39
|
+
context "with symbolized keys" do
|
40
|
+
it "sets a value if it does not exist yet" do
|
41
|
+
expect(context.two?).to eq(nil)
|
42
|
+
new_context = Simple::Workflow::Context.new({two: 2}, context)
|
43
|
+
expect(new_context.two).to eq(2)
|
44
|
+
|
45
|
+
# doesn't change the source context
|
46
|
+
expect(context.two?).to eq(nil)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "sets a value if it does exist" do
|
50
|
+
new_context = Simple::Workflow::Context.new({one: 2}, context)
|
51
|
+
expect(new_context.one).to eq(2)
|
52
|
+
|
53
|
+
# doesn't change the source context
|
54
|
+
expect(context.one).to eq(1)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with stringified keys" do
|
59
|
+
it "sets a value if it does not exist yet" do
|
60
|
+
expect(context.two?).to eq(nil)
|
61
|
+
new_context = Simple::Workflow::Context.new({"two" => 2}, context)
|
62
|
+
expect(new_context.two).to eq(2)
|
63
|
+
|
64
|
+
# doesn't change the source context
|
65
|
+
expect(context.two?).to eq(nil)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "sets a value if it does exist" do
|
69
|
+
new_context = Simple::Workflow::Context.new({"one" => 2}, context)
|
70
|
+
expect(new_context.one).to eq(2)
|
71
|
+
|
72
|
+
# doesn't change the source context
|
73
|
+
expect(context.one).to eq(1)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "with a symbolized context" do
|
80
|
+
let(:context) { Simple::Workflow::Context.new(one: 1) }
|
81
|
+
|
82
|
+
it_behaves_like "context requesting"
|
83
|
+
end
|
84
|
+
|
85
|
+
context "with a stringified context" do
|
86
|
+
let(:context) { Simple::Workflow::Context.new("one" => 1) }
|
87
|
+
|
88
|
+
it_behaves_like "context requesting"
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Simple::Workflow::CurrentContext do
|
4
|
+
describe ".with_context" do
|
5
|
+
it "merges the current context for the duration of the block" do
|
6
|
+
block_called = false
|
7
|
+
|
8
|
+
Simple::Workflow.with_context(a: "a", foo: "bar") do
|
9
|
+
expect(Simple::Workflow.current_context.a).to eq("a")
|
10
|
+
expect(Simple::Workflow.current_context.foo).to eq("bar")
|
11
|
+
|
12
|
+
# layering
|
13
|
+
Simple::Workflow.with_context() do
|
14
|
+
expect(Simple::Workflow.current_context.foo).to eq("bar")
|
15
|
+
|
16
|
+
expect(Simple::Workflow.current_context.unknown?).to be_nil
|
17
|
+
expect {
|
18
|
+
Simple::Workflow.current_context.unknown
|
19
|
+
}.to raise_error(NameError)
|
20
|
+
end
|
21
|
+
|
22
|
+
# overwrite value
|
23
|
+
Simple::Workflow.with_context(a: "b") do
|
24
|
+
expect(Simple::Workflow.current_context.a).to eq("b")
|
25
|
+
block_called = true
|
26
|
+
end
|
27
|
+
|
28
|
+
# overwrite value w/nil
|
29
|
+
Simple::Workflow.with_context(a: nil) do
|
30
|
+
expect(Simple::Workflow.current_context.a).to be_nil
|
31
|
+
Simple::Workflow.with_context(a: "c") do
|
32
|
+
expect(Simple::Workflow.current_context.a).to eq("c")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
expect(Simple::Workflow.current_context.a).to eq("a")
|
36
|
+
end
|
37
|
+
|
38
|
+
expect(block_called).to eq(true)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require_relative "reloader_spec/example1"
|
4
|
+
require_relative "reloader_spec/example2"
|
5
|
+
|
6
|
+
describe "Simple::Service::Reloader" do
|
7
|
+
describe ".locate" do
|
8
|
+
def locate(mod)
|
9
|
+
Simple::Workflow::Reloader.locate(mod)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "Returns all source files of a module" do
|
13
|
+
root = Dir.getwd
|
14
|
+
|
15
|
+
expected = [
|
16
|
+
"#{root}/lib/simple/workflow/reloader.rb"
|
17
|
+
]
|
18
|
+
expect(locate(Simple::Workflow::Reloader)).to contain_exactly(*expected)
|
19
|
+
|
20
|
+
expected = [
|
21
|
+
"#{__dir__}/reloader_spec/example1.rb",
|
22
|
+
"#{__dir__}/reloader_spec/example2.rb"
|
23
|
+
]
|
24
|
+
expect(locate(ReloaderSpecExample1)).to contain_exactly(*expected)
|
25
|
+
|
26
|
+
expected = [
|
27
|
+
"#{__dir__}/reloader_spec/example2.rb"
|
28
|
+
]
|
29
|
+
expect(locate(ReloaderSpecExample2)).to contain_exactly(*expected)
|
30
|
+
|
31
|
+
expected = [
|
32
|
+
"#{__dir__}/reloader_spec/example1.rb"
|
33
|
+
]
|
34
|
+
expect(locate(ReloaderSpecExample3)).to contain_exactly(*expected)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe ".reload" do
|
39
|
+
def reload(mod)
|
40
|
+
Simple::Workflow::Reloader.reload(mod)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "Reloads a module" do
|
44
|
+
# [TODO] this doesn't really check reloading...
|
45
|
+
reload Simple::Workflow::Reloader
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
ENV["RACK_ENV"] = "test"
|
2
2
|
ENV["RAILS_ENV"] = "test"
|
3
3
|
|
4
|
-
require "byebug"
|
4
|
+
require "pry-byebug"
|
5
5
|
require "rspec"
|
6
6
|
|
7
7
|
require "simplecov"
|
@@ -18,6 +18,7 @@ SimpleCov.start do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
require "simple/service"
|
21
|
+
require "simple/workflow"
|
21
22
|
|
22
23
|
Dir.glob("./spec/support/**/*.rb").sort.each { |path| load path }
|
23
24
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple-service
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- radiospiel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-10-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: expectation
|
@@ -24,6 +24,26 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: simple-immutable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '1.1'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.1'
|
27
47
|
description: Pretty simple and somewhat abstract service description
|
28
48
|
email: eno@radiospiel.org
|
29
49
|
executables: []
|
@@ -75,26 +95,36 @@ files:
|
|
75
95
|
- doc/method_list.html
|
76
96
|
- doc/top-level-namespace.html
|
77
97
|
- lib/simple-service.rb
|
98
|
+
- lib/simple-workflow.rb
|
78
99
|
- lib/simple/service.rb
|
79
100
|
- lib/simple/service/action.rb
|
80
101
|
- lib/simple/service/action/comment.rb
|
81
102
|
- lib/simple/service/action/method_reflection.rb
|
82
103
|
- lib/simple/service/action/parameter.rb
|
83
|
-
- lib/simple/service/context.rb
|
84
104
|
- lib/simple/service/errors.rb
|
85
105
|
- lib/simple/service/version.rb
|
106
|
+
- lib/simple/workflow.rb
|
107
|
+
- lib/simple/workflow/context.rb
|
108
|
+
- lib/simple/workflow/current_context.rb
|
109
|
+
- lib/simple/workflow/reloader.rb
|
110
|
+
- lib/simple/workflow/rspec_helper.rb
|
86
111
|
- log/.gitkeep
|
87
112
|
- scripts/release
|
88
113
|
- scripts/release.rb
|
89
114
|
- scripts/stats
|
115
|
+
- scripts/test
|
90
116
|
- scripts/watch
|
91
117
|
- simple-service.gemspec
|
92
118
|
- spec/simple/service/action_invoke3_spec.rb
|
93
119
|
- spec/simple/service/action_invoke_spec.rb
|
94
120
|
- spec/simple/service/action_spec.rb
|
95
|
-
- spec/simple/service/context_spec.rb
|
96
121
|
- spec/simple/service/service_spec.rb
|
97
122
|
- spec/simple/service/version_spec.rb
|
123
|
+
- spec/simple/workflow/context_spec.rb
|
124
|
+
- spec/simple/workflow/current_context_spec.rb
|
125
|
+
- spec/simple/workflow/reloader_spec.rb
|
126
|
+
- spec/simple/workflow/reloader_spec/example1.rb
|
127
|
+
- spec/simple/workflow/reloader_spec/example2.rb
|
98
128
|
- spec/spec_helper.rb
|
99
129
|
- spec/support/spec_services.rb
|
100
130
|
homepage: http://github.com/radiospiel/simple-service
|
@@ -115,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
145
|
- !ruby/object:Gem::Version
|
116
146
|
version: '0'
|
117
147
|
requirements: []
|
118
|
-
rubygems_version: 3.
|
148
|
+
rubygems_version: 3.1.4
|
119
149
|
signing_key:
|
120
150
|
specification_version: 4
|
121
151
|
summary: Pretty simple and somewhat abstract service description
|
@@ -123,8 +153,12 @@ test_files:
|
|
123
153
|
- spec/simple/service/action_invoke3_spec.rb
|
124
154
|
- spec/simple/service/action_invoke_spec.rb
|
125
155
|
- spec/simple/service/action_spec.rb
|
126
|
-
- spec/simple/service/context_spec.rb
|
127
156
|
- spec/simple/service/service_spec.rb
|
128
157
|
- spec/simple/service/version_spec.rb
|
158
|
+
- spec/simple/workflow/context_spec.rb
|
159
|
+
- spec/simple/workflow/current_context_spec.rb
|
160
|
+
- spec/simple/workflow/reloader_spec.rb
|
161
|
+
- spec/simple/workflow/reloader_spec/example1.rb
|
162
|
+
- spec/simple/workflow/reloader_spec/example2.rb
|
129
163
|
- spec/spec_helper.rb
|
130
164
|
- spec/support/spec_services.rb
|
@@ -1,94 +0,0 @@
|
|
1
|
-
module Simple::Service
|
2
|
-
# Returns the current context.
|
3
|
-
def self.context
|
4
|
-
Thread.current[:"Simple::Service.context"]
|
5
|
-
end
|
6
|
-
|
7
|
-
# yields a block with a given context, and restores the previous context
|
8
|
-
# object afterwards.
|
9
|
-
def self.with_context(ctx = nil, &block)
|
10
|
-
old_ctx = Thread.current[:"Simple::Service.context"]
|
11
|
-
new_ctx = old_ctx ? old_ctx.merge(ctx) : Context.new(ctx)
|
12
|
-
|
13
|
-
Thread.current[:"Simple::Service.context"] = new_ctx
|
14
|
-
|
15
|
-
block.call
|
16
|
-
ensure
|
17
|
-
Thread.current[:"Simple::Service.context"] = old_ctx
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
module Simple::Service
|
22
|
-
# A context object
|
23
|
-
#
|
24
|
-
# Each service executes with a current context. The system manages a stack of
|
25
|
-
# contexts; whenever a service execution is done the current context is reverted
|
26
|
-
# to its previous value.
|
27
|
-
#
|
28
|
-
# A context object can store a large number of values; the only way to set or
|
29
|
-
# access a value is via getters and setters. These are implemented via
|
30
|
-
# +method_missing(..)+.
|
31
|
-
#
|
32
|
-
# Also, once a value is set in the context it is not possible to change or
|
33
|
-
# unset it.
|
34
|
-
class Context
|
35
|
-
def initialize(hsh = nil) # @private
|
36
|
-
@hsh = hsh || {}
|
37
|
-
end
|
38
|
-
|
39
|
-
# returns a new Context object, which merges the values in the +overlay+
|
40
|
-
# argument (which must be a Hash or nil) with the values in this context.
|
41
|
-
#
|
42
|
-
# The overlay is allowed to change values in the current context.
|
43
|
-
#
|
44
|
-
# It does not change this context.
|
45
|
-
def merge(overlay)
|
46
|
-
expect! overlay => [Hash, nil]
|
47
|
-
|
48
|
-
overlay ||= {}
|
49
|
-
new_context_hsh = @hsh.merge(overlay)
|
50
|
-
::Simple::Service::Context.new(new_context_hsh)
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
|
56
|
-
IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
|
57
|
-
ASSIGNMENT_REGEXP = Regexp.compile("\\A(#{IDENTIFIER_PATTERN})=\\z") # @private
|
58
|
-
|
59
|
-
def method_missing(sym, *args, &block)
|
60
|
-
raise ArgumentError, "Block given" if block
|
61
|
-
|
62
|
-
if args.count == 0 && sym =~ IDENTIFIER_REGEXP
|
63
|
-
self[sym]
|
64
|
-
elsif args.count == 1 && sym =~ ASSIGNMENT_REGEXP
|
65
|
-
self[$1.to_sym] = args.first
|
66
|
-
else
|
67
|
-
super
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def respond_to_missing?(sym, include_private = false)
|
72
|
-
# :nocov:
|
73
|
-
return true if IDENTIFIER_REGEXP.match?(sym)
|
74
|
-
return true if ASSIGNMENT_REGEXP.match?(sym)
|
75
|
-
|
76
|
-
super
|
77
|
-
# :nocov:
|
78
|
-
end
|
79
|
-
|
80
|
-
def [](key)
|
81
|
-
@hsh[key]
|
82
|
-
end
|
83
|
-
|
84
|
-
def []=(key, value)
|
85
|
-
existing_value = @hsh[key]
|
86
|
-
|
87
|
-
unless existing_value.nil? || existing_value == value
|
88
|
-
raise ::Simple::Service::ContextReadOnlyError, key
|
89
|
-
end
|
90
|
-
|
91
|
-
@hsh[key] = value
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe Simple::Service do
|
4
|
-
describe ".with_context" do
|
5
|
-
it "merges the current context for the duration of the block" do
|
6
|
-
block_called = false
|
7
|
-
|
8
|
-
Simple::Service.with_context(a: "a") do
|
9
|
-
expect(Simple::Service.context.a).to eq("a")
|
10
|
-
|
11
|
-
# overwrite value
|
12
|
-
Simple::Service.with_context(a: "b") do
|
13
|
-
expect(Simple::Service.context.a).to eq("b")
|
14
|
-
block_called = true
|
15
|
-
end
|
16
|
-
|
17
|
-
# overwrite value w/nil
|
18
|
-
Simple::Service.with_context(a: nil) do
|
19
|
-
expect(Simple::Service.context.a).to be_nil
|
20
|
-
Simple::Service.context.a = "c"
|
21
|
-
expect(Simple::Service.context.a).to eq("c")
|
22
|
-
end
|
23
|
-
expect(Simple::Service.context.a).to eq("a")
|
24
|
-
end
|
25
|
-
|
26
|
-
expect(block_called).to eq(true)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
describe Simple::Service::Context do
|
32
|
-
let(:context) { Simple::Service::Context.new }
|
33
|
-
|
34
|
-
before do
|
35
|
-
context.one = 1
|
36
|
-
end
|
37
|
-
|
38
|
-
describe "invalid identifier" do
|
39
|
-
it "raises an error" do
|
40
|
-
expect { context.one! }.to raise_error(NoMethodError)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
describe "context reading" do
|
45
|
-
it "returns a value if set" do
|
46
|
-
expect(context.one).to eq(1)
|
47
|
-
end
|
48
|
-
|
49
|
-
it "returns nil if not set" do
|
50
|
-
expect(context.two).to be_nil
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
describe "context writing" do
|
55
|
-
it "sets a value if it does not exist yet" do
|
56
|
-
context.two = 2
|
57
|
-
expect(context.two).to eq(2)
|
58
|
-
end
|
59
|
-
|
60
|
-
it "raises a ReadOnly error if the value exists and is not equal" do
|
61
|
-
expect { context.one = 2 }.to raise_error(::Simple::Service::ContextReadOnlyError)
|
62
|
-
end
|
63
|
-
|
64
|
-
it "sets the value if it exists and is equal" do
|
65
|
-
context.one = 1
|
66
|
-
expect(context.one).to eq(1)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|