simple-service 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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