toys 0.3.0 → 0.3.1

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.
@@ -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