simple-service 0.1.5 → 0.2.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -2
  3. data/Gemfile +3 -1
  4. data/Makefile +5 -2
  5. data/TODO.txt +3 -0
  6. data/VERSION +1 -1
  7. data/doc/Simple/Service/Action/Comment/Extractor.html +1 -1
  8. data/doc/Simple/Service/Action/Comment.html +1 -1
  9. data/doc/Simple/Service/Action/MethodReflection.html +1 -1
  10. data/doc/Simple/Service/Action/Parameter.html +2 -2
  11. data/doc/Simple/Service/Action.html +59 -150
  12. data/doc/Simple/Service/ArgumentError.html +1 -1
  13. data/doc/Simple/Service/ClassMethods.html +5 -5
  14. data/doc/Simple/Service/Context.html +5 -5
  15. data/doc/Simple/Service/ContextMissingError.html +1 -1
  16. data/doc/Simple/Service/ContextReadOnlyError.html +1 -1
  17. data/doc/Simple/Service/ExtraArguments.html +1 -1
  18. data/doc/Simple/Service/GemHelper.html +1 -1
  19. data/doc/Simple/Service/MissingArguments.html +1 -1
  20. data/doc/Simple/Service/NoSuchAction.html +1 -1
  21. data/doc/Simple/Service.html +89 -87
  22. data/doc/Simple.html +1 -1
  23. data/doc/_index.html +7 -19
  24. data/doc/class_list.html +1 -1
  25. data/doc/file.README.html +3 -3
  26. data/doc/file.TODO.html +70 -0
  27. data/doc/file_list.html +5 -0
  28. data/doc/index.html +3 -3
  29. data/doc/method_list.html +59 -115
  30. data/doc/top-level-namespace.html +1 -1
  31. data/lib/simple/service/action/comment.rb +1 -1
  32. data/lib/simple/service/action.rb +10 -3
  33. data/lib/simple/service/errors.rb +4 -3
  34. data/lib/simple/service.rb +33 -28
  35. data/lib/simple/workflow/context.rb +105 -0
  36. data/lib/simple/workflow/current_context.rb +33 -0
  37. data/lib/simple/workflow/reloader.rb +84 -0
  38. data/lib/simple/workflow/rspec_helper.rb +15 -0
  39. data/lib/simple/workflow.rb +96 -0
  40. data/lib/simple-workflow.rb +3 -0
  41. data/scripts/test +2 -0
  42. data/simple-service.gemspec +1 -0
  43. data/spec/simple/service/action_invoke3_spec.rb +0 -8
  44. data/spec/simple/service/action_invoke_spec.rb +82 -20
  45. data/spec/simple/service/service_spec.rb +13 -56
  46. data/spec/simple/workflow/context_spec.rb +90 -0
  47. data/spec/simple/workflow/current_context_spec.rb +41 -0
  48. data/spec/simple/workflow/reloader_spec/example1.rb +10 -0
  49. data/spec/simple/workflow/reloader_spec/example2.rb +7 -0
  50. data/spec/simple/workflow/reloader_spec.rb +48 -0
  51. data/spec/spec_helper.rb +2 -1
  52. data/spec/support/spec_services.rb +1 -3
  53. metadata +42 -7
  54. data/doc/Simple/Service/Action/IndieHash.html +0 -506
  55. data/lib/simple/service/context.rb +0 -94
  56. data/spec/simple/service/context_spec.rb +0 -69
@@ -1,11 +1,14 @@
1
1
  module Simple # @private
2
2
  end
3
3
 
4
+ module Simple::Service # @private
5
+ end
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
14
  # <b>The Simple::Service interface</b>
@@ -31,37 +34,43 @@ require_relative "service/version"
31
34
  # end
32
35
  #
33
36
  # 2. <em>Discover services:</em> To discover services in a service module use the #actions method. This returns a Hash
34
- # of actions. [TODO] why a Hash?
37
+ # of actions.
35
38
  #
36
39
  # Simple::Service.actions(GodMode)
37
40
  # => {:build_universe=>#<Simple::Service::Action...>, ...}
38
41
  #
39
- # 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>. You must set a context first.
42
+ # TODO: why a Hash? It feels much better if Simple::Service.actions returns an array of names.
40
43
  #
41
- # Simple::Service.with_context do
42
- # Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
43
- # end
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)
44
48
  # => 42
45
49
  #
46
50
  module Simple::Service
47
- def self.included(klass) # @private
48
- 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
49
57
  end
50
58
 
51
- # returns true if the passed in object is a service module.
52
- #
53
- # A service must be a module, and it must include the Simple::Service module.
54
- def self.service?(service)
55
- verify_service! service
56
- true
57
- rescue ::ArgumentError
58
- false
59
+ def self.included(klass) # @private
60
+ klass.extend ClassMethods
61
+ klass.include ServiceExpectations
59
62
  end
60
63
 
61
- # 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
62
65
  def self.verify_service!(service) # @private
63
- raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, but is not even a Module" unless service.is_a?(Module)
64
- raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, did you 'include Simple::Service'" unless service.include?(self)
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
65
74
  end
66
75
 
67
76
  # returns a Hash with all actions in the +service+ module
@@ -73,6 +82,8 @@ module Simple::Service
73
82
 
74
83
  # returns the action with the given name.
75
84
  def self.action(service, name)
85
+ expect! name => Symbol
86
+
76
87
  actions = self.actions(service)
77
88
  actions[name] || begin
78
89
  raise ::Simple::Service::NoSuchAction.new(service, name)
@@ -86,10 +97,8 @@ module Simple::Service
86
97
  #
87
98
  # As the main purpose of this module is to call services with outside data,
88
99
  # the +.invoke+ action is usually preferred.
89
- #
90
- # *Note:* You cannot call this method if the context is not set.
91
100
  def self.invoke3(service, name, *args, **flags)
92
- flags = flags.each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v }
101
+ flags = flags.transform_keys(&:to_s)
93
102
  invoke service, name, args: args, flags: flags
94
103
  end
95
104
 
@@ -133,14 +142,10 @@ module Simple::Service
133
142
  # by the method we raise an ArgumentError.
134
143
  #
135
144
  # Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
136
-
137
- # <b>Note:</b> You cannot call this method if the context is not set.
138
145
  def self.invoke(service, name, args: {}, flags: {})
139
- raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke3" unless context
140
-
141
146
  expect! args => [Hash, Array], flags: Hash
142
- args.keys.each { |key| expect! key => String } if args.is_a?(Hash)
143
- flags.keys.each { |key| expect! key => String }
147
+ args.each_key { |key| expect! key => String } if args.is_a?(Hash)
148
+ flags.each_key { |key| expect! key => String }
144
149
 
145
150
  action(service, name).invoke(args: args, flags: flags)
146
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
@@ -0,0 +1,3 @@
1
+ # rubocop:disable Naming/FileName
2
+
3
+ require "simple/workflow"
data/scripts/test ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ rspec "$@"
@@ -22,4 +22,5 @@ Gem::Specification.new do |gem|
22
22
  gem.required_ruby_version = '~> 2.5'
23
23
 
24
24
  gem.add_dependency "expectation", "~> 1"
25
+ gem.add_dependency "simple-immutable", "~> 1", ">= 1.1"
25
26
  end
@@ -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,21 +1,12 @@
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
 
17
8
  # a shortcut
18
- def invoke!(args = {}, flags: {})
9
+ def invoke!(args: {}, flags: {})
19
10
  @actual = ::Simple::Service.invoke(service, action, args: args, flags: flags)
20
11
  # rescue ::StandardError => e
21
12
  rescue ::Simple::Service::ArgumentError => e
@@ -42,7 +33,14 @@ describe "Simple::Service.invoke" do
42
33
 
43
34
  context "calling with extra named args" do
44
35
  it "ignores extra args" do
45
- invoke!("foo" => "foo", "bar" => "bar")
36
+ invoke!(args: { "foo" => "foo", "bar" => "bar" })
37
+ expect(actual).to eq("service2 return")
38
+ end
39
+ end
40
+
41
+ context "calling with extra flags" do
42
+ it "ignores extra args" do
43
+ invoke!(flags: { "foo" => "foo", "bar" => "bar" })
46
44
  expect(actual).to eq("service2 return")
47
45
  end
48
46
  end
@@ -67,21 +65,28 @@ describe "Simple::Service.invoke" do
67
65
 
68
66
  context "with the required number of args" do
69
67
  it "runs" do
70
- invoke!("a" => "foo", "b" => "bar")
68
+ invoke!(args: { "a" => "foo", "b" => "bar" })
69
+ expect(actual).to eq(["foo", "bar", "speed-of-light", 2.781])
70
+ end
71
+ end
72
+
73
+ context "with the required number of args and flags" do
74
+ it "merges flags and args to provide arguments" do
75
+ invoke!(args: { "a" => "foo" }, flags: { "b" => "bar" })
71
76
  expect(actual).to eq(["foo", "bar", "speed-of-light", 2.781])
72
77
  end
73
78
  end
74
79
 
75
80
  context "with the allowed number of args" do
76
81
  it "runs" do
77
- invoke!("a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4")
82
+ invoke!(args: { "a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4" })
78
83
  expect(actual).to eq(%w[foo bar baz number4])
79
84
  end
80
85
  end
81
86
 
82
87
  context "calling with extra named args" do
83
88
  it "ignores extra args" do
84
- invoke!("a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4", "extra3" => 3)
89
+ invoke!(args: { "a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4", "extra3" => 3 })
85
90
  expect(actual).to eq(%w[foo bar baz number4])
86
91
  end
87
92
  end
@@ -106,21 +111,21 @@ describe "Simple::Service.invoke" do
106
111
 
107
112
  context "with the required number of args" do
108
113
  it "runs" do
109
- invoke!("a" => "foo", "b" => "bar")
114
+ invoke!(args: { "a" => "foo", "b" => "bar" })
110
115
  expect(actual).to eq(["foo", "bar", "speed-of-light", 2.781])
111
116
  end
112
117
  end
113
118
 
114
119
  context "with the allowed number of args" do
115
120
  it "runs" do
116
- invoke!("a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4")
121
+ invoke!(args: { "a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4" })
117
122
  expect(actual).to eq(%w[foo bar baz number4])
118
123
  end
119
124
  end
120
125
 
121
126
  context "with extra named args" do
122
127
  it "ignores extra args" do
123
- invoke!("a" => "foo", "b" => "bar", "c" => "baz", "extra3" => 3)
128
+ invoke!(args: { "a" => "foo", "b" => "bar", "c" => "baz", "extra3" => 3 })
124
129
  expect(actual).to eq(["foo", "bar", "baz", 2.781])
125
130
  end
126
131
  end
@@ -144,23 +149,80 @@ describe "Simple::Service.invoke" do
144
149
 
145
150
  context "with the required number of args" do
146
151
  it "runs" do
147
- invoke!("a" => "foo")
152
+ invoke!(args: { "a" => "foo" })
148
153
  expect(actual).to eq(["foo", "default-b", "speed-of-light", 2.781])
149
154
  end
150
155
  end
151
156
 
152
157
  context "with the allowed number of args" do
153
158
  it "runs" do
154
- invoke!("a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4")
159
+ invoke!(args: { "a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4" })
155
160
  expect(actual).to eq(%w[foo bar baz number4])
156
161
  end
157
162
  end
158
163
 
159
164
  context "with extra named args" do
160
165
  it "ignores extra args" do
161
- invoke!("a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4", "extra3" => 3)
166
+ invoke!(args: { "a" => "foo", "b" => "bar", "c" => "baz", "e" => "number4", "extra3" => 3 })
162
167
  expect(actual).to eq(["foo", "bar", "baz", "number4"])
163
168
  end
164
169
  end
165
170
  end
171
+
172
+ context "calling an action w/mixed and variadic parameters" do
173
+ # reminder: this is the definition of variadic_params
174
+ #
175
+ # def variadic_params(a, b = "queen bee", *args, e: 2.781)
176
+ # [a, b, args, e]
177
+ # end
178
+
179
+ let(:action) { :variadic_params }
180
+
181
+ context "without args" do
182
+ it "raises MissingArguments" do
183
+ invoke!
184
+ expect(actual).to be_a(::Simple::Service::MissingArguments)
185
+ end
186
+ end
187
+
188
+ context "with the required number of args" do
189
+ it "runs" do
190
+ invoke!(args: { "a" => "foo" })
191
+ expect(actual).to eq(["foo", "queen bee", [], 2.781])
192
+ end
193
+ end
194
+
195
+ context "with the allowed number of args" do
196
+ it "runs" do
197
+ invoke!(args: { "a" => "foo", "b" => "bar", "args" => ["baz"] }, flags: { "e" => "number4" })
198
+ expect(actual).to eq(["foo", "bar", ["baz"], "number4"])
199
+ end
200
+ end
201
+
202
+ context "with variadic args" do
203
+ it "sends the variadic args from the args: parameter" do
204
+ invoke!(args: { "a" => "foo", "b" => "bar", "args" => ["baz", "extra"] }, flags: { "e" => "number4", "extra3" => 2 })
205
+ expect(actual).to eq(["foo", "bar", ["baz", "extra"], "number4"])
206
+ end
207
+
208
+ it "sends the variadic args from the flags: parameter" do
209
+ invoke!(args: { "a" => "foo", "b" => "bar" }, flags: { "args" => ["baz", "extra"], "e" => "number4", "extra3" => 2 })
210
+ expect(actual).to eq(["foo", "bar", ["baz", "extra"], "number4"])
211
+ end
212
+ end
213
+ end
214
+
215
+ describe "calling with symbolized Hashes" do
216
+ it "raises ArgumentError" do
217
+ hsh = { a: "foo", "b" => "KJH" }
218
+
219
+ expect do
220
+ invoke!(args: hsh)
221
+ end.to raise_error(Expectation::Matcher::Mismatch)
222
+
223
+ expect do
224
+ invoke!(flags: hsh)
225
+ end.to raise_error(Expectation::Matcher::Mismatch)
226
+ end
227
+ end
166
228
  end