simple-service 0.1.3 → 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/.gitignore +2 -2
- data/.rubocop.yml +10 -2
- data/Gemfile +3 -1
- data/Makefile +7 -11
- data/README.md +67 -2
- data/TODO.txt +3 -0
- data/VERSION +1 -1
- data/doc/Simple/Service/Action/Comment/Extractor.html +347 -0
- data/doc/Simple/Service/Action/Comment.html +451 -0
- data/doc/Simple/Service/Action/MethodReflection.html +285 -0
- data/doc/Simple/Service/Action/Parameter.html +816 -0
- data/doc/Simple/Service/Action.html +923 -0
- data/doc/Simple/Service/ArgumentError.html +128 -0
- data/doc/Simple/Service/ClassMethods.html +187 -0
- data/doc/Simple/Service/Context.html +379 -0
- data/doc/Simple/Service/ContextMissingError.html +124 -0
- data/doc/Simple/Service/ContextReadOnlyError.html +206 -0
- data/doc/Simple/Service/ExtraArguments.html +428 -0
- data/doc/Simple/Service/GemHelper.html +190 -0
- data/doc/Simple/Service/MissingArguments.html +426 -0
- data/doc/Simple/Service/NoSuchAction.html +433 -0
- data/doc/Simple/Service.html +865 -0
- data/doc/Simple.html +117 -0
- data/doc/_index.html +274 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +496 -0
- data/doc/file.README.html +146 -0
- data/doc/file.TODO.html +70 -0
- data/doc/file_list.html +61 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +146 -0
- data/doc/js/app.js +303 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +483 -0
- data/doc/top-level-namespace.html +110 -0
- data/lib/simple/service/action/comment.rb +2 -2
- data/lib/simple/service/action/method_reflection.rb +1 -1
- data/lib/simple/service/action/parameter.rb +1 -1
- data/lib/simple/service/action.rb +34 -46
- data/lib/simple/service/errors.rb +4 -3
- data/lib/simple/service/version.rb +2 -2
- data/lib/simple/service.rb +109 -34
- 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 +258 -0
- data/spec/simple/service/action_invoke_spec.rb +49 -87
- data/spec/simple/service/service_spec.rb +40 -32
- 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 +8 -2
- metadata +74 -9
- data/lib/simple/service/action/indie_hash.rb +0 -37
- data/lib/simple/service/context.rb +0 -94
- data/spec/simple/service/action_invoke2_spec.rb +0 -166
- data/spec/simple/service/context_spec.rb +0 -69
@@ -1,25 +1,24 @@
|
|
1
1
|
module Simple::Service
|
2
|
+
# rubocop:disable Lint/EmptyClass
|
2
3
|
class Action
|
3
4
|
end
|
4
5
|
end
|
5
6
|
|
6
7
|
require_relative "./action/comment"
|
7
8
|
require_relative "./action/parameter"
|
8
|
-
require_relative "./action/indie_hash"
|
9
9
|
|
10
10
|
module Simple::Service
|
11
11
|
# rubocop:disable Metrics/AbcSize
|
12
12
|
# rubocop:disable Metrics/PerceivedComplexity
|
13
13
|
# rubocop:disable Metrics/CyclomaticComplexity
|
14
|
-
# rubocop:disable Style/GuardClause
|
15
14
|
# rubocop:disable Metrics/ClassLength
|
16
15
|
|
17
16
|
class Action
|
18
|
-
IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" #
|
19
|
-
IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") #
|
17
|
+
IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
|
18
|
+
IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
|
20
19
|
|
21
20
|
# determines all services provided by the +service+ service module.
|
22
|
-
def self.enumerate(service:) #
|
21
|
+
def self.enumerate(service:) # @private
|
23
22
|
service.public_instance_methods(false)
|
24
23
|
.grep(IDENTIFIER_REGEXP)
|
25
24
|
.each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) }
|
@@ -32,7 +31,7 @@ module Simple::Service
|
|
32
31
|
"#{service.name}##{name}"
|
33
32
|
end
|
34
33
|
|
35
|
-
def to_s #
|
34
|
+
def to_s # @private
|
36
35
|
full_name
|
37
36
|
end
|
38
37
|
|
@@ -41,7 +40,7 @@ module Simple::Service
|
|
41
40
|
@parameters ||= Parameter.reflect_on_method(service: service, name: name)
|
42
41
|
end
|
43
42
|
|
44
|
-
def initialize(service, name) #
|
43
|
+
def initialize(service, name) # @private
|
45
44
|
@service = service
|
46
45
|
@name = name
|
47
46
|
|
@@ -71,30 +70,11 @@ module Simple::Service
|
|
71
70
|
@service.instance_method(name).source_location
|
72
71
|
end
|
73
72
|
|
74
|
-
# build a service_instance and run the action, with arguments constructed from
|
75
|
-
# args_hsh and params_hsh.
|
76
|
-
def invoke(*args, **named_args)
|
77
|
-
# convert Array arguments into a Hash of named arguments. This is strictly
|
78
|
-
# necessary to be able to apply default value-based type conversions. (On
|
79
|
-
# the downside this also means we convert an array to a hash and then back
|
80
|
-
# into an array. This, however, should only be an issue for CLI based action
|
81
|
-
# invocations, because any other use case (that I can think of) should allow
|
82
|
-
# us to provide arguments as a Hash.
|
83
|
-
args = convert_argument_array_to_hash(args)
|
84
|
-
named_args = named_args.merge(args)
|
85
|
-
|
86
|
-
invoke2(args: named_args, flags: {})
|
87
|
-
end
|
88
|
-
|
89
73
|
# invokes an action with a given +name+ in a service with a Hash of arguments.
|
90
74
|
#
|
91
75
|
# You cannot call this method if the context is not set.
|
92
|
-
def
|
93
|
-
|
94
|
-
# unchecked input to DOS this process by just providing always changing
|
95
|
-
# key values.
|
96
|
-
args = IndieHash.new(args)
|
97
|
-
flags = IndieHash.new(flags)
|
76
|
+
def invoke(args:, flags:)
|
77
|
+
args = convert_argument_array_to_hash(args) if args.is_a?(Array)
|
98
78
|
|
99
79
|
verify_required_args!(args, flags)
|
100
80
|
|
@@ -116,7 +96,7 @@ module Simple::Service
|
|
116
96
|
private
|
117
97
|
|
118
98
|
# returns an error if the keywords hash does not define all required keyword arguments.
|
119
|
-
def verify_required_args!(args, flags) #
|
99
|
+
def verify_required_args!(args, flags) # @private
|
120
100
|
@required_names ||= parameters.select(&:required?).map(&:name).map(&:to_s)
|
121
101
|
|
122
102
|
missing_parameters = @required_names - args.keys - flags.keys
|
@@ -135,7 +115,7 @@ module Simple::Service
|
|
135
115
|
|
136
116
|
# Note that +keys+ now only contains names of keyword arguments that actually exist.
|
137
117
|
# This is therefore not a way to DOS this process.
|
138
|
-
|
118
|
+
keys.map(&:to_sym).zip(values).to_h
|
139
119
|
end
|
140
120
|
|
141
121
|
def variadic_parameter
|
@@ -145,7 +125,7 @@ module Simple::Service
|
|
145
125
|
end
|
146
126
|
|
147
127
|
def positional_names
|
148
|
-
@positional_names ||= parameters.select(&:positional?).map(&:name)
|
128
|
+
@positional_names ||= parameters.select(&:positional?).map(&:name).map(&:to_s)
|
149
129
|
end
|
150
130
|
|
151
131
|
# Enumerating all parameters it collects all positional parameters into
|
@@ -177,26 +157,34 @@ module Simple::Service
|
|
177
157
|
def convert_argument_array_to_hash(ary)
|
178
158
|
expect! ary => Array
|
179
159
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
160
|
+
# +ary* might contain more, less, or the exact number of positional
|
161
|
+
# arguments. If the number is less, we return a hash with only whart
|
162
|
+
# exists in ary - the action might define default values after all.
|
163
|
+
#
|
164
|
+
# If it contains more the action better supports a variadic parameter;
|
165
|
+
# we otherwise raise a ExtraArguments exception.
|
166
|
+
case ary.length <=> positional_names.length
|
167
|
+
when 1 # i.e. ary.length > positional_names.length
|
168
|
+
extra_arguments = ary[positional_names.length..]
|
169
|
+
ary = ary[0..positional_names.length]
|
170
|
+
|
171
|
+
if !extra_arguments.empty? && !variadic_parameter
|
192
172
|
raise ::Simple::Service::ExtraArguments.new(self, extra_arguments)
|
193
173
|
end
|
194
|
-
end
|
195
174
|
|
196
|
-
|
197
|
-
|
175
|
+
existing_positional_names = positional_names
|
176
|
+
when 0 # i.e. ary.length == positional_names.length
|
177
|
+
existing_positional_names = positional_names
|
178
|
+
when -1 # i.e. ary.length < positional_names.length
|
179
|
+
existing_positional_names = positional_names[0, ary.length]
|
198
180
|
end
|
199
181
|
|
182
|
+
# Build a hash with the existing_positional_names and the values from the array.
|
183
|
+
hsh = existing_positional_names.zip(ary).to_h
|
184
|
+
|
185
|
+
# Add the variadic_parameter, if any.
|
186
|
+
hsh[variadic_parameter.name] = extra_arguments if variadic_parameter
|
187
|
+
|
200
188
|
hsh
|
201
189
|
end
|
202
190
|
end
|
@@ -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,33 +1,76 @@
|
|
1
|
-
module Simple #
|
1
|
+
module Simple # @private
|
2
|
+
end
|
3
|
+
|
4
|
+
module Simple::Service # @private
|
2
5
|
end
|
3
6
|
|
4
7
|
require "expectation"
|
8
|
+
require "logger"
|
5
9
|
|
6
10
|
require_relative "service/errors"
|
7
11
|
require_relative "service/action"
|
8
|
-
require_relative "service/context"
|
9
12
|
require_relative "service/version"
|
10
13
|
|
11
|
-
# The Simple::Service
|
14
|
+
# <b>The Simple::Service interface</b>
|
15
|
+
#
|
16
|
+
# This module implements the main API of the Simple::Service ruby gem.
|
17
|
+
#
|
18
|
+
# 1. <em>Marking a service module:</em> To turn a target module as a service module one must include <tt>Simple::Service</tt>
|
19
|
+
# into the target. This serves as a marker that this module is actually intended
|
20
|
+
# to provide one or more services. Example:
|
21
|
+
#
|
22
|
+
# module GodMode
|
23
|
+
# include Simple::Service
|
24
|
+
#
|
25
|
+
# # Build a universe.
|
26
|
+
# #
|
27
|
+
# # This comment will become part of the full description of the
|
28
|
+
# # "build_universe" service
|
29
|
+
# def build_universe(name, c: , pi: 3.14, e: 2.781)
|
30
|
+
# # at this point I realize that *I* am not God.
|
31
|
+
#
|
32
|
+
# 42 # Best try approach
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# 2. <em>Discover services:</em> To discover services in a service module use the #actions method. This returns a Hash
|
37
|
+
# of actions.
|
38
|
+
#
|
39
|
+
# Simple::Service.actions(GodMode)
|
40
|
+
# => {:build_universe=>#<Simple::Service::Action...>, ...}
|
12
41
|
#
|
13
|
-
#
|
14
|
-
#
|
42
|
+
# TODO: why a Hash? It feels much better if Simple::Service.actions returns an array of names.
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>.
|
46
|
+
#
|
47
|
+
# Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
|
48
|
+
# => 42
|
15
49
|
#
|
16
|
-
# This serves as a marker that this module is actually intended
|
17
|
-
# to be used as a service.
|
18
50
|
module Simple::Service
|
19
|
-
|
20
|
-
|
51
|
+
module ServiceExpectations
|
52
|
+
def expect!(*args, &block)
|
53
|
+
Expectation.expect!(*args, &block)
|
54
|
+
rescue ::Expectation::Error => e
|
55
|
+
raise ArgumentError, e.to_s
|
56
|
+
end
|
21
57
|
end
|
22
58
|
|
23
|
-
|
24
|
-
|
25
|
-
|
59
|
+
def self.included(klass) # @private
|
60
|
+
klass.extend ClassMethods
|
61
|
+
klass.include ServiceExpectations
|
26
62
|
end
|
27
63
|
|
28
|
-
|
29
|
-
|
30
|
-
|
64
|
+
# Raises an error if the passed in object is not a Simple::Service
|
65
|
+
def self.verify_service!(service) # @private
|
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
|
31
74
|
end
|
32
75
|
|
33
76
|
# returns a Hash with all actions in the +service+ module
|
@@ -39,19 +82,58 @@ module Simple::Service
|
|
39
82
|
|
40
83
|
# returns the action with the given name.
|
41
84
|
def self.action(service, name)
|
85
|
+
expect! name => Symbol
|
86
|
+
|
42
87
|
actions = self.actions(service)
|
43
88
|
actions[name] || begin
|
44
89
|
raise ::Simple::Service::NoSuchAction.new(service, name)
|
45
90
|
end
|
46
91
|
end
|
47
92
|
|
48
|
-
# invokes an action with a given +name+ in a service with +
|
93
|
+
# invokes an action with a given +name+ in a service with +args+ and +flags+.
|
94
|
+
#
|
95
|
+
# This is a helper method which one can use to easily call an action from
|
96
|
+
# ruby source code.
|
97
|
+
#
|
98
|
+
# As the main purpose of this module is to call services with outside data,
|
99
|
+
# the +.invoke+ action is usually preferred.
|
100
|
+
def self.invoke3(service, name, *args, **flags)
|
101
|
+
flags = flags.transform_keys(&:to_s)
|
102
|
+
invoke service, name, args: args, flags: flags
|
103
|
+
end
|
104
|
+
|
105
|
+
# invokes an action with a given +name+.
|
106
|
+
#
|
107
|
+
# This is the general form of invoking a service. It accepts the following
|
108
|
+
# arguments:
|
109
|
+
#
|
110
|
+
# - args: an Array of positional arguments OR a Hash of named arguments.
|
111
|
+
# - flags: a Hash of flags.
|
112
|
+
#
|
113
|
+
# Note that the keys in both the +flags+ and the +args+ Hash must be strings.
|
114
|
+
#
|
115
|
+
# The service is being called with a parameters built out of those like this:
|
49
116
|
#
|
50
|
-
#
|
117
|
+
# - The service's positional arguments are being built from the +args+ array
|
118
|
+
# parameter or from the +named_args+ hash parameter.
|
119
|
+
# - The service's keyword arguments are being built from the +named_args+
|
120
|
+
# and +flags+ arguments.
|
51
121
|
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
122
|
+
# In other words:
|
123
|
+
#
|
124
|
+
# 1. You cannot set both +args+ and +named_args+ at the same time.
|
125
|
+
# 2. The +flags+ arguments are only being used to determine the
|
126
|
+
# service's keyword parameters.
|
127
|
+
#
|
128
|
+
# So, if the service X implements an action "def foo(bar, baz:)", the following would
|
129
|
+
# all invoke that service:
|
130
|
+
#
|
131
|
+
# - +Service.invoke3(X, :foo, "bar-value", baz: "baz-value")+, or
|
132
|
+
# - +Service.invoke3(X, :foo, bar: "bar-value", baz: "baz-value")+, or
|
133
|
+
# - +Service.invoke(X, :foo, args: ["bar-value"], flags: { "baz" => "baz-value" })+, or
|
134
|
+
# - +Service.invoke(X, :foo, args: { "bar" => "bar-value", "baz" => "baz-value" })+.
|
135
|
+
#
|
136
|
+
# (see spec/service_spec.rb)
|
55
137
|
#
|
56
138
|
# When there are not enough positional arguments to match the number of required
|
57
139
|
# positional arguments of the method we raise an ArgumentError.
|
@@ -59,23 +141,16 @@ module Simple::Service
|
|
59
141
|
# When there are more positional arguments provided than the number accepted
|
60
142
|
# by the method we raise an ArgumentError.
|
61
143
|
#
|
62
|
-
# Entries in the named_args Hash that are not defined in the action itself are ignored.
|
63
|
-
def self.invoke(service, name,
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
68
|
-
|
69
|
-
# invokes an action with a given +name+ in a service with a Hash of arguments.
|
70
|
-
#
|
71
|
-
# You cannot call this method if the context is not set.
|
72
|
-
def self.invoke2(service, name, args: {}, flags: {})
|
73
|
-
raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke" unless context
|
144
|
+
# Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
|
145
|
+
def self.invoke(service, name, args: {}, flags: {})
|
146
|
+
expect! args => [Hash, Array], flags: Hash
|
147
|
+
args.each_key { |key| expect! key => String } if args.is_a?(Hash)
|
148
|
+
flags.each_key { |key| expect! key => String }
|
74
149
|
|
75
|
-
action(service, name).
|
150
|
+
action(service, name).invoke(args: args, flags: flags)
|
76
151
|
end
|
77
152
|
|
78
|
-
module ClassMethods #
|
153
|
+
module ClassMethods # @private
|
79
154
|
# returns a Hash of actions provided by the service module.
|
80
155
|
def __simple_service_actions__
|
81
156
|
@__simple_service_actions__ ||= Action.enumerate(service: self)
|
@@ -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
|