simple-service 0.1.5 → 0.2.1

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