toys 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -29,9 +29,35 @@
29
29
 
30
30
  module Toys
31
31
  ##
32
- # The lookup service that finds a tool given a set of arguments
32
+ # The Loader service loads tools from configuration files, and finds the
33
+ # appropriate tool given a set of command line arguments.
33
34
  #
34
35
  class Loader
36
+ ##
37
+ # Create a Loader
38
+ #
39
+ # @param [String,nil] config_dir_name A directory with this name that
40
+ # appears in the loader path, is treated as a configuration directory
41
+ # whose contents are loaded into the toys configuration. Optional.
42
+ # If not provided, toplevel configuration directories are disabled.
43
+ # @param [String,nil] config_file_name A file with this name that appears
44
+ # in the loader path, is treated as a toplevel configuration file
45
+ # whose contents are loaded into the toys configuration. Optional.
46
+ # If not provided, toplevel configuration files are disabled.
47
+ # @param [String,nil] index_file_name A file with this name that appears
48
+ # in any configuration directory (not just a toplevel directory) is
49
+ # loaded first as a standalone configuration file. If not provided,
50
+ # standalone configuration files are disabled.
51
+ # @param [String,nil] preload_file_name A file with this name that appears
52
+ # in any configuration directory (not just a toplevel directory) is
53
+ # loaded before any configuration files. It is not treated as a
54
+ # configuration file in that the configuration DSL is not honored. You
55
+ # may use such a file to define auxiliary Ruby modules and classes that
56
+ # used by the tools defined in that directory.
57
+ # @param [Array] middleware An array of middleware that will be used by
58
+ # default for all tools loaded by this CLI.
59
+ # @param [String] root_desc The description of the root tool.
60
+ #
35
61
  def initialize(config_dir_name: nil, config_file_name: nil,
36
62
  index_file_name: nil, preload_file_name: nil,
37
63
  middleware: [], root_desc: nil)
@@ -42,12 +68,22 @@ module Toys
42
68
  @middleware = middleware
43
69
  check_init_options
44
70
  @load_worklist = []
45
- root_tool = Tool.new([], middleware)
71
+ root_tool = Tool.new([])
72
+ root_tool.middleware_stack.concat(@middleware)
46
73
  root_tool.long_desc = root_desc if root_desc
47
74
  @tools = {[] => [root_tool, nil]}
48
75
  @max_priority = @min_priority = 0
49
76
  end
50
77
 
78
+ ##
79
+ # Add one or more configuration files/directories to the loader.
80
+ # This might point to a directory that defines a default set of tools.
81
+ #
82
+ # @param [String,Array<String>] paths One or more paths to add.
83
+ # @param [Boolean] high_priority If true, add these paths at the top of
84
+ # the priority list. Defaults to false, indicating new paths should
85
+ # be at the bottom of the priority list.
86
+ #
51
87
  def add_config_paths(paths, high_priority: false)
52
88
  paths = Array(paths)
53
89
  paths = paths.reverse if high_priority
@@ -57,6 +93,15 @@ module Toys
57
93
  self
58
94
  end
59
95
 
96
+ ##
97
+ # Add a single configuration file/directory to the loader.
98
+ # This might point to a directory that defines a default set of tools.
99
+ #
100
+ # @param [String] path A path to add.
101
+ # @param [Boolean] high_priority If true, add this path at the top of the
102
+ # priority list. Defaults to false, indicating the new path should be
103
+ # at the bottom of the priority list.
104
+ #
60
105
  def add_config_path(path, high_priority: false)
61
106
  path = check_path(path)
62
107
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
@@ -64,6 +109,15 @@ module Toys
64
109
  self
65
110
  end
66
111
 
112
+ ##
113
+ # Add one or more path directories to the loader. These directories are
114
+ # searched for toplevel config directories and files.
115
+ #
116
+ # @param [String,Array<String>] paths One or more paths to add.
117
+ # @param [Boolean] high_priority If true, add these paths at the top of
118
+ # the priority list. Defaults to false, indicating new paths should
119
+ # be at the bottom of the priority list.
120
+ #
67
121
  def add_paths(paths, high_priority: false)
68
122
  paths = Array(paths)
69
123
  paths = paths.reverse if high_priority
@@ -73,6 +127,15 @@ module Toys
73
127
  self
74
128
  end
75
129
 
130
+ ##
131
+ # Add a single path directory to the loader. This directory is searched
132
+ # for toplevel config directories and files.
133
+ #
134
+ # @param [String] path A path to add.
135
+ # @param [Boolean] high_priority If true, add this path at the top of the
136
+ # priority list. Defaults to false, indicating the new path should be
137
+ # at the bottom of the priority list.
138
+ #
76
139
  def add_path(path, high_priority: false)
77
140
  path = check_path(path, type: :dir)
78
141
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
@@ -91,6 +154,16 @@ module Toys
91
154
  self
92
155
  end
93
156
 
157
+ ##
158
+ # Given a list of command line arguments, find the appropriate tool to
159
+ # handle the command, loading it from the configuration if necessary.
160
+ # This always returns a tool. If the specific tool path is not defined and
161
+ # cannot be found in any configuration, it returns the nearest group that
162
+ # _would_ contain that tool, up to the root tool.
163
+ #
164
+ # @param [String] args Command line arguments
165
+ # @return [Toys::Tool]
166
+ #
94
167
  def lookup(args)
95
168
  orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
96
169
  cur_prefix = orig_prefix.dup
@@ -105,59 +178,119 @@ module Toys
105
178
  end
106
179
  end
107
180
 
181
+ ##
182
+ # Returns a list of subtools for the given path, loading from the
183
+ # configuration if necessary.
184
+ #
185
+ # @param [Array<String>] words The name of the parent tool
186
+ # @param [Boolean] recursive If true, return all subtools recursively
187
+ # rather than just the immediate children (the default)
188
+ # @return [Array<Toys::Tool>]
189
+ #
190
+ def list_subtools(words, recursive: false)
191
+ load_for_prefix(words)
192
+ found_tools = []
193
+ len = words.length
194
+ @tools.each do |n, tp|
195
+ next if n.empty?
196
+ if recursive
197
+ next if n.length <= len || n.slice(0, len) != words
198
+ else
199
+ next unless n.slice(0..-2) == words
200
+ end
201
+ found_tools << tp.first
202
+ end
203
+ sort_tools_by_name(found_tools)
204
+ end
205
+
206
+ ##
207
+ # Execute the tool given by the given arguments, in the given context.
208
+ #
209
+ # @param [Toys::Context::Base] context_base The context in which to
210
+ # execute.
211
+ # @param [String] args Command line arguments
212
+ # @param [Integer] verbosity Starting verbosity. Defaults to 0.
213
+ # @return [Integer] The exit code
214
+ #
215
+ # @private
216
+ #
108
217
  def execute(context_base, args, verbosity: 0)
109
218
  tool = lookup(args)
110
219
  tool.execute(context_base, args.slice(tool.full_name.length..-1), verbosity: verbosity)
111
220
  end
112
221
 
113
- def exact_tool(words)
114
- return nil unless tool_defined?(words)
115
- @tools[words].first
116
- end
117
-
118
- def get_tool(words, priority, assume_parent: false)
222
+ ##
223
+ # Returns a tool specified by the given words, with the given priority.
224
+ # Does not do any loading. If the tool is not present, creates it.
225
+ #
226
+ # @param [Array<String>] words The name of the tool.
227
+ # @param [Integer] priority The priority of the request.
228
+ # @param [Boolean] assume_parent If true, does not check the parent tool's
229
+ # priority.
230
+ # @return [Toys::Tool,nil] The tool, or `nil` if the given priority is
231
+ # insufficient.
232
+ #
233
+ # @private
234
+ #
235
+ def get_or_create_tool(words, priority, assume_parent: false)
119
236
  if tool_defined?(words)
120
237
  tool, tool_priority = @tools[words]
121
238
  return tool if priority.nil? || tool_priority.nil? || tool_priority == priority
122
239
  return nil if tool_priority > priority
123
240
  end
124
241
  unless assume_parent
125
- parent = get_tool(words[0..-2], priority)
242
+ parent = get_or_create_tool(words[0..-2], priority)
126
243
  return nil if parent.nil?
127
244
  end
128
- tool = Tool.new(words, @middleware)
245
+ prune_from(words)
246
+ tool = Tool.new(words)
247
+ tool.middleware_stack.concat(@middleware)
129
248
  @tools[words] = [tool, priority]
130
249
  tool
131
250
  end
132
251
 
252
+ ##
253
+ # Adds a tool directly to the loader.
254
+ # This should be used only for testing, as it overrides normal priority
255
+ # checking.
256
+ #
257
+ # @param [Toys::Tool] tool Tool to add.
258
+ # @param [Integer,nil] priority Priority for the tool.
259
+ #
260
+ # @private
261
+ #
133
262
  def put_tool!(tool, priority = nil)
134
263
  @tools[tool.full_name] = [tool, priority]
135
264
  self
136
265
  end
137
266
 
267
+ ##
268
+ # Returns true if the given tool name currently exists in the loader.
269
+ # Does not load the tool if not found.
270
+ #
271
+ # @param [Array<String>] words The name of the tool.
272
+ # @return [Boolean]
273
+ #
274
+ # @private
275
+ #
138
276
  def tool_defined?(words)
139
277
  @tools.key?(words)
140
278
  end
141
279
 
142
- def list_subtools(words, recursive)
143
- found_tools = []
144
- len = words.length
145
- @tools.each do |n, tp|
146
- next if n.empty?
147
- if recursive
148
- next if n.length <= len || n.slice(0, len) != words
149
- else
150
- next unless n.slice(0..-2) == words
151
- end
152
- found_tools << tp.first
153
- end
154
- sort_tools_by_name(found_tools)
155
- end
156
-
280
+ ##
281
+ # Load configuration from the given path.
282
+ #
283
+ # @private
284
+ #
157
285
  def include_path(path, words, remaining_words, priority)
158
286
  handle_path(check_path(path), words, remaining_words, priority)
159
287
  end
160
288
 
289
+ ##
290
+ # Determine the next setting for remaining_words, given a word.
291
+ #
292
+ # @private
293
+ #
161
294
  def self.next_remaining_words(remaining_words, word)
162
295
  if remaining_words.nil?
163
296
  nil
@@ -185,6 +318,11 @@ module Toys
185
318
  end
186
319
  end
187
320
 
321
+ def prune_from(words)
322
+ return unless @tools.key?(words)
323
+ @tools.delete_if { |k, _v| k[0, words.size] == words }
324
+ end
325
+
188
326
  def load_for_prefix(prefix)
189
327
  cur_worklist = @load_worklist
190
328
  @load_worklist = []
@@ -203,9 +341,9 @@ module Toys
203
341
 
204
342
  def load_path(path, words, remaining_words, priority)
205
343
  if ::File.extname(path) == ".rb"
206
- tool = get_tool(words, priority)
344
+ tool = get_or_create_tool(words, priority)
207
345
  if tool
208
- Builder.build(path, tool, remaining_words, priority, self, ::IO.read(path), :tool)
346
+ ConfigDSL.evaluate(path, tool, remaining_words, priority, self, :tool, ::IO.read(path))
209
347
  end
210
348
  else
211
349
  require_preload_in(path)
@@ -34,6 +34,20 @@ module Toys
34
34
  # Namespace for common middleware
35
35
  #
36
36
  module Middleware
37
+ ##
38
+ # Return a middleware class by name.
39
+ #
40
+ # Currently recognized middleware names are:
41
+ #
42
+ # * `:group_default` : Provides a default implementation for a group.
43
+ # * `:set_verbosity` : Switches for affecting log verbosity.
44
+ # * `:show_tool_help` : A switch that causes a tool to print its usage
45
+ # documentation.
46
+ # * `:show_usage_errors` : Displays the usage error if one occurs.
47
+ #
48
+ # @param [String,Symbol] name Name of the middleware class to return
49
+ # @return [Class,nil] The class, or `nil` if not found
50
+ #
37
51
  def self.lookup(name)
38
52
  Utils::ModuleLookup.lookup(:middleware, name)
39
53
  end
@@ -30,13 +30,19 @@
30
30
  module Toys
31
31
  module Middleware
32
32
  ##
33
- # A base middleware with a no-op implementation
33
+ # A base middleware with a no-op implementation.
34
34
  #
35
35
  class Base
36
+ ##
37
+ # The base middleware does not affect tool configuration.
38
+ #
36
39
  def config(_tool)
37
40
  yield
38
41
  end
39
42
 
43
+ ##
44
+ # The base middleware does not affect tool execution.
45
+ #
40
46
  def execute(_context)
41
47
  yield
42
48
  end
@@ -35,6 +35,9 @@ module Toys
35
35
  # A middleware that provides switches for editing the verbosity
36
36
  #
37
37
  class SetVerbosity < Base
38
+ ##
39
+ # This middleware adds `--verbose` and `--quiet` flags.
40
+ #
38
41
  def config(tool)
39
42
  tool.add_switch(Context::VERBOSITY, "-v", "--verbose",
40
43
  doc: "Increase verbosity",
@@ -33,23 +33,34 @@ require "toys/utils/usage"
33
33
  module Toys
34
34
  module Middleware
35
35
  ##
36
- # A middleware that provides a default implementation for groups
36
+ # A middleware that provides a default implementation for groups. If a
37
+ # tool has no executor, this middleware assumes it to be a group, and it
38
+ # provides a default executor that displays group usage documentation.
37
39
  #
38
- class GroupDefault < Base
40
+ class ShowGroupUsage < Base
41
+ ##
42
+ # This middleware adds a "--no-recursive" flag to groups. This flag, when
43
+ # set, shows only immediate subcommands rather than all recursively.
44
+ #
39
45
  def config(tool)
40
46
  if tool.includes_executor?
41
47
  yield
42
48
  else
43
- tool.add_switch(:_recursive, "-r", "--[no-]recursive",
44
- doc: "Show all subcommands recursively")
49
+ tool.add_switch(:_no_recursive, "--no-recursive",
50
+ doc: "Show immediate rather than all subcommands",
51
+ only_unique: true)
45
52
  end
46
53
  end
47
54
 
55
+ ##
56
+ # This middleware displays the usage documentation for groups. It has
57
+ # no effect on tools that have their own executor.
58
+ #
48
59
  def execute(context)
49
60
  if context[Context::TOOL].includes_executor?
50
61
  yield
51
62
  else
52
- puts(Utils::Usage.from_context(context).string(recursive: context[:_recursive]))
63
+ puts(Utils::Usage.from_context(context).string(recursive: !context[:_no_recursive]))
53
64
  end
54
65
  end
55
66
  end
@@ -35,7 +35,10 @@ module Toys
35
35
  ##
36
36
  # A middleware that shows usage documentation
37
37
  #
38
- class ShowToolHelp < Base
38
+ class ShowToolUsage < Base
39
+ ##
40
+ # This middleware adds a `--help` flag that triggers display of help.
41
+ #
39
42
  def config(tool)
40
43
  if tool.includes_executor?
41
44
  tool.add_switch(:_help, "-?", "--help",
@@ -45,6 +48,10 @@ module Toys
45
48
  yield
46
49
  end
47
50
 
51
+ ##
52
+ # If the `--help` flag is present, this middleware causes the tool to
53
+ # display its usage documentation and exit, rather than executing.
54
+ #
48
55
  def execute(context)
49
56
  if context[:_help]
50
57
  puts(Utils::Usage.from_context(context).string(recursive: context[:_recursive]))
@@ -36,6 +36,12 @@ module Toys
36
36
  # A middleware that shows usage errors
37
37
  #
38
38
  class ShowUsageErrors < Base
39
+ ##
40
+ # If a usage error happens, e.g. an unrecognized switch or an unfulfilled
41
+ # required argument, this middleware causes the tool to display the error
42
+ # and usage documentation and exit with a nonzero result. Otherwise, it
43
+ # does nothing.
44
+ #
39
45
  def execute(context)
40
46
  if context[Context::USAGE_ERROR]
41
47
  puts(context[Context::USAGE_ERROR])
@@ -31,7 +31,73 @@ module Toys
31
31
  ##
32
32
  # A template definition. Template classes should include this module.
33
33
  #
34
+ # A template is a configurable set of DSL code that can be run in a toys
35
+ # configuration to automate tool defintion. For example, toys provides a
36
+ # "minitest" template that generates a "test" tool that invokes minitest.
37
+ # Templates will often support configuration; for example the minitest
38
+ # template lets you configure the paths to the test files.
39
+ #
40
+ # ## Usage
41
+ #
42
+ # To create a template, define a class and include this module.
43
+ # The class defines the "configuration" of the template. If your template
44
+ # has options/parameters, you should provide a constructor, and methods
45
+ # appropriate to edit those options. The arguments given to the
46
+ # {Toys::ConfigDSL#expand} method are passed to your constructor, and your
47
+ # template object is passed to any block given to {Toys::ConfigDSL#expand}.
48
+ #
49
+ # Next, in your template class, call the `to_expand` method, which is defined
50
+ # in {Toys::Template::ClassMethods#to_expand}. Pass this a block which
51
+ # defines the implementation of the template. Effectively, the contents of
52
+ # this block are "inserted" into the user's configuration. The template
53
+ # object is passed to the block so you have access to the template options.
54
+ #
55
+ # ## Example
56
+ #
57
+ # This is a simple template that generates a "hello" tool. The tool simply
58
+ # prints a `"Hello, #{name}!"` greeting. The name is set as a template
59
+ # option; it is defined when the template is expanded in a toys
60
+ # configuration.
61
+ #
62
+ # # Define a template by creating a class that includes Toys::Template.
63
+ # class MyHelloTemplate
64
+ # include Toys::Template
65
+ #
66
+ # # A user of the template may pass an optional name as a parameter to
67
+ # # `expand`, or leave it as the default of "world".
68
+ # def initialize(name: "world")
69
+ # @name = name
70
+ # end
71
+ #
72
+ # # The template is passed to the expand block, so a user of the
73
+ # # template may also call this method to set the name.
74
+ # attr_accessor :name
75
+ #
76
+ # # The following block is inserted when the template is expanded.
77
+ # to_expand do |template|
78
+ # desc "Prints a greeting to #{template.name}"
79
+ # tool "templated-greeting" do
80
+ # execute do
81
+ # puts "Hello, #{template.name}!"
82
+ # end
83
+ # end
84
+ # end
85
+ # end
86
+ #
87
+ # Now you can use the template in your `.toys.rb` file like this:
88
+ #
89
+ # expand(MyHelloTemplate, name: "rubyists")
90
+ #
91
+ # or alternately:
92
+ #
93
+ # expand(MyHelloTemplate) do |template|
94
+ # template.name = "rubyists"
95
+ # end
96
+ #
97
+ # And it will create a tool called "templated-greeting".
98
+ #
34
99
  module Template
100
+ ## @private
35
101
  def self.included(mod)
36
102
  mod.extend(ClassMethods)
37
103
  end
@@ -40,10 +106,17 @@ module Toys
40
106
  # Class methods that will be added to a template class.
41
107
  #
42
108
  module ClassMethods
109
+ ##
110
+ # Provide the block that implements the template.
111
+ #
43
112
  def to_expand(&block)
44
113
  @expander = block
45
114
  end
46
115
 
116
+ ##
117
+ # You may alternately set the expander block using this accessor.
118
+ # @return [Proc]
119
+ #
47
120
  attr_accessor :expander
48
121
  end
49
122
  end