simple-service 0.1.1 → 0.1.6

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