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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -2
  3. data/.rubocop.yml +10 -2
  4. data/Gemfile +3 -1
  5. data/Makefile +7 -11
  6. data/README.md +67 -2
  7. data/TODO.txt +3 -0
  8. data/VERSION +1 -1
  9. data/doc/Simple/Service/Action/Comment/Extractor.html +347 -0
  10. data/doc/Simple/Service/Action/Comment.html +451 -0
  11. data/doc/Simple/Service/Action/MethodReflection.html +285 -0
  12. data/doc/Simple/Service/Action/Parameter.html +816 -0
  13. data/doc/Simple/Service/Action.html +923 -0
  14. data/doc/Simple/Service/ArgumentError.html +128 -0
  15. data/doc/Simple/Service/ClassMethods.html +187 -0
  16. data/doc/Simple/Service/Context.html +379 -0
  17. data/doc/Simple/Service/ContextMissingError.html +124 -0
  18. data/doc/Simple/Service/ContextReadOnlyError.html +206 -0
  19. data/doc/Simple/Service/ExtraArguments.html +428 -0
  20. data/doc/Simple/Service/GemHelper.html +190 -0
  21. data/doc/Simple/Service/MissingArguments.html +426 -0
  22. data/doc/Simple/Service/NoSuchAction.html +433 -0
  23. data/doc/Simple/Service.html +865 -0
  24. data/doc/Simple.html +117 -0
  25. data/doc/_index.html +274 -0
  26. data/doc/class_list.html +51 -0
  27. data/doc/css/common.css +1 -0
  28. data/doc/css/full_list.css +58 -0
  29. data/doc/css/style.css +496 -0
  30. data/doc/file.README.html +146 -0
  31. data/doc/file.TODO.html +70 -0
  32. data/doc/file_list.html +61 -0
  33. data/doc/frames.html +17 -0
  34. data/doc/index.html +146 -0
  35. data/doc/js/app.js +303 -0
  36. data/doc/js/full_list.js +216 -0
  37. data/doc/js/jquery.js +4 -0
  38. data/doc/method_list.html +483 -0
  39. data/doc/top-level-namespace.html +110 -0
  40. data/lib/simple/service/action/comment.rb +2 -2
  41. data/lib/simple/service/action/method_reflection.rb +1 -1
  42. data/lib/simple/service/action/parameter.rb +1 -1
  43. data/lib/simple/service/action.rb +34 -46
  44. data/lib/simple/service/errors.rb +4 -3
  45. data/lib/simple/service/version.rb +2 -2
  46. data/lib/simple/service.rb +109 -34
  47. data/lib/simple/workflow/context.rb +105 -0
  48. data/lib/simple/workflow/current_context.rb +33 -0
  49. data/lib/simple/workflow/reloader.rb +84 -0
  50. data/lib/simple/workflow/rspec_helper.rb +15 -0
  51. data/lib/simple/workflow.rb +96 -0
  52. data/lib/simple-workflow.rb +3 -0
  53. data/scripts/test +2 -0
  54. data/simple-service.gemspec +1 -0
  55. data/spec/simple/service/action_invoke3_spec.rb +258 -0
  56. data/spec/simple/service/action_invoke_spec.rb +49 -87
  57. data/spec/simple/service/service_spec.rb +40 -32
  58. data/spec/simple/workflow/context_spec.rb +90 -0
  59. data/spec/simple/workflow/current_context_spec.rb +41 -0
  60. data/spec/simple/workflow/reloader_spec/example1.rb +10 -0
  61. data/spec/simple/workflow/reloader_spec/example2.rb +7 -0
  62. data/spec/simple/workflow/reloader_spec.rb +48 -0
  63. data/spec/spec_helper.rb +2 -1
  64. data/spec/support/spec_services.rb +8 -2
  65. metadata +74 -9
  66. data/lib/simple/service/action/indie_hash.rb +0 -37
  67. data/lib/simple/service/context.rb +0 -94
  68. data/spec/simple/service/action_invoke2_spec.rb +0 -166
  69. 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_]*" # :nodoc:
19
- IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # :nodoc:
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:) # :nodoc:
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 # :nodoc:
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) # :nodoc:
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 invoke2(args:, flags:)
93
- # args and flags are being stringified. This is necessary to not allow any
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) # :nodoc:
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
- Hash[keys.map(&:to_sym).zip(values)]
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
- hsh = {}
181
-
182
- if variadic_parameter
183
- hsh[variadic_parameter.name] = []
184
- end
185
-
186
- if ary.length > positional_names.length
187
- extra_arguments = ary[positional_names.length..-1]
188
-
189
- if variadic_parameter
190
- hsh[variadic_parameter.name] = extra_arguments
191
- else
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
- ary.zip(positional_names).each do |value, parameter_name|
197
- hsh[parameter_name] = value
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}"
@@ -1,8 +1,8 @@
1
- module Simple # :nodoc:
1
+ module Simple # @private
2
2
  end
3
3
 
4
4
  module Simple::Service
5
- module GemHelper # :nodoc:
5
+ module GemHelper # @private
6
6
  extend self
7
7
 
8
8
  def version(name)
@@ -1,33 +1,76 @@
1
- module Simple # :nodoc:
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 module.
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
- # To mark a target module as a service module one must include the
14
- # Simple::Service module into the target module.
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
- def self.included(klass) # :nodoc:
20
- klass.extend ClassMethods
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
- # returns true if the passed in object is a service module.
24
- def self.service?(service)
25
- service.is_a?(Module) && service.include?(self)
59
+ def self.included(klass) # @private
60
+ klass.extend ClassMethods
61
+ klass.include ServiceExpectations
26
62
  end
27
63
 
28
- def self.verify_service!(service) # :nodoc:
29
- raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, but is not even a Module" unless service.is_a?(Module)
30
- raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, did you 'include Simple::Service'" unless service?(service)
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 +arguments+ and +params+.
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
- # You cannot call this method if the context is not set.
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
- # When calling #invoke using positional arguments they will be matched against
53
- # positional arguments of the invoked method - but they will not be matched
54
- # against named arguments.
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, *args, **named_args)
64
- raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke" unless context
65
-
66
- action(service, name).invoke(*args, **named_args)
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).invoke2(args: args, flags: flags)
150
+ action(service, name).invoke(args: args, flags: flags)
76
151
  end
77
152
 
78
- module ClassMethods # :nodoc:
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