rubycom 0.3.2 → 0.4.0

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 (43) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +162 -146
  3. data/Rakefile +12 -12
  4. data/lib/rubycom.rb +156 -226
  5. data/lib/rubycom/arg_parse.rb +252 -0
  6. data/lib/rubycom/command_interface.rb +97 -0
  7. data/lib/rubycom/completions.rb +62 -0
  8. data/lib/rubycom/error_handler.rb +15 -0
  9. data/lib/rubycom/executor.rb +23 -0
  10. data/lib/rubycom/helpers.rb +98 -0
  11. data/lib/rubycom/output_handler.rb +15 -0
  12. data/lib/rubycom/parameter_extract.rb +262 -0
  13. data/lib/rubycom/singleton_commands.rb +78 -0
  14. data/lib/rubycom/sources.rb +99 -0
  15. data/lib/rubycom/version.rb +1 -1
  16. data/lib/rubycom/yard_doc.rb +146 -0
  17. data/rubycom.gemspec +14 -16
  18. data/test/rubycom/arg_parse_test.rb +247 -0
  19. data/test/rubycom/command_interface_test.rb +293 -0
  20. data/test/rubycom/completions_test.rb +94 -0
  21. data/test/rubycom/error_handler_test.rb +72 -0
  22. data/test/rubycom/executor_test.rb +64 -0
  23. data/test/rubycom/helpers_test.rb +467 -0
  24. data/test/rubycom/output_handler_test.rb +76 -0
  25. data/test/rubycom/parameter_extract_test.rb +141 -0
  26. data/test/rubycom/rubycom_test.rb +290 -548
  27. data/test/rubycom/singleton_commands_test.rb +122 -0
  28. data/test/rubycom/sources_test.rb +59 -0
  29. data/test/rubycom/util_test_bin.rb +8 -0
  30. data/test/rubycom/util_test_composite.rb +23 -20
  31. data/test/rubycom/util_test_module.rb +142 -112
  32. data/test/rubycom/util_test_no_singleton.rb +2 -2
  33. data/test/rubycom/util_test_sub_module.rb +13 -0
  34. data/test/rubycom/yard_doc_test.rb +165 -0
  35. metadata +61 -24
  36. data/lib/rubycom/arguments.rb +0 -133
  37. data/lib/rubycom/commands.rb +0 -63
  38. data/lib/rubycom/documentation.rb +0 -212
  39. data/test/rubycom/arguments_test.rb +0 -289
  40. data/test/rubycom/commands_test.rb +0 -51
  41. data/test/rubycom/documentation_test.rb +0 -186
  42. data/test/rubycom/util_test_job.yaml +0 -21
  43. data/test/rubycom/utility_tester.rb +0 -17
@@ -0,0 +1,98 @@
1
+ module Rubycom
2
+ module Helpers
3
+
4
+ # Arranges each given tag hash such that all tags will line up nicely in vertical columns.
5
+ #
6
+ # @param [Array] tag_list an Array of Hashes which include keys: :name, :tag_name, :text, :types
7
+ # @param [Integer] desc_width the maximum width to use for the description column
8
+ # @return [Array] a list of strings comprised of types, separator, name||tag_name, separator, text
9
+ # formatted to line up vertically
10
+ def self.format_tags(tag_list, desc_width = 90)
11
+ tag_list = [] if tag_list.nil?
12
+ raise ArgumentError, "tag_list should be an Array but was a #{tag_list.class}" unless tag_list.class == Array
13
+ tag_list.each { |h|
14
+ raise ArgumentError, "tag #{h} should be a Hash but was a #{h.class}" unless h.class == Hash
15
+ [:name, :tag_name, :text, :types].each { |t| h.fetch(t) }
16
+ types = h[:types]
17
+ raise ArgumentError, "tag[:types] #{types} should be an Array but was #{types.class}" unless types.class == Array
18
+ }
19
+
20
+ longest_name = tag_list.map { |tag| (tag[:name].nil?) ? tag[:tag_name] : tag[:name] }.max
21
+ longest_types = tag_list.map { |tag| "#{tag[:types]}" }.max
22
+ longest_combo_name = tag_list.map { |tag| "#{tag[:tag_name]}#{tag[:name]}" }.max
23
+ {
24
+ others: tag_list.select { |tag| !["param", "return"].include?(tag[:tag_name]) }.map { |tag|
25
+ combo_name = (tag[:tag_name].nil? || tag[:tag_name].empty? || tag[:name].nil? || tag[:name].empty?) ? '' : "#{tag[:tag_name]}: #{tag[:name]}"
26
+ "#{(tag[:types].empty?) ? '' : tag[:types]}#{self.get_separator(tag[:types], longest_types, tag[:types].empty? ? nil : ' ')}#{self.format_command_summary(combo_name, tag[:text], self.get_separator(combo_name, longest_combo_name), desc_width)}"
27
+ },
28
+ params: tag_list.select { |tag| tag[:tag_name] == "param" }.map { |tag|
29
+ "#{tag[:types]}#{self.get_separator(tag[:types], longest_types, ' ')}#{self.format_command_summary(tag[:name], tag[:text], self.get_separator(tag[:name], longest_name), desc_width)}"
30
+ },
31
+ returns: tag_list.select { |tag| tag[:tag_name] == "return" }.map { |tag|
32
+ "#{tag[:types]}#{self.get_separator(tag[:types], longest_types, ' ')}#{self.format_command_summary(tag[:tag_name], tag[:text], self.get_separator(tag[:tag_name], longest_name), desc_width)}"
33
+ }
34
+ }
35
+ end
36
+
37
+ # Arranges each command_name => command_description in command_doc with a separator such that all command names and
38
+ # descriptions will line up nicely in vertical columns.
39
+ #
40
+ # @param [Hash] command_doc a mapping of command names to documentation summaries
41
+ # @param [Integer] desc_width the maximum width to use for the description column
42
+ # @return [Array] a list of strings comprised of command_name, separator, and description
43
+ # formatted to line up vertically
44
+ def self.format_command_list(command_doc, desc_width = 90, indent ='')
45
+ return [] if command_doc.nil?
46
+ raise ArgumentError, "command_doc should be a Hash but was a #{command_doc.class}" unless command_doc.class == Hash
47
+ command_doc = {} if command_doc.nil?
48
+ longest_command_name = command_doc.keys.max { |t, n| t.to_s.length <=> n.to_s.length }
49
+ command_doc.map { |command_name, doc|
50
+ self.format_command_summary("#{indent}#{command_name}", doc, self.get_separator(command_name, longest_command_name), desc_width)
51
+ }
52
+ end
53
+
54
+ # Creates a separator with the appropriate spacing to line up a command/description pair in a command list
55
+ #
56
+ # @param [String] name the command name to create a doc separator for
57
+ # @param [String] longest_name the longest name which will be shown above or below the given name
58
+ # @param [String] sep the separator to use
59
+ # @return [String] a spaced separator String for use in a command/description list
60
+ def self.get_separator(name, longest_name='', sep=' - ')
61
+ name = "#{name}" unless name.class == String
62
+ sep = '' if sep.nil?
63
+ longest_name = name if name.size > longest_name.size
64
+ (' ' * (longest_name.to_s.length - name.to_s.length)) << sep
65
+ end
66
+
67
+ # Arranges the given command_name and command_description with the separator in a standard format
68
+ #
69
+ # @param [String] command_name the command format
70
+ # @param [String] command_description the description for the given command
71
+ # @param [String] separator optional separator to use
72
+ def self.format_command_summary(command_name, command_description, separator = ' - ', max_width = 90)
73
+ command_name = '' if command_name.nil?
74
+ command_description = '' if command_description.nil?
75
+ separator = '' if separator == nil
76
+ raise ArgumentError, "command_name and separator #{command_name}#{separator} size should not be greater than max_width: #{max_width} but was #{separator.size + command_name.size}" if command_name.size+separator.size > max_width
77
+ $stdout.sync = true
78
+ prefix_space = (' ' * "#{command_name}#{separator}".length)
79
+ line_width = max_width - prefix_space.length
80
+ "#{command_name}#{separator}#{self.word_wrap(command_description, line_width, prefix_space)}\n"
81
+ end
82
+
83
+ # Converts a string longer than line_width to a multiline string where each line is at most line_width long.
84
+ #
85
+ # @param [String] text the text to be wrapped
86
+ # @param [Integer] line_width the maximum length any single line in the string should be, default: 80, minimum: 1
87
+ # @param [String] prefix a prefix to add the the front of any new lines created as a result of the wrap, default: ''
88
+ def self.word_wrap(text, line_width=80, prefix='')
89
+ text = "#{text}" unless text.class == String
90
+ prefix = "#{prefix}" unless prefix.class == String
91
+ line_width = 1 if line_width < 1
92
+ ([text.gsub("\n", ' ')].map { |line|
93
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
94
+ } * "\n").gsub("\n", "\n#{prefix}")
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,15 @@
1
+ module Rubycom
2
+ module OutputHandler
3
+
4
+ # Prints the command_result if it is a basic type or a Yaml representation of command_result if it is not
5
+ # basic types: String, NilClass, TrueClass, FalseClass, Fixnum, Float, Symbol
6
+ #
7
+ # @param [Object] command_result the result of a method call to be printed
8
+ def self.process_output(command_result)
9
+ std_output = nil
10
+ std_output = command_result.to_yaml unless [String, NilClass, TrueClass, FalseClass, Fixnum, Float, Symbol].include?(command_result.class)
11
+ $stdout.puts std_output || command_result
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,262 @@
1
+ module Rubycom
2
+ module ParameterExtract
3
+
4
+ # Calls #resolve_params with the given parameters after calling #check to assert the state of the inputs
5
+ #
6
+ # @param [Method] command the method whose parameters should be resolved
7
+ # @param [Hash] parsed_command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
8
+ # @param [Hash] command_doc :parameters => an array consisting of a hash for method parameter where
9
+ # :param_name => the param name as a string,
10
+ # :type => :req|:opt|:rest,
11
+ # :default => the default value for the param
12
+ # @return [Hash] command.parameters.each => the value for that parameter extracted from parsed_command_line or the default in command_doc
13
+ def self.extract_parameters(command, parsed_command_line, command_doc)
14
+ command, parsed_command_line, command_doc = self.check(command, parsed_command_line, command_doc)
15
+ self.resolve_params(command, parsed_command_line, command_doc)
16
+ end
17
+
18
+ # Provides upfront checking for this inputs to #extract_parameters and raises a ParameterExtractError if
19
+ # parsed_command_line includes a help argument, option, or flag
20
+ #
21
+ # @param [Method] command the method whose parameters should be resolved
22
+ # @param [Hash] parsed_command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
23
+ # @param [Hash] command_doc :parameters => an array consisting of a hash for method parameter where
24
+ # :param_name => the param name as a string,
25
+ # :type => :req|:opt|:rest,
26
+ # :default => the default value for the param
27
+ # @return [Array] the given parameters if none of the checks raised an error
28
+ def self.check(command, parsed_command_line, command_doc)
29
+ has_help_optional = false
30
+ command.parameters.select { |type, _| type == :opt }.map { |_, name| name.to_s }.each { |param|
31
+ has_help_optional = ['help', 'h'].include?(param)
32
+ } if command.class == Method
33
+ help_opt = !parsed_command_line[:opts].nil? && [
34
+ parsed_command_line[:opts]['help'],
35
+ parsed_command_line[:opts]['h']
36
+ ].include?(true)
37
+ help_flag = !parsed_command_line[:flags].nil? && [
38
+ parsed_command_line[:flags]['help'],
39
+ parsed_command_line[:flags]['h']
40
+ ].include?(true)
41
+ if !has_help_optional && (help_opt || help_flag)
42
+ raise ParameterExtractError, 'Help Requested'
43
+ end
44
+
45
+ raise ParameterExtractError, "No command specified." if command.nil?
46
+ raise ParameterExtractError, "No command specified." if command.class == Module
47
+ raise ParameterExtractError, "Unrecognized command." unless [Method, Module].include?(command.class)
48
+ raise "#{parsed_command_line} should be a Hash but was #{parsed_command_line.class}" if parsed_command_line.class != Hash
49
+
50
+ raise ArgumentError, "command_doc should be a Hash but was #{command_doc.class}" unless command_doc.class == Hash
51
+ raise ArgumentError, "command_doc should have key :parameters" unless command_doc.has_key?(:parameters)
52
+ raise ArgumentError, "command_doc[:parameters] should be an array but was #{command_doc[:parameters].class}" unless command_doc[:parameters].class == Array
53
+ command_doc[:parameters].each { |param_hsh|
54
+ raise ArgumentError, "parameter #{param_hsh} should be a Hash but was #{param_hsh.class}" unless param_hsh.class == Hash
55
+ raise ArgumentError, "parameter #{param_hsh} should have key :param_name" unless param_hsh.has_key?(:param_name)
56
+ raise ArgumentError, "parameter #{param_hsh} should have key :type" unless param_hsh.has_key?(:type)
57
+ raise ArgumentError, "parameter #{param_hsh} should have key :default" unless param_hsh.has_key?(:default)
58
+ }
59
+ [command, parsed_command_line, command_doc]
60
+ end
61
+
62
+ # Matches parameter names in command.parameters to values from command_line or their default values in command_doc
63
+ #
64
+ # @param [Method] command the method whose parameters should be resolved
65
+ # @param [Hash] command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
66
+ # @param [Hash] command_doc :parameters => an array consisting of a hash for method parameter where
67
+ # :param_name => the param name as a string,
68
+ # :type => :req|:opt|:rest,
69
+ # :default => the default value for the param
70
+ # @return [Hash] command.parameters.each => the value for that parameter extracted from command_line or the default in command_doc
71
+ def self.resolve_params(command, command_line, command_doc)
72
+ raise ArgumentError, "command should be a Method but was #{command.class}" unless command.class == Method
73
+ command_line = command_line.clone.map { |type, entry|
74
+ {type => entry.clone}
75
+ }.reduce({}, &:merge)
76
+ command_line = self.extract_command_args!(command.name.to_s, command_line)
77
+ params = command.parameters
78
+ param_names = self.get_param_names(params)
79
+ raise ArgumentError, "command_doc should have key :parameters but was #{command_doc}" unless command_doc.has_key?(:parameters)
80
+ param_docs = command_doc[:parameters].map { |param_hsh|
81
+ if param_hsh[:type] == :rest
82
+ {param_hsh.fetch(:param_name).reverse.chomp('*').reverse.to_sym => param_hsh.reject { |k, _| k == :param_name }}
83
+ else
84
+ {param_hsh.fetch(:param_name).to_sym => param_hsh.reject { |k, _| k == :param_name }}
85
+ end
86
+ }.reduce({}, &:merge)
87
+
88
+ params.map { |type, sym|
89
+ case type
90
+ when :opt
91
+ unless param_docs.has_key?(sym) && param_docs[sym].has_key?(:default)
92
+ raise ArgumentError, "#{sym} should exist in command_doc[:parameters] and have key :default but has values #{param_docs[sym]}"
93
+ end
94
+ self.resolve_opt!(sym, param_names[sym][:long], param_names[sym][:short], param_docs[sym][:default], command_line)
95
+ when :rest
96
+ self.resolve_rest!(sym, param_docs[sym][:default], command_line)
97
+ else
98
+ self.resolve_others!(sym, type, param_docs[sym][:default], command_line)
99
+ end
100
+ }.reduce({}, &:merge).reject { |_, val| val == :rubycom_no_value }
101
+ end
102
+
103
+ # Trims command_line[:args] down to the entries which occur after command_name
104
+ #
105
+ # @param [Object] command_name the entry in command_line[:args] which marks the start of the args to be returned
106
+ # @param [Hash] command_line :args => array of arguments
107
+ # @return [Hash] :args => array of arguments including only the entries which occur after the command_name
108
+ def self.extract_command_args!(command_name, command_line)
109
+ raise ArgumentError, "command_name should be a String|Symbol but was #{command_name}" unless [String, Symbol].include?(command_name.class)
110
+ raise ArgumentError, "command_line should be a hash but was #{command_line}" unless command_line.class == Hash
111
+ return command_line if command_line[:args].nil?
112
+
113
+ i = command_line[:args].index(command_name.to_s)
114
+ command_line[:args] = (i.nil?) ? [] : command_line[:args][i..-1]
115
+ command_line[:args].shift if command_line[:args].first == command_name.to_s
116
+ command_line
117
+ end
118
+
119
+ # Extracts the a value from command_line for the param_name or returns the default with command_line has no values
120
+ #
121
+ # @param [Object] param_name the key in the returned hash
122
+ # @param [Object] default_value the value in the returned hash if no value could be extracted from command_line
123
+ # @param [Hash] command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
124
+ # @return [Hash] param_name => extracted_value|default_value
125
+ def self.resolve_opt!(param_name, long_name, short_name, default_value, command_line)
126
+ raise ArgumentError, "command_line should be a hash but was #{command_line}" unless command_line.class == Hash
127
+ extraction = self.extract!(long_name, short_name, command_line[:opts], command_line[:flags], command_line[:args])
128
+ if extraction == :rubycom_no_value
129
+ {param_name => default_value}
130
+ else
131
+ {param_name => extraction}
132
+ end
133
+ end
134
+
135
+ # Creates a long and short name for each symbol in the given params
136
+ #
137
+ # @param [Array] params a list of Symbols to create names for
138
+ # @return [Hash] params.each symbol => a Hash where long => string form of symbol and
139
+ # short => the first char in symbol if unique in params or the string form of symbol if not
140
+ def self.get_param_names(params)
141
+ first_char_map = params.group_by { |_, sym| sym.to_s[0] }
142
+ params.map { |_, sym|
143
+ {
144
+ sym => {
145
+ long: sym.to_s,
146
+ short: (first_char_map[sym.to_s[0]].size == 1) ? sym.to_s[0] : sym.to_s
147
+ }
148
+ }
149
+ }.reduce({}, &:merge)
150
+ end
151
+
152
+ # Extracts the remaining values from command_line as an Array or returns the default with command_line has no values
153
+ #
154
+ # @param [Object] param_name the key in the returned hash
155
+ # @param [Object] default_value the value in the returned hash if no value could be extracted from command_line
156
+ # @param [Hash] command_line :args => array of arguments, :opts => { opt_key => opt_val }, :flags => { flag_key => flag_val }
157
+ # @return [Array] the rest of the keys/values in command_line
158
+ def self.resolve_rest!(param_name, default_value, command_line)
159
+ args = command_line[:args] || []
160
+ opts = command_line[:opts] || {}
161
+ flags = command_line[:flags] || {}
162
+ # TODO seems like we still can not call out a rest param on the command line, not sure if that is a problem
163
+ rest_arr = {
164
+ param_name => if args.empty? && opts.empty? && flags.empty?
165
+ default_value
166
+ elsif !args.empty? && opts.empty? && flags.empty?
167
+ args
168
+ elsif args.empty? && (!opts.empty? || !flags.empty?)
169
+ joined = self.join(flags, opts)
170
+ keyed = joined[param_name] || []
171
+ keyed.to_a << joined.reject { |k, _| k == param_name }
172
+ else
173
+ joined = self.join(flags, opts)
174
+ keyed = joined[param_name] || []
175
+ rest = keyed.to_a << joined.reject { |k, _| k == param_name }
176
+ args + rest
177
+ end
178
+ }
179
+
180
+ command_line[:opts] = {} unless command_line[:opts].nil?
181
+ command_line[:flags] = {} unless command_line[:flags].nil?
182
+ command_line[:args] = [] unless command_line[:args].nil?
183
+
184
+ rest_arr
185
+ end
186
+
187
+ # Calls #update on left passing in right and resolving conflicts by combining left and right values in an array
188
+ #
189
+ # @param [Hash] left the base thing to be updated
190
+ # @param [Hash] right the thing whose keys will be added to left and values combined with left on key collisions
191
+ # @return [Hash] left.keys + right.keys where each key => values from left or right or combined in an array if both
192
+ def self.join(left, right)
193
+ left.update(right) { |_, left_val, right_val|
194
+ if left_val.class == Array
195
+ combined = left_val
196
+ else
197
+ combined = [left_val]
198
+ end
199
+
200
+ if right_val.class == Array
201
+ right_val.each { |rv|
202
+ combined << rv
203
+ }
204
+ else
205
+ combined << right_val
206
+ end
207
+
208
+ combined
209
+ }
210
+ end
211
+
212
+ # Extracts a value from the command_line or returns the default_value if the command_line has no values.
213
+ # Raises a ParameterExtractError if the type was :req and no value was found.
214
+ #
215
+ # @param [Object] param_name the key in the returned hash
216
+ # @param [Symbol] type :req if the parameter is required
217
+ # @param [Object] default_value the value in the returned hash if no value could be extracted from command_line
218
+ # @param [Hash] command_line :args => array of arguments
219
+ # @return [Hash] param_name => value extracted for param_name
220
+ def self.resolve_others!(param_name, type, default_value, command_line)
221
+ if command_line[:args].size > 0
222
+ {param_name => (command_line[:args].shift)}
223
+ else
224
+ raise ParameterExtractError, "Missing required argument: #{param_name}" if type == :req
225
+ {param_name => default_value}
226
+ end
227
+ end
228
+
229
+ # Searches opts, then flags, then args for a key matching the given long_name or short_name
230
+ # The first matched key will be removed from the set. The valued paired to the matched key will be returned along
231
+ # with a hash containing the remaining args, opts, and flags.
232
+ # !destructively modifies opts, flags, and args by deleting or shifting a matched value out of the Hash/Array
233
+ #
234
+ # @param [Object] long_name the first key to be searched for in each opts, flags, args
235
+ # @param [Object] short_name the key to be searched for if the long_key is not found in the group under search
236
+ # @param [Hash] opts long_key|short_key => option value for that key
237
+ # @param [Hash] flags long_key|short_key => true|false value for that key
238
+ # @param [Array] args if no matching keys for the long or short name exist in either opts or flags and at least one
239
+ # value is left is args then the first value in args will be pulled as the value to return
240
+ # @return [Hash] the extracted value or :rubycom_no_value if a value could not be matched and args was empty
241
+ def self.extract!(long_name, short_name, opts, flags, args)
242
+ opts = {} if opts.nil?
243
+ flags = {} if flags.nil?
244
+ args = [] if args.nil?
245
+
246
+ if opts.has_key?(long_name)
247
+ opts.delete(long_name)
248
+ elsif opts.has_key?(short_name)
249
+ opts.delete(short_name)
250
+ elsif flags.has_key?(long_name)
251
+ flags.delete(long_name)
252
+ elsif flags.has_key?(short_name)
253
+ flags.delete(short_name)
254
+ elsif args.size > 0
255
+ args.shift
256
+ else
257
+ :rubycom_no_value
258
+ end
259
+ end
260
+
261
+ end
262
+ end
@@ -0,0 +1,78 @@
1
+ module Rubycom
2
+ module SingletonCommands
3
+
4
+ # Uses #discover_commands to look up commands in parsed_command_line, filters the result to the last matched command
5
+ # object
6
+ #
7
+ # @param [Module] base_module the module in which to search for commands
8
+ # @param [Hash] parsed_command_line :args => an array of strings representing the search terms
9
+ # @return [Module|Method] the last matched module or method object
10
+ def self.discover_command(base_module, parsed_command_line)
11
+ self.discover_commands(base_module, parsed_command_line).select { |candidate|
12
+ candidate.class == Module || candidate.class == Method
13
+ }.last
14
+ end
15
+
16
+ # Performs a depth only search of included modules starting with the base_module. The first word which matches a
17
+ # singleton method in one of the sub modules will be a Method object in the returned array. All matched sub modules
18
+ # will be Module objects in the returned array. All words occurring after a method match will be returned as they
19
+ # appear in parsed_command_line[:args]
20
+ #
21
+ # @param [Module] base_module the module in which to search for commands
22
+ # @param [Hash] parsed_command_line :args => an array of strings representing the search terms
23
+ # @return [Array] consisting of the matched sub Modules followed by the matched Method followed by the remaining args
24
+ def self.discover_commands(base_module, parsed_command_line)
25
+ base_module, args = self.check(base_module, parsed_command_line)
26
+ args.reduce([base_module]) { |acc, arg|
27
+ if acc.last.class == Method || acc.last.class == String
28
+ acc << arg
29
+ else
30
+ arg_sym = arg.to_s.to_sym
31
+ if self.get_commands(acc.last, false)[acc.last.to_s.to_sym][arg_sym] == :method
32
+ acc << acc.last.public_method(arg_sym)
33
+ else
34
+ acc << acc.last.const_get(arg_sym) rescue (acc << arg)
35
+ end
36
+ end
37
+ }
38
+ end
39
+
40
+ # Provides upfront checking for this inputs to #discover_commands
41
+ def self.check(base_module, parsed_command_line)
42
+ raise ArgumentError, 'base_module should not be nil' if base_module.nil?
43
+ raise ArgumentError, 'parsed_command_line should not be nil' if parsed_command_line.nil?
44
+ raise ArgumentError, "parsed_command_line should be a Hash but was #{parsed_command_line.class}" if parsed_command_line.class != Hash
45
+ arguments = parsed_command_line[:args] || []
46
+ raise ArgumentError, "args should be an Array but was #{arguments.class}" unless arguments.class == Array
47
+ unless [Module, String, Symbol].include?(base_module.class)
48
+ raise ArgumentError, "base_module should be a Module, String, or Symbol but was #{base_module.class}"
49
+ end
50
+ base_module = Kernel.const_get(base_module) if base_module.class == Symbol
51
+ base_module = Kernel.const_get(base_module.to_sym) if base_module.class == String
52
+ [base_module, arguments.map { |arg| arg.to_s } ]
53
+ end
54
+
55
+ # Retrieves the singleton methods in the given base and included Modules
56
+ #
57
+ # @param [Module] base the module which invoked 'include Rubycom'
58
+ # @param [Boolean] all if true recursively search for included modules' commands, if false return only top level commands
59
+ # @return [Hash] a Hash of Symbols representing the command methods in the given base and it's included modules (if all=true)
60
+ def self.get_commands(base, all=true)
61
+ return {} if base.nil? || !base.respond_to?(:singleton_methods) || !base.respond_to?(:included_modules)
62
+ {
63
+ base.to_s.to_sym => base.singleton_methods(true).select { |sym| ![:included, :extended].include?(sym) }.map { |sym|
64
+ {
65
+ sym => :method
66
+ }
67
+ }.reduce({}, &:merge).merge(
68
+ base.included_modules.select { |mod| mod.name.to_sym != :Rubycom }.map { |mod|
69
+ {
70
+ mod.to_s.to_sym => (all ? self.get_commands(mod, all)[mod.to_s.to_sym] : :module)
71
+ }
72
+ }.reduce({}, &:merge)
73
+ )
74
+ }
75
+ end
76
+
77
+ end
78
+ end