simple-service 0.1.1 → 0.1.6

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -2
  3. data/.rubocop.yml +7 -0
  4. data/.tm_properties +1 -1
  5. data/Makefile +8 -0
  6. data/README.md +68 -1
  7. data/TODO.txt +3 -0
  8. data/VERSION +1 -1
  9. data/doc/Simple.html +117 -0
  10. data/doc/Simple/Service.html +865 -0
  11. data/doc/Simple/Service/Action.html +923 -0
  12. data/doc/Simple/Service/Action/Comment.html +451 -0
  13. data/doc/Simple/Service/Action/Comment/Extractor.html +347 -0
  14. data/doc/Simple/Service/Action/MethodReflection.html +285 -0
  15. data/doc/Simple/Service/Action/Parameter.html +816 -0
  16. data/doc/Simple/Service/ArgumentError.html +128 -0
  17. data/doc/Simple/Service/ClassMethods.html +187 -0
  18. data/doc/Simple/Service/Context.html +379 -0
  19. data/doc/Simple/Service/ContextMissingError.html +124 -0
  20. data/doc/Simple/Service/ContextReadOnlyError.html +206 -0
  21. data/doc/Simple/Service/ExtraArguments.html +428 -0
  22. data/doc/Simple/Service/GemHelper.html +190 -0
  23. data/doc/Simple/Service/MissingArguments.html +426 -0
  24. data/doc/Simple/Service/NoSuchAction.html +433 -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.rb +161 -56
  41. data/lib/simple/service/action.rb +104 -107
  42. data/lib/simple/service/action/comment.rb +3 -1
  43. data/lib/simple/service/action/method_reflection.rb +3 -3
  44. data/lib/simple/service/action/parameter.rb +3 -7
  45. data/lib/simple/service/context.rb +69 -27
  46. data/lib/simple/service/errors.rb +54 -0
  47. data/lib/simple/service/version.rb +7 -2
  48. data/scripts/stats +2 -1
  49. data/spec/simple/service/action_invoke3_spec.rb +266 -0
  50. data/spec/simple/service/action_invoke_spec.rb +237 -0
  51. data/spec/simple/service/action_spec.rb +51 -0
  52. data/spec/simple/service/context_spec.rb +39 -7
  53. data/spec/simple/service/service_spec.rb +158 -0
  54. data/spec/simple/service/version_spec.rb +7 -0
  55. data/spec/spec_helper.rb +15 -2
  56. data/spec/support/spec_services.rb +58 -0
  57. metadata +48 -6
  58. data/lib/simple.rb +0 -2
  59. data/spec/support/004_simplecov.rb +0 -13
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.20
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="Simple.html" title="Simple (module)">Simple</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Wed Dec 4 22:57:12 2019 by
104
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.20 (ruby-2.5.1).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
@@ -1,89 +1,194 @@
1
- module Simple::Service
2
- class ArgumentError < ::ArgumentError
3
- end
1
+ module Simple # @private
4
2
  end
5
3
 
4
+ require "expectation"
5
+ require "logger"
6
+
7
+ require_relative "service/errors"
6
8
  require_relative "service/action"
7
9
  require_relative "service/context"
10
+ require_relative "service/version"
8
11
 
9
- # The Simple::Service module.
12
+ # <b>The Simple::Service interface</b>
13
+ #
14
+ # This module implements the main API of the Simple::Service ruby gem.
15
+ #
16
+ # 1. <em>Marking a service module:</em> To turn a target module as a service module one must include <tt>Simple::Service</tt>
17
+ # into the target. This serves as a marker that this module is actually intended
18
+ # to provide one or more services. Example:
19
+ #
20
+ # module GodMode
21
+ # include Simple::Service
22
+ #
23
+ # # Build a universe.
24
+ # #
25
+ # # This comment will become part of the full description of the
26
+ # # "build_universe" service
27
+ # def build_universe(name, c: , pi: 3.14, e: 2.781)
28
+ # # at this point I realize that *I* am not God.
29
+ #
30
+ # 42 # Best try approach
31
+ # end
32
+ # end
33
+ #
34
+ # 2. <em>Discover services:</em> To discover services in a service module use the #actions method. This returns a Hash
35
+ # of actions.
10
36
  #
11
- # To mark a target module as a service module one must include the
12
- # Simple::Service module into the target module.
37
+ # Simple::Service.actions(GodMode)
38
+ # => {:build_universe=>#<Simple::Service::Action...>, ...}
39
+ #
40
+ # TODO: why a Hash? It feels much better if Simple::Service.actions returns an array of names.
41
+ #
42
+ #
43
+ # 3. <em>Invoke a service:</em> run <tt>Simple::Service.invoke3</tt> or <tt>Simple::Service.invoke</tt>. You must set a context first.
44
+ #
45
+ # Simple::Service.with_context do
46
+ # Simple::Service.invoke3(GodMode, :build_universe, "TestWorld", c: 1e9)
47
+ # end
48
+ # => 42
13
49
  #
14
- # This serves as a marker that this module is actually intended
15
- # to be used as a service.
16
50
  module Simple::Service
17
- def self.included(klass)
18
- 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
19
57
  end
20
58
 
21
- # Returns the current context.
22
- def self.context
23
- Thread.current[:"Simple::Service.context"]
59
+ def self.included(klass) # @private
60
+ klass.extend ClassMethods
61
+ klass.include ServiceExpectations
24
62
  end
25
63
 
26
- # yields a block with a given context, and restores the previous context
27
- # object afterwards.
28
- def self.with_context(ctx, &block)
29
- expect! ctx => [Simple::Service::Context, nil]
30
- _ = block
31
-
32
- old_ctx = Thread.current[:"Simple::Service.context"]
33
- Thread.current[:"Simple::Service.context"] = ctx
34
- yield
35
- ensure
36
- Thread.current[:"Simple::Service.context"] = old_ctx
64
+ def self.logger
65
+ @logger ||= ::Logger.new(STDOUT)
37
66
  end
38
67
 
39
- def self.action(service, name)
40
- actions = self.actions(service)
41
- actions[name] || begin
42
- action_names = actions.keys.sort
43
- informal = "service #{service} has these actions: #{action_names.map(&:inspect).join(", ")}"
44
- raise "No such action #{name.inspect}; #{informal}"
45
- end
68
+ def self.logger=(logger)
69
+ @logger = logger
46
70
  end
47
71
 
72
+ # returns true if the passed in object is a service module.
73
+ #
74
+ # A service must be a module, and it must include the Simple::Service module.
48
75
  def self.service?(service)
49
- service.is_a?(Module) && service.include?(self)
76
+ verify_service! service
77
+ true
78
+ rescue ::ArgumentError
79
+ false
50
80
  end
51
81
 
82
+ # Raises an error if the passed in object is not a service
83
+ def self.verify_service!(service) # @private
84
+ raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, but is not even a Module" unless service.is_a?(Module)
85
+ raise ::ArgumentError, "#{service.inspect} must be a Simple::Service, did you 'include Simple::Service'" unless service.include?(self)
86
+ end
87
+
88
+ # returns a Hash with all actions in the +service+ module
52
89
  def self.actions(service)
53
- raise ArgumentError, "service must be a #{self}" unless service?(service)
90
+ verify_service!(service)
54
91
 
55
92
  service.__simple_service_actions__
56
93
  end
57
94
 
58
- def self.invoke(service, name, arguments, params, context: nil)
59
- with_context(context) do
60
- action(service, name).invoke(arguments, params)
95
+ # returns the action with the given name.
96
+ def self.action(service, name)
97
+ actions = self.actions(service)
98
+ actions[name] || begin
99
+ raise ::Simple::Service::NoSuchAction.new(service, name)
61
100
  end
62
101
  end
63
102
 
64
- module ClassMethods
65
- # returns a Hash of actions provided by the service module.
66
- def __simple_service_actions__ # :nodoc:
67
- @__simple_service_actions__ ||= Action.enumerate(service: self)
68
- end
103
+ # invokes an action with a given +name+ in a service with +args+ and +flags+.
104
+ #
105
+ # This is a helper method which one can use to easily call an action from
106
+ # ruby source code.
107
+ #
108
+ # As the main purpose of this module is to call services with outside data,
109
+ # the +.invoke+ action is usually preferred.
110
+ #
111
+ # *Note:* You cannot call this method if the context is not set.
112
+ def self.invoke3(service, name, *args, **flags)
113
+ flags = flags.each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v }
114
+ invoke service, name, args: args, flags: flags
69
115
  end
70
116
 
71
- # Resolves a service by name. Returns nil if the name does not refer to a service,
72
- # or the service module otherwise.
73
- def self.resolve(str)
74
- return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
75
-
76
- service = resolve_constant(str)
77
-
78
- return unless service.is_a?(Module)
79
- return unless service.include?(::Simple::Service)
80
-
81
- service
117
+ # invokes an action with a given +name+.
118
+ #
119
+ # This is the general form of invoking a service. It accepts the following
120
+ # arguments:
121
+ #
122
+ # - args: an Array of positional arguments OR a Hash of named arguments.
123
+ # - flags: a Hash of flags.
124
+ #
125
+ # Note that the keys in both the +flags+ and the +args+ Hash must be strings.
126
+ #
127
+ # The service is being called with a parameters built out of those like this:
128
+ #
129
+ # - The service's positional arguments are being built from the +args+ array
130
+ # parameter or from the +named_args+ hash parameter.
131
+ # - The service's keyword arguments are being built from the +named_args+
132
+ # and +flags+ arguments.
133
+ #
134
+ # In other words:
135
+ #
136
+ # 1. You cannot set both +args+ and +named_args+ at the same time.
137
+ # 2. The +flags+ arguments are only being used to determine the
138
+ # service's keyword parameters.
139
+ #
140
+ # So, if the service X implements an action "def foo(bar, baz:)", the following would
141
+ # all invoke that service:
142
+ #
143
+ # - +Service.invoke3(X, :foo, "bar-value", baz: "baz-value")+, or
144
+ # - +Service.invoke3(X, :foo, bar: "bar-value", baz: "baz-value")+, or
145
+ # - +Service.invoke(X, :foo, args: ["bar-value"], flags: { "baz" => "baz-value" })+, or
146
+ # - +Service.invoke(X, :foo, args: { "bar" => "bar-value", "baz" => "baz-value" })+.
147
+ #
148
+ # (see spec/service_spec.rb)
149
+ #
150
+ # When there are not enough positional arguments to match the number of required
151
+ # positional arguments of the method we raise an ArgumentError.
152
+ #
153
+ # When there are more positional arguments provided than the number accepted
154
+ # by the method we raise an ArgumentError.
155
+ #
156
+ # Entries in the +named_args+ Hash that are not defined in the action itself are ignored.
157
+
158
+ # <b>Note:</b> You cannot call this method if the context is not set.
159
+ def self.invoke(service, name, args: {}, flags: {})
160
+ raise ContextMissingError, "Need to set context before calling ::Simple::Service.invoke3" unless context
161
+
162
+ expect! args => [Hash, Array], flags: Hash
163
+ args.keys.each { |key| expect! key => String } if args.is_a?(Hash)
164
+ flags.keys.each { |key| expect! key => String }
165
+
166
+ action(service, name).invoke(args: args, flags: flags)
82
167
  end
83
168
 
84
- def self.resolve_constant(str)
85
- const_get(str)
86
- rescue NameError
87
- nil
169
+ module ClassMethods # @private
170
+ # returns a Hash of actions provided by the service module.
171
+ def __simple_service_actions__
172
+ @__simple_service_actions__ ||= Action.enumerate(service: self)
173
+ end
88
174
  end
175
+
176
+ # # Resolves a service by name. Returns nil if the name does not refer to a service,
177
+ # # or the service module otherwise.
178
+ # def self.resolve(str)
179
+ # return unless str =~ /^[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*$/
180
+ #
181
+ # service = resolve_constant(str)
182
+ #
183
+ # return unless service.is_a?(Module)
184
+ # return unless service.include?(::Simple::Service)
185
+ #
186
+ # service
187
+ # end
188
+ #
189
+ # def self.resolve_constant(str)
190
+ # const_get(str)
191
+ # rescue NameError
192
+ # nil
193
+ # end
89
194
  end
@@ -1,8 +1,3 @@
1
- # rubocop:disable Metrics/CyclomaticComplexity
2
- # rubocop:disable Metrics/AbcSize
3
- # rubocop:disable Metrics/MethodLength
4
- # rubocop:disable Metrics/PerceivedComplexity
5
-
6
1
  module Simple::Service
7
2
  class Action
8
3
  end
@@ -12,14 +7,17 @@ require_relative "./action/comment"
12
7
  require_relative "./action/parameter"
13
8
 
14
9
  module Simple::Service
15
- class Action
16
- ArgumentError = ::Simple::Service::ArgumentError
10
+ # rubocop:disable Metrics/AbcSize
11
+ # rubocop:disable Metrics/PerceivedComplexity
12
+ # rubocop:disable Metrics/CyclomaticComplexity
13
+ # rubocop:disable Metrics/ClassLength
17
14
 
18
- IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*"
19
- IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z")
15
+ class Action
16
+ IDENTIFIER_PATTERN = "[a-z][a-z0-9_]*" # @private
17
+ IDENTIFIER_REGEXP = Regexp.compile("\\A#{IDENTIFIER_PATTERN}\\z") # @private
20
18
 
21
19
  # determines all services provided by the +service+ service module.
22
- def self.enumerate(service:) # :nodoc:
20
+ def self.enumerate(service:) # @private
23
21
  service.public_instance_methods(false)
24
22
  .grep(IDENTIFIER_REGEXP)
25
23
  .each_with_object({}) { |name, hsh| hsh[name] = Action.new(service, name) }
@@ -28,12 +26,20 @@ module Simple::Service
28
26
  attr_reader :service
29
27
  attr_reader :name
30
28
 
29
+ def full_name
30
+ "#{service.name}##{name}"
31
+ end
32
+
33
+ def to_s # @private
34
+ full_name
35
+ end
36
+
31
37
  # returns an Array of Parameter structures.
32
38
  def parameters
33
39
  @parameters ||= Parameter.reflect_on_method(service: service, name: name)
34
40
  end
35
41
 
36
- def initialize(service, name)
42
+ def initialize(service, name) # @private
37
43
  @service = service
38
44
  @name = name
39
45
 
@@ -63,131 +69,122 @@ module Simple::Service
63
69
  @service.instance_method(name).source_location
64
70
  end
65
71
 
66
- # build a service_instance and run the action, with arguments constructed from
67
- # args_hsh and params_hsh.
68
- def invoke(args, options)
69
- args ||= {}
70
- options ||= {}
71
-
72
- # convert Array arguments into a Hash of named arguments. This is strictly
73
- # necessary to be able to apply default value-based type conversions. (On
74
- # the downside this also means we convert an array to a hash and then back
75
- # into an array. This, however, should only be an issue for CLI based action
76
- # invocations, because any other use case (that I can think of) should allow
77
- # us to provide arguments as a Hash.
78
- if args.is_a?(Array)
79
- args = convert_argument_array_to_hash(args)
80
- end
72
+ # invokes an action with a given +name+ in a service with a Hash of arguments.
73
+ #
74
+ # You cannot call this method if the context is not set.
75
+ def invoke(args:, flags:)
76
+ args = convert_argument_array_to_hash(args) if args.is_a?(Array)
77
+
78
+ verify_required_args!(args, flags)
81
79
 
82
- # [TODO] Type conversion according to default values.
83
- args_ary = build_method_arguments(args, options)
80
+ positionals = build_positional_arguments(args, flags)
81
+ keywords = build_keyword_arguments(args.merge(flags))
84
82
 
85
83
  service_instance = Object.new
86
84
  service_instance.extend service
87
- service_instance.public_send(@name, *args_ary)
85
+
86
+ if keywords.empty?
87
+ service_instance.public_send(@name, *positionals)
88
+ else
89
+ # calling this with an empty keywords Hash still raises an ArgumentError
90
+ # if the target method does not accept arguments.
91
+ service_instance.public_send(@name, *positionals, **keywords)
92
+ end
88
93
  end
89
94
 
90
95
  private
91
96
 
92
- module IndifferentHashEx
93
- def self.fetch(hsh, name)
94
- missing_key!(name) unless hsh
97
+ # returns an error if the keywords hash does not define all required keyword arguments.
98
+ def verify_required_args!(args, flags) # @private
99
+ @required_names ||= parameters.select(&:required?).map(&:name).map(&:to_s)
95
100
 
96
- hsh.fetch(name.to_sym) do
97
- hsh.fetch(name.to_s) do
98
- missing_key!(name)
99
- end
100
- end
101
- end
101
+ missing_parameters = @required_names - args.keys - flags.keys
102
+ return if missing_parameters.empty?
102
103
 
103
- def self.key?(hsh, name)
104
- return false unless hsh
104
+ raise ::Simple::Service::MissingArguments.new(self, missing_parameters)
105
+ end
105
106
 
106
- hsh.key?(name.to_sym) || hsh.key?(name.to_s)
107
- end
107
+ # Enumerating all parameters it puts all named parameters into a Hash
108
+ # of keyword arguments.
109
+ def build_keyword_arguments(args)
110
+ @keyword_names ||= parameters.select(&:keyword?).map(&:name).map(&:to_s)
108
111
 
109
- def self.missing_key!(name)
110
- raise ArgumentError, "Missing argument in arguments hash: #{name}"
111
- end
112
- end
112
+ keys = @keyword_names & args.keys
113
+ values = args.fetch_values(*keys)
113
114
 
114
- I = IndifferentHashEx
115
-
116
- # returns an array of arguments suitable to be sent to the action method.
117
- def build_method_arguments(args_hsh, params_hsh)
118
- args = []
119
- keyword_args = {}
120
-
121
- parameters.each do |parameter|
122
- if parameter.keyword?
123
- if I.key?(params_hsh, parameter.name)
124
- keyword_args[parameter.name] = I.fetch(params_hsh, parameter.name)
125
- end
126
- else
127
- if parameter.variadic?
128
- if I.key?(args_hsh, parameter.name)
129
- args.concat(Array(I.fetch(args_hsh, parameter.name)))
130
- end
131
- else
132
- if !parameter.optional? || I.key?(args_hsh, parameter.name)
133
- args << I.fetch(args_hsh, parameter.name)
134
- end
135
- end
136
- end
137
- end
115
+ # Note that +keys+ now only contains names of keyword arguments that actually exist.
116
+ # This is therefore not a way to DOS this process.
117
+ Hash[keys.map(&:to_sym).zip(values)]
118
+ end
138
119
 
139
- unless keyword_args.empty?
140
- args << keyword_args
141
- end
120
+ def variadic_parameter
121
+ return @variadic_parameter if defined? @variadic_parameter
142
122
 
143
- args
123
+ @variadic_parameter = parameters.detect(&:variadic?)
144
124
  end
145
125
 
146
- def convert_argument_array_to_hash(ary)
147
- # enumerate all of the action's anonymous arguments, trying to match them
148
- # against the values in +ary+. If afterwards any arguments are still left
149
- # in +ary+ they will be assigned to the variadic arguments array, which
150
- # - if a variadic parameter is defined in this action - will be added to
151
- # the hash as well.
152
- hsh = {}
153
- variadic_parameter_name = nil
154
-
155
- parameters.each do |parameter|
156
- next if parameter.keyword?
157
- parameter_name = parameter.name
158
-
159
- if parameter.variadic?
160
- variadic_parameter_name = parameter_name
161
- next
162
- end
126
+ def positional_names
127
+ @positional_names ||= parameters.select(&:positional?).map(&:name).map(&:to_s)
128
+ end
163
129
 
164
- if ary.empty? && !parameter.optional?
165
- raise ::Simple::Service::ArgumentError, "Missing #{parameter_name} parameter"
130
+ # Enumerating all parameters it collects all positional parameters into
131
+ # an Array.
132
+ def build_positional_arguments(args, flags)
133
+ positionals = positional_names.each_with_object([]) do |parameter_name, ary|
134
+ if args.key?(parameter_name)
135
+ ary << args[parameter_name]
136
+ elsif flags.key?(parameter_name)
137
+ ary << flags[parameter_name]
166
138
  end
139
+ end
167
140
 
168
- next if ary.empty?
141
+ # A variadic parameter is appended to the positionals array.
142
+ # It is always optional - but if it exists it must be an Array.
143
+ if variadic_parameter
144
+ value = if args.key?(variadic_parameter.name)
145
+ args[variadic_parameter.name]
146
+ elsif flags.key?(variadic_parameter.name)
147
+ flags[variadic_parameter.name]
148
+ end
169
149
 
170
- hsh[parameter_name] = ary.shift
150
+ positionals.concat(value) if value
171
151
  end
172
152
 
173
- # Any arguments are left? Set variadic parameter, if defined, raise an error otherwise.
174
- unless ary.empty?
175
- unless variadic_parameter_name
176
- raise ::Simple::Service::ArgumentError, "Extra parameters: #{ary.map(&:inspect).join(", ")}"
153
+ positionals
154
+ end
155
+
156
+ def convert_argument_array_to_hash(ary)
157
+ expect! ary => Array
158
+
159
+ # +ary* might contain more, less, or the exact number of positional
160
+ # arguments. If the number is less, we return a hash with only whart
161
+ # exists in ary - the action might define default values after all.
162
+ #
163
+ # If it contains more the action better supports a variadic parameter;
164
+ # we otherwise raise a ExtraArguments exception.
165
+ case ary.length <=> positional_names.length
166
+ when 1 # i.e. ary.length > positional_names.length
167
+ extra_arguments = ary[positional_names.length..-1]
168
+ ary = ary[0..positional_names.length]
169
+
170
+ if !extra_arguments.empty? && !variadic_parameter
171
+ raise ::Simple::Service::ExtraArguments.new(self, extra_arguments)
177
172
  end
178
173
 
179
- hsh[variadic_parameter_name] = ary
174
+ existing_positional_names = positional_names
175
+ when 0 # i.e. ary.length == positional_names.length
176
+ existing_positional_names = positional_names
177
+ when -1 # i.e. ary.length < positional_names.length
178
+ existing_positional_names = positional_names[0, ary.length]
180
179
  end
181
180
 
182
- hsh
183
- end
181
+ # Build a hash with the existing_positional_names and the values from the array.
182
+ hsh = Hash[existing_positional_names.zip(ary)]
184
183
 
185
- def full_name
186
- "#{service}##{name}"
187
- end
184
+ # Add the variadic_parameter, if any.
185
+ hsh[variadic_parameter.name] = extra_arguments if variadic_parameter
188
186
 
189
- def to_s
190
- full_name
187
+ hsh
191
188
  end
192
189
  end
193
190
  end