toys-core 0.7.0 → 0.8.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.md +16 -24
  4. data/README.md +307 -59
  5. data/docs/guide.md +44 -4
  6. data/lib/toys-core.rb +58 -49
  7. data/lib/toys/acceptor.rb +672 -0
  8. data/lib/toys/alias.rb +106 -0
  9. data/lib/toys/arg_parser.rb +624 -0
  10. data/lib/toys/cli.rb +422 -181
  11. data/lib/toys/compat.rb +83 -0
  12. data/lib/toys/completion.rb +442 -0
  13. data/lib/toys/context.rb +354 -0
  14. data/lib/toys/core_version.rb +18 -26
  15. data/lib/toys/dsl/flag.rb +213 -56
  16. data/lib/toys/dsl/flag_group.rb +237 -51
  17. data/lib/toys/dsl/positional_arg.rb +210 -0
  18. data/lib/toys/dsl/tool.rb +968 -317
  19. data/lib/toys/errors.rb +46 -28
  20. data/lib/toys/flag.rb +821 -0
  21. data/lib/toys/flag_group.rb +282 -0
  22. data/lib/toys/input_file.rb +18 -26
  23. data/lib/toys/loader.rb +110 -100
  24. data/lib/toys/middleware.rb +24 -31
  25. data/lib/toys/mixin.rb +90 -59
  26. data/lib/toys/module_lookup.rb +125 -0
  27. data/lib/toys/positional_arg.rb +184 -0
  28. data/lib/toys/source_info.rb +192 -0
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
  32. data/lib/toys/standard_middleware/show_help.rb +130 -113
  33. data/lib/toys/standard_middleware/show_root_version.rb +29 -35
  34. data/lib/toys/standard_mixins/exec.rb +116 -78
  35. data/lib/toys/standard_mixins/fileutils.rb +16 -24
  36. data/lib/toys/standard_mixins/gems.rb +29 -30
  37. data/lib/toys/standard_mixins/highline.rb +34 -41
  38. data/lib/toys/standard_mixins/terminal.rb +72 -26
  39. data/lib/toys/template.rb +51 -35
  40. data/lib/toys/tool.rb +1161 -206
  41. data/lib/toys/utils/completion_engine.rb +171 -0
  42. data/lib/toys/utils/exec.rb +279 -182
  43. data/lib/toys/utils/gems.rb +58 -49
  44. data/lib/toys/utils/help_text.rb +117 -111
  45. data/lib/toys/utils/terminal.rb +69 -62
  46. data/lib/toys/wrappable_string.rb +162 -0
  47. metadata +24 -22
  48. data/lib/toys/definition/acceptor.rb +0 -191
  49. data/lib/toys/definition/alias.rb +0 -112
  50. data/lib/toys/definition/arg.rb +0 -140
  51. data/lib/toys/definition/flag.rb +0 -370
  52. data/lib/toys/definition/flag_group.rb +0 -205
  53. data/lib/toys/definition/source_info.rb +0 -190
  54. data/lib/toys/definition/tool.rb +0 -842
  55. data/lib/toys/dsl/arg.rb +0 -132
  56. data/lib/toys/runner.rb +0 -188
  57. data/lib/toys/standard_middleware.rb +0 -47
  58. data/lib/toys/utils/module_lookup.rb +0 -135
  59. data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Daniel Azuma
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
22
+ ;
23
+
24
+ module Toys
25
+ ##
26
+ # A FlagGroup is a group of flags with the same requirement settings.
27
+ #
28
+ module FlagGroup
29
+ ##
30
+ # Create a flag group object of the given type.
31
+ #
32
+ # The type should be one of the following symbols:
33
+ # * `:optional` All flags in the group are optional
34
+ # * `:required` All flags in the group are required
35
+ # * `:exactly_one` Exactly one flag in the group must be provided
36
+ # * `:at_least_one` At least one flag in the group must be provided
37
+ # * `:at_most_one` At most one flag in the group must be provided
38
+ #
39
+ # @param type [Symbol] The type of group. Default is `:optional`.
40
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
41
+ # description for the group. See {Toys::Tool#desc=} for a description
42
+ # of allowed formats. Defaults to `"Flags"`.
43
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
44
+ # Long description for the flag group. See {Toys::Tool#long_desc=} for
45
+ # a description of allowed formats. Defaults to the empty array.
46
+ # @param name [String,Symbol,nil] The name of the group, or nil for no
47
+ # name.
48
+ # @return [Toys::FlagGroup::Base] A flag group of the correct subclass.
49
+ #
50
+ def self.create(type: nil, name: nil, desc: nil, long_desc: nil)
51
+ type ||= Optional
52
+ unless type.is_a?(::Class)
53
+ class_name = ModuleLookup.to_module_name(type)
54
+ type =
55
+ begin
56
+ FlagGroup.const_get(class_name)
57
+ rescue ::NameError
58
+ raise ToolDefinitionError, "Unknown flag group type: #{type}"
59
+ end
60
+ end
61
+ unless type.ancestors.include?(Base)
62
+ raise ToolDefinitionError, "Unknown flag group type: #{type}"
63
+ end
64
+ type.new(name, desc, long_desc)
65
+ end
66
+
67
+ ##
68
+ # The base class of a FlagGroup, implementing everything except validation.
69
+ # The base class effectively behaves as an Optional group. And the default
70
+ # group that contains flags not otherwise assigned to a group, is of this
71
+ # type. However, you should use {Toys::FlagGroup::Optional} when creating
72
+ # an explicit optional group.
73
+ #
74
+ class Base
75
+ ##
76
+ # Create a flag group.
77
+ # This argument list is subject to change. Use {Toys::FlagGroup.create}
78
+ # instead for a more stable interface.
79
+ # @private
80
+ #
81
+ def initialize(name, desc, long_desc)
82
+ @name = name
83
+ @desc = WrappableString.make(desc)
84
+ @long_desc = WrappableString.make_array(long_desc)
85
+ @flags = []
86
+ end
87
+
88
+ ##
89
+ # The symbolic name for this group
90
+ # @return [String,Symbol,nil]
91
+ #
92
+ attr_reader :name
93
+
94
+ ##
95
+ # The short description string.
96
+ #
97
+ # When reading, this is always returned as a {Toys::WrappableString}.
98
+ #
99
+ # When setting, the description may be provided as any of the following:
100
+ # * A {Toys::WrappableString}.
101
+ # * A normal String, which will be transformed into a
102
+ # {Toys::WrappableString} using spaces as word delimiters.
103
+ # * An Array of String, which will be transformed into a
104
+ # {Toys::WrappableString} where each array element represents an
105
+ # individual word for wrapping.
106
+ #
107
+ # @return [Toys::WrappableString]
108
+ #
109
+ attr_reader :desc
110
+
111
+ ##
112
+ # The long description strings.
113
+ #
114
+ # When reading, this is returned as an Array of {Toys::WrappableString}
115
+ # representing the lines in the description.
116
+ #
117
+ # When setting, the description must be provided as an Array where *each
118
+ # element* may be any of the following:
119
+ # * A {Toys::WrappableString} representing one line.
120
+ # * A normal String representing a line. This will be transformed into
121
+ # a {Toys::WrappableString} using spaces as word delimiters.
122
+ # * An Array of String representing a line. This will be transformed
123
+ # into a {Toys::WrappableString} where each array element represents
124
+ # an individual word for wrapping.
125
+ #
126
+ # @return [Array<Toys::WrappableString>]
127
+ #
128
+ attr_reader :long_desc
129
+
130
+ ##
131
+ # An array of flags that are in this group.
132
+ # Do not modify the returned array.
133
+ # @return [Array<Toys::Flag>]
134
+ #
135
+ attr_reader :flags
136
+
137
+ ##
138
+ # Returns true if this group is empty
139
+ # @return [Boolean]
140
+ #
141
+ def empty?
142
+ flags.empty?
143
+ end
144
+
145
+ ##
146
+ # Returns a string summarizing this group. This is generally either the
147
+ # short description or a representation of all the flags included.
148
+ # @return [String]
149
+ #
150
+ def summary
151
+ return desc.to_s.inspect unless desc.empty?
152
+ flags.map(&:display_name).inspect
153
+ end
154
+
155
+ ##
156
+ # Set the short description string.
157
+ #
158
+ # See {#desc} for details.
159
+ #
160
+ # @param desc [Toys::WrappableString,String,Array<String>]
161
+ #
162
+ def desc=(desc)
163
+ @desc = WrappableString.make(desc)
164
+ end
165
+
166
+ ##
167
+ # Set the long description strings.
168
+ #
169
+ # See {#long_desc} for details.
170
+ #
171
+ # @param long_desc [Array<Toys::WrappableString,String,Array<String>>]
172
+ #
173
+ def long_desc=(long_desc)
174
+ @long_desc = WrappableString.make_array(long_desc)
175
+ end
176
+
177
+ ##
178
+ # Append long description strings.
179
+ #
180
+ # You must pass an array of lines in the long description. See {#long_desc}
181
+ # for details on how each line may be represented.
182
+ #
183
+ # @param long_desc [Array<Toys::WrappableString,String,Array<String>>]
184
+ # @return [self]
185
+ #
186
+ def append_long_desc(long_desc)
187
+ @long_desc.concat(WrappableString.make_array(long_desc))
188
+ self
189
+ end
190
+
191
+ ## @private
192
+ def <<(flag)
193
+ flags << flag
194
+ end
195
+
196
+ ## @private
197
+ def validation_errors(_seen)
198
+ []
199
+ end
200
+ end
201
+
202
+ ##
203
+ # A FlagGroup containing all required flags
204
+ #
205
+ class Required < Base
206
+ ## @private
207
+ def validation_errors(seen)
208
+ results = []
209
+ flags.each do |flag|
210
+ unless seen.include?(flag.key)
211
+ str = "Flag \"#{flag.display_name}\" is required."
212
+ results << ArgParser::FlagGroupConstraintError.new(str)
213
+ end
214
+ end
215
+ results
216
+ end
217
+ end
218
+
219
+ ##
220
+ # A FlagGroup containing all optional flags
221
+ #
222
+ class Optional < Base
223
+ end
224
+
225
+ ##
226
+ # A FlagGroup in which exactly one flag must be set
227
+ #
228
+ class ExactlyOne < Base
229
+ ## @private
230
+ def validation_errors(seen)
231
+ seen_names = []
232
+ flags.each do |flag|
233
+ seen_names << flag.display_name if seen.include?(flag.key)
234
+ end
235
+ if seen_names.size > 1
236
+ str = "Exactly one flag out of group #{summary} is required, but #{seen_names.size}" \
237
+ " were provided: #{seen_names.inspect}."
238
+ [ArgParser::FlagGroupConstraintError.new(str)]
239
+ elsif seen_names.empty?
240
+ str = "Exactly one flag out of group #{summary} is required, but none were provided."
241
+ [ArgParser::FlagGroupConstraintError.new(str)]
242
+ else
243
+ []
244
+ end
245
+ end
246
+ end
247
+
248
+ ##
249
+ # A FlagGroup in which at most one flag must be set
250
+ #
251
+ class AtMostOne < Base
252
+ ## @private
253
+ def validation_errors(seen)
254
+ seen_names = []
255
+ flags.each do |flag|
256
+ seen_names << flag.display_name if seen.include?(flag.key)
257
+ end
258
+ if seen_names.size > 1
259
+ str = "At most one flag out of group #{summary} is required, but #{seen_names.size}" \
260
+ " were provided: #{seen_names.inspect}."
261
+ [ArgParser::FlagGroupConstraintError.new(str)]
262
+ else
263
+ []
264
+ end
265
+ end
266
+ end
267
+
268
+ ##
269
+ # A FlagGroup in which at least one flag must be set
270
+ #
271
+ class AtLeastOne < Base
272
+ ## @private
273
+ def validation_errors(seen)
274
+ flags.each do |flag|
275
+ return [] if seen.include?(flag.key)
276
+ end
277
+ str = "At least one flag out of group #{summary} is required, but none were provided."
278
+ [ArgParser::FlagGroupConstraintError.new(str)]
279
+ end
280
+ end
281
+ end
282
+ end
@@ -1,32 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2018 Daniel Azuma
3
+ # Copyright 2019 Daniel Azuma
4
4
  #
5
- # All rights reserved.
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
6
11
  #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
9
14
  #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
30
22
  ;
31
23
 
32
24
  ##
@@ -43,7 +35,7 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
43
35
  def self.evaluate(tool_class, remaining_words, source)
44
36
  namespace = ::Module.new
45
37
  namespace.module_eval do
46
- include ::Toys::Tool::Keys
38
+ include ::Toys::Context::Key
47
39
  @tool_class = tool_class
48
40
  end
49
41
  path = source.source_path
@@ -55,7 +47,7 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
55
47
  ::Toys::DSL::Tool.prepare(tool_class, remaining_words, source) do
56
48
  ::Toys::ContextualError.capture_path("Error while loading Toys config!", path) do
57
49
  # rubocop:disable Security/Eval
58
- eval(str, __binding, path, 0)
50
+ eval(str, __binding, path, -2)
59
51
  # rubocop:enable Security/Eval
60
52
  end
61
53
  end
@@ -1,32 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2018 Daniel Azuma
3
+ # Copyright 2019 Daniel Azuma
4
4
  #
5
- # All rights reserved.
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
6
11
  #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
9
14
  #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
30
22
  ;
31
23
 
32
24
  module Toys
@@ -39,10 +31,12 @@ module Toys
39
31
  class Loader
40
32
  ## @private
41
33
  ToolData = ::Struct.new(:definitions, :top_priority, :active_priority) do
34
+ ## @private
42
35
  def top_definition
43
36
  top_priority ? definitions[top_priority] : nil
44
37
  end
45
38
 
39
+ ## @private
46
40
  def active_definition
47
41
  active_priority ? definitions[active_priority] : nil
48
42
  end
@@ -51,66 +45,67 @@ module Toys
51
45
  ##
52
46
  # Create a Loader
53
47
  #
54
- # @param [String,nil] index_file_name A file with this name that appears
48
+ # @param index_file_name [String,nil] A file with this name that appears
55
49
  # in any configuration directory (not just a toplevel directory) is
56
50
  # loaded first as a standalone configuration file. If not provided,
57
51
  # standalone configuration files are disabled.
58
- # @param [String,nil] preload_file_name A file with this name that appears
52
+ # @param preload_file_name [String,nil] A file with this name that appears
59
53
  # in any configuration directory is preloaded before any tools in that
60
54
  # configuration directory are defined.
61
- # @param [String,nil] preload_directory_name A directory with this name
62
- # that appears in any configuration directory is searched for Ruby
63
- # files, which are preloaded before any tools in that configuration
64
- # directory are defined.
65
- # @param [String,nil] data_directory_name A directory with this name that
66
- # appears in any configuration directory is added to the data directory
67
- # search path for any tool file in that directory.
68
- # @param [Array] middleware_stack An array of middleware that will be used
55
+ # @param preload_dir_name [String,nil] A directory with this name that
56
+ # appears in any configuration directory is searched for Ruby files,
57
+ # which are preloaded before any tools in that configuration directory
58
+ # are defined.
59
+ # @param data_dir_name [String,nil] A directory with this name that appears
60
+ # in any configuration directory is added to the data directory search
61
+ # path for any tool file in that directory.
62
+ # @param middleware_stack [Array] An array of middleware that will be used
69
63
  # by default for all tools loaded by this loader.
70
- # @param [String] extra_delimiters A string containing characters that can
64
+ # @param extra_delimiters [String] A string containing characters that can
71
65
  # function as delimiters in a tool name. Defaults to empty. Allowed
72
66
  # characters are period, colon, and slash.
73
- # @param [Toys::Utils::ModuleLookup] mixin_lookup A lookup for well-known
67
+ # @param mixin_lookup [Toys::ModuleLookup] A lookup for well-known
74
68
  # mixin modules. Defaults to an empty lookup.
75
- # @param [Toys::Utils::ModuleLookup] middleware_lookup A lookup for
69
+ # @param middleware_lookup [Toys::ModuleLookup] A lookup for
76
70
  # well-known middleware classes. Defaults to an empty lookup.
77
- # @param [Toys::Utils::ModuleLookup] template_lookup A lookup for
71
+ # @param template_lookup [Toys::ModuleLookup] A lookup for
78
72
  # well-known template classes. Defaults to an empty lookup.
79
73
  #
80
- def initialize(index_file_name: nil, preload_directory_name: nil, preload_file_name: nil,
81
- data_directory_name: nil, middleware_stack: [], extra_delimiters: "",
74
+ def initialize(index_file_name: nil, preload_dir_name: nil, preload_file_name: nil,
75
+ data_dir_name: nil, middleware_stack: [], extra_delimiters: "",
82
76
  mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil)
83
77
  if index_file_name && ::File.extname(index_file_name) != ".rb"
84
78
  raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
85
79
  end
86
- @mixin_lookup = mixin_lookup || Utils::ModuleLookup.new
87
- @middleware_lookup = middleware_lookup || Utils::ModuleLookup.new
88
- @template_lookup = template_lookup || Utils::ModuleLookup.new
80
+ @mixin_lookup = mixin_lookup || ModuleLookup.new
81
+ @middleware_lookup = middleware_lookup || ModuleLookup.new
82
+ @template_lookup = template_lookup || ModuleLookup.new
89
83
  @index_file_name = index_file_name
90
84
  @preload_file_name = preload_file_name
91
- @preload_directory_name = preload_directory_name
92
- @data_directory_name = data_directory_name
85
+ @preload_dir_name = preload_dir_name
86
+ @data_dir_name = data_dir_name
93
87
  @middleware_stack = middleware_stack
94
88
  @worklist = []
95
89
  @tool_data = {}
96
90
  @max_priority = @min_priority = 0
97
91
  @extra_delimiters = process_extra_delimiters(extra_delimiters)
98
- get_tool_definition([], -999_999)
92
+ get_tool([], -999_999)
99
93
  end
100
94
 
101
95
  ##
102
96
  # Add a configuration file/directory to the loader.
103
97
  #
104
- # @param [String,Array<String>] paths One or more paths to add.
105
- # @param [Boolean] high_priority If true, add this path at the top of the
98
+ # @param paths [String,Array<String>] One or more paths to add.
99
+ # @param high_priority [Boolean] If true, add this path at the top of the
106
100
  # priority list. Defaults to false, indicating the new path should be
107
101
  # at the bottom of the priority list.
102
+ # @return [self]
108
103
  #
109
104
  def add_path(paths, high_priority: false)
110
105
  paths = Array(paths)
111
106
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
112
107
  paths.each do |path|
113
- source = Definition::SourceInfo.create_path_root(path)
108
+ source = SourceInfo.create_path_root(path)
114
109
  @worklist << [source, [], priority]
115
110
  end
116
111
  self
@@ -119,17 +114,20 @@ module Toys
119
114
  ##
120
115
  # Add a configuration block to the loader.
121
116
  #
122
- # @param [Boolean] high_priority If true, add this block at the top of the
117
+ # @param high_priority [Boolean] If true, add this block at the top of the
123
118
  # priority list. Defaults to false, indicating the block should be at
124
119
  # the bottom of the priority list.
125
- # @param [String] name The source name that will be shown in documentation
120
+ # @param name [String] The source name that will be shown in documentation
126
121
  # for tools defined in this block. If omitted, a default unique string
127
122
  # will be generated.
123
+ # @param block [Proc] The block of configuration, executed in the context
124
+ # of the tool DSL {Toys::DSL::Tool}.
125
+ # @return [self]
128
126
  #
129
127
  def add_block(high_priority: false, name: nil, &block)
130
128
  name ||= "(Code block #{block.object_id})"
131
129
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
132
- source = Definition::SourceInfo.create_proc_root(block, name)
130
+ source = SourceInfo.create_proc_root(block, name)
133
131
  @worklist << [source, [], priority]
134
132
  self
135
133
  end
@@ -140,13 +138,13 @@ module Toys
140
138
  # following aliases.
141
139
  # This always returns a tool. If the specific tool path is not defined and
142
140
  # cannot be found in any configuration, it finds the nearest namespace that
143
- # _would_ contain that tool, up to the root tool.
141
+ # *would* contain that tool, up to the root tool.
144
142
  #
145
143
  # Returns a tuple of the found tool, and the array of remaining arguments
146
144
  # that are not part of the tool name and should be passed as tool args.
147
145
  #
148
- # @param [Array<String>] args Command line arguments
149
- # @return [Array(Toys::Definition::Tool,Array<String>)]
146
+ # @param args [Array<String>] Command line arguments
147
+ # @return [Array(Toys::Tool,Array<String>)]
150
148
  #
151
149
  def lookup(args)
152
150
  orig_prefix, args = find_orig_prefix(args)
@@ -155,10 +153,10 @@ module Toys
155
153
  load_for_prefix(cur_prefix)
156
154
  prefix = orig_prefix
157
155
  loop do
158
- tool_definition = get_active_tool(prefix, [])
159
- if tool_definition
160
- finish_definitions_in_tree(tool_definition.full_name)
161
- return [tool_definition, args.slice(prefix.length..-1)]
156
+ tool = get_active_tool(prefix, [])
157
+ if tool
158
+ finish_definitions_in_tree(tool.full_name)
159
+ return [tool, args.slice(prefix.length..-1)]
162
160
  end
163
161
  break if prefix.empty? || prefix.length <= cur_prefix.length
164
162
  prefix = prefix.slice(0..-2)
@@ -172,12 +170,13 @@ module Toys
172
170
  # Returns a list of subtools for the given path, loading from the
173
171
  # configuration if necessary.
174
172
  #
175
- # @param [Array<String>] words The name of the parent tool
176
- # @param [Boolean] recursive If true, return all subtools recursively
173
+ # @param words [Array<String>] The name of the parent tool
174
+ # @param recursive [Boolean] If true, return all subtools recursively
177
175
  # rather than just the immediate children (the default)
178
- # @param [Boolean] include_hidden If true, include hidden subtools,
176
+ # @param include_hidden [Boolean] If true, include hidden subtools,
179
177
  # e.g. names beginning with underscores.
180
- # @return [Array<Toys::Definition::Tool,Toys::Definition::Alias>]
178
+ # @return [Array<Toys::Tool,Toys::Alias>] An array of subtools, which may
179
+ # be tools or aliases.
181
180
  #
182
181
  def list_subtools(words, recursive: false, include_hidden: false)
183
182
  load_for_prefix(words)
@@ -201,14 +200,16 @@ module Toys
201
200
  # Returns true if the given path has at least one subtool. Loads from the
202
201
  # configuration if necessary.
203
202
  #
204
- # @param [Array<String>] words The name of the parent tool
203
+ # @param words [Array<String>] The name of the parent tool
205
204
  # @return [Boolean]
206
205
  #
207
- def has_subtools?(words)
206
+ def has_subtools?(words) # rubocop:disable Naming/PredicateName
208
207
  load_for_prefix(words)
209
208
  len = words.length
210
- @tool_data.each_key do |n|
211
- return true if !n.empty? && n.length > len && n.slice(0, len) == words
209
+ @tool_data.each do |n, td|
210
+ if !n.empty? && n.length > len && n.slice(0, len) == words && !td.definitions.empty?
211
+ return true
212
+ end
212
213
  end
213
214
  false
214
215
  end
@@ -220,29 +221,31 @@ module Toys
220
221
  # the active priority, returns `nil`. If the given priority is higher than
221
222
  # the active priority, returns and activates a new tool.
222
223
  #
223
- # @param [Array<String>] words The name of the tool.
224
- # @param [Integer] priority The priority of the request.
225
- # @return [Toys::Definition::Tool,Toys::Definition::Alias,nil] The tool or
226
- # alias, or `nil` if the given priority is insufficient.
224
+ # @param words [Array<String>] The name of the tool.
225
+ # @param priority [Integer] The priority of the request.
226
+ #
227
+ # @return [Toys::Tool] The tool found.
228
+ # @return [Toys::Alias] The alias found.
229
+ # @return [nil] if the given priority is insufficient.
227
230
  #
228
231
  # @private
229
232
  #
230
- def activate_tool_definition(words, priority)
233
+ def activate_tool(words, priority)
231
234
  tool_data = get_tool_data(words)
232
235
  return tool_data.active_definition if tool_data.active_priority == priority
233
236
  return nil if tool_data.active_priority && tool_data.active_priority > priority
234
237
  tool_data.active_priority = priority
235
- get_tool_definition(words, priority)
238
+ get_tool(words, priority)
236
239
  end
237
240
 
238
241
  ##
239
242
  # Sets the given name as an alias to the given target.
240
243
  #
241
- # @param [Array<String>] words The alias name
242
- # @param [Array<String>] target The alias target name
243
- # @param [Integer] priority The priority of the request
244
+ # @param words [Array<String>] The alias name
245
+ # @param target [Array<String>] The alias target name
246
+ # @param priority [Integer] The priority of the request
244
247
  #
245
- # @return [Toys::Definition::Alias] The alias created
248
+ # @return [Toys::Alias] The alias created
246
249
  #
247
250
  # @private
248
251
  #
@@ -252,9 +255,9 @@ module Toys
252
255
  raise ToolDefinitionError,
253
256
  "Cannot make #{words.inspect} an alias because it is already defined"
254
257
  end
255
- alias_def = Definition::Alias.new(self, words, target, priority)
258
+ alias_def = Alias.new(self, words, target, priority)
256
259
  tool_data.definitions[priority] = alias_def
257
- activate_tool_definition(words, priority)
260
+ activate_tool(words, priority)
258
261
  alias_def
259
262
  end
260
263
 
@@ -262,7 +265,7 @@ module Toys
262
265
  # Returns true if the given tool name currently exists in the loader.
263
266
  # Does not load the tool if not found.
264
267
  #
265
- # @param [Array<String>] words The name of the tool.
268
+ # @param words [Array<String>] The name of the tool.
266
269
  # @return [Boolean]
267
270
  #
268
271
  # @private
@@ -277,9 +280,9 @@ module Toys
277
280
  #
278
281
  # @private
279
282
  #
280
- def get_tool_definition(words, priority)
281
- parent = words.empty? ? nil : get_tool_definition(words.slice(0..-2), priority)
282
- if parent.is_a?(Definition::Alias)
283
+ def get_tool(words, priority)
284
+ parent = words.empty? ? nil : get_tool(words.slice(0..-2), priority)
285
+ if parent.is_a?(Alias)
283
286
  raise ToolDefinitionError,
284
287
  "Cannot create children of #{parent.display_name.inspect} because it is an alias"
285
288
  end
@@ -289,15 +292,18 @@ module Toys
289
292
  end
290
293
  tool_data.definitions[priority] ||= begin
291
294
  middlewares = @middleware_stack.map { |m| resolve_middleware(m) }
292
- Definition::Tool.new(self, parent, words, priority, middlewares)
295
+ Tool.new(self, parent, words, priority, middlewares)
293
296
  end
294
297
  end
295
298
 
296
299
  ##
297
300
  # Attempt to get a well-known mixin module for the given symbolic name.
298
301
  #
299
- # @param [Symbol] name Mixin name
300
- # @return [Module,nil] The mixin, or `nil` if not found.
302
+ # @param name [Symbol] Mixin name
303
+ # @return [Module] The mixin
304
+ # @return [nil] if not found.
305
+ #
306
+ # @private
301
307
  #
302
308
  def resolve_standard_mixin(name)
303
309
  @mixin_lookup.lookup(name)
@@ -306,8 +312,11 @@ module Toys
306
312
  ##
307
313
  # Attempt to get a well-known template class for the given symbolic name.
308
314
  #
309
- # @param [Symbol] name Template name
310
- # @return [Class,nil] The template, or `nil` if not found.
315
+ # @param name [Symbol] Template name
316
+ # @return [Class] The template.
317
+ # @return [nil] if not found.
318
+ #
319
+ # @private
311
320
  #
312
321
  def resolve_standard_template(name)
313
322
  @template_lookup.lookup(name)
@@ -342,6 +351,7 @@ module Toys
342
351
  private
343
352
 
344
353
  ALLOWED_DELIMITERS = %r{^[\./:]*$}.freeze
354
+ private_constant :ALLOWED_DELIMITERS
345
355
 
346
356
  def process_extra_delimiters(input)
347
357
  unless ALLOWED_DELIMITERS =~ input
@@ -378,9 +388,9 @@ module Toys
378
388
  tool_data = get_tool_data(words)
379
389
  result = tool_data.active_definition
380
390
  case result
381
- when Definition::Alias
391
+ when Alias
382
392
  resolve_alias(result, looked_up)
383
- when Definition::Tool
393
+ when Tool
384
394
  result
385
395
  else
386
396
  tool_data.top_definition
@@ -428,7 +438,7 @@ module Toys
428
438
  @tool_data.each do |n, td|
429
439
  next if n.length < len || n.slice(0, len) != words
430
440
  tool = td.active_definition || td.top_definition
431
- tool.finish_definition(self) if tool.is_a?(Definition::Tool)
441
+ tool.finish_definition(self) if tool.is_a?(Tool)
432
442
  end
433
443
  end
434
444
 
@@ -447,7 +457,7 @@ module Toys
447
457
 
448
458
  def load_proc(source, words, remaining_words, priority)
449
459
  if remaining_words
450
- tool_class = get_tool_definition(words, priority).tool_class
460
+ tool_class = get_tool(words, priority).tool_class
451
461
  DSL::Tool.prepare(tool_class, remaining_words, source) do
452
462
  ContextualError.capture("Error while loading Toys config!") do
453
463
  tool_class.class_eval(&source.source_proc)
@@ -468,7 +478,7 @@ module Toys
468
478
 
469
479
  def load_relevant_path(source, words, remaining_words, priority)
470
480
  if source.source_type == :file
471
- tool_class = get_tool_definition(words, priority).tool_class
481
+ tool_class = get_tool(words, priority).tool_class
472
482
  InputFile.evaluate(tool_class, remaining_words, source)
473
483
  else
474
484
  do_preload(source.source_path)
@@ -481,15 +491,15 @@ module Toys
481
491
 
482
492
  def load_index_in(source, words, remaining_words, priority)
483
493
  return unless @index_file_name
484
- index_source = source.relative_child(@index_file_name, @data_directory_name)
494
+ index_source = source.relative_child(@index_file_name, @data_dir_name)
485
495
  load_relevant_path(index_source, words, remaining_words, priority) if index_source
486
496
  end
487
497
 
488
498
  def load_child_in(source, child, words, remaining_words, priority)
489
499
  return if child.start_with?(".") || child == @index_file_name ||
490
- child == @preload_file_name || child == @preload_directory_name ||
491
- child == @data_directory_name
492
- child_source = source.relative_child(child, @data_directory_name)
500
+ child == @preload_file_name || child == @preload_dir_name ||
501
+ child == @data_dir_name
502
+ child_source = source.relative_child(child, @data_dir_name)
493
503
  child_word = ::File.basename(child, ".rb")
494
504
  next_words = words + [child_word]
495
505
  next_remaining = Loader.next_remaining_words(remaining_words, child_word)
@@ -503,8 +513,8 @@ module Toys
503
513
  require preload_file
504
514
  end
505
515
  end
506
- if @preload_directory_name
507
- preload_dir = ::File.join(path, @preload_directory_name)
516
+ if @preload_dir_name
517
+ preload_dir = ::File.join(path, @preload_dir_name)
508
518
  if ::File.directory?(preload_dir) && ::File.readable?(preload_dir)
509
519
  ::Dir.entries(preload_dir).each do |child|
510
520
  next unless ::File.extname(child) == ".rb"
@@ -538,7 +548,7 @@ module Toys
538
548
 
539
549
  def tool_hidden?(tool, next_tool)
540
550
  return true if tool.full_name.any? { |n| n.start_with?("_") }
541
- return tool_hidden?(resolve_alias(tool), nil) if tool.is_a? Definition::Alias
551
+ return tool_hidden?(resolve_alias(tool), nil) if tool.is_a?(Alias)
542
552
  !tool.runnable? && next_tool && next_tool.full_name.slice(0..-2) == tool.full_name
543
553
  end
544
554