toys 0.3.1 → 0.3.2

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.
@@ -1,39 +0,0 @@
1
- # Copyright 2018 Daniel Azuma
2
- #
3
- # All rights reserved.
4
- #
5
- # Redistribution and use in source and binary forms, with or without
6
- # modification, are permitted provided that the following conditions are met:
7
- #
8
- # * Redistributions of source code must retain the above copyright notice,
9
- # this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above copyright notice,
11
- # this list of conditions and the following disclaimer in the documentation
12
- # and/or other materials provided with the distribution.
13
- # * Neither the name of the copyright holder, nor the names of any other
14
- # contributors to this software, may be used to endorse or promote products
15
- # derived from this software without specific prior written permission.
16
- #
17
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
- # POSSIBILITY OF SUCH DAMAGE.
28
- ;
29
-
30
- require "fileutils"
31
-
32
- module Toys
33
- module Helpers
34
- ##
35
- # File system utilities. See the "fileutils" standard library.
36
- #
37
- FileUtils = ::FileUtils
38
- end
39
- end
data/lib/toys/loader.rb DELETED
@@ -1,423 +0,0 @@
1
- # Copyright 2018 Daniel Azuma
2
- #
3
- # All rights reserved.
4
- #
5
- # Redistribution and use in source and binary forms, with or without
6
- # modification, are permitted provided that the following conditions are met:
7
- #
8
- # * Redistributions of source code must retain the above copyright notice,
9
- # this list of conditions and the following disclaimer.
10
- # * Redistributions in binary form must reproduce the above copyright notice,
11
- # this list of conditions and the following disclaimer in the documentation
12
- # and/or other materials provided with the distribution.
13
- # * Neither the name of the copyright holder, nor the names of any other
14
- # contributors to this software, may be used to endorse or promote products
15
- # derived from this software without specific prior written permission.
16
- #
17
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
- # POSSIBILITY OF SUCH DAMAGE.
28
- ;
29
-
30
- module Toys
31
- ##
32
- # The Loader service loads tools from configuration files, and finds the
33
- # appropriate tool given a set of command line arguments.
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
- #
61
- def initialize(config_dir_name: nil, config_file_name: nil,
62
- index_file_name: nil, preload_file_name: nil,
63
- middleware: [], root_desc: nil)
64
- @config_dir_name = config_dir_name
65
- @config_file_name = config_file_name
66
- @index_file_name = index_file_name
67
- @preload_file_name = preload_file_name
68
- @middleware = middleware
69
- check_init_options
70
- @load_worklist = []
71
- root_tool = Tool.new([])
72
- root_tool.middleware_stack.concat(@middleware)
73
- root_tool.long_desc = root_desc if root_desc
74
- @tools = {[] => [root_tool, nil]}
75
- @max_priority = @min_priority = 0
76
- end
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
- #
87
- def add_config_paths(paths, high_priority: false)
88
- paths = Array(paths)
89
- paths = paths.reverse if high_priority
90
- paths.each do |path|
91
- add_config_path(path, high_priority: high_priority)
92
- end
93
- self
94
- end
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
- #
105
- def add_config_path(path, high_priority: false)
106
- path = check_path(path)
107
- priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
108
- @load_worklist << [path, [], priority]
109
- self
110
- end
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
- #
121
- def add_paths(paths, high_priority: false)
122
- paths = Array(paths)
123
- paths = paths.reverse if high_priority
124
- paths.each do |path|
125
- add_path(path, high_priority: high_priority)
126
- end
127
- self
128
- end
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
- #
139
- def add_path(path, high_priority: false)
140
- path = check_path(path, type: :dir)
141
- priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
142
- if @config_file_name
143
- p = ::File.join(path, @config_file_name)
144
- if !::File.directory?(p) && ::File.readable?(p)
145
- @load_worklist << [p, [], priority]
146
- end
147
- end
148
- if @config_dir_name
149
- p = ::File.join(path, @config_dir_name)
150
- if ::File.directory?(p) && ::File.readable?(p)
151
- @load_worklist << [p, [], priority]
152
- end
153
- end
154
- self
155
- end
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
- #
167
- def lookup(args)
168
- orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
169
- cur_prefix = orig_prefix.dup
170
- loop do
171
- load_for_prefix(cur_prefix)
172
- p = orig_prefix.dup
173
- while p.length >= cur_prefix.length
174
- return @tools[p].first if @tools.key?(p)
175
- p.pop
176
- end
177
- raise "Bug: No tools found" unless cur_prefix.pop
178
- end
179
- end
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
- #
217
- def execute(context_base, args, verbosity: 0)
218
- tool = lookup(args)
219
- tool.execute(context_base, args.slice(tool.full_name.length..-1), verbosity: verbosity)
220
- end
221
-
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)
236
- if tool_defined?(words)
237
- tool, tool_priority = @tools[words]
238
- return tool if priority.nil? || tool_priority.nil? || tool_priority == priority
239
- return nil if tool_priority > priority
240
- end
241
- unless assume_parent
242
- parent = get_or_create_tool(words[0..-2], priority)
243
- return nil if parent.nil?
244
- end
245
- prune_from(words)
246
- tool = Tool.new(words)
247
- tool.middleware_stack.concat(@middleware)
248
- @tools[words] = [tool, priority]
249
- tool
250
- end
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
- #
262
- def put_tool!(tool, priority = nil)
263
- @tools[tool.full_name] = [tool, priority]
264
- self
265
- end
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
- #
276
- def tool_defined?(words)
277
- @tools.key?(words)
278
- end
279
-
280
- ##
281
- # Load configuration from the given path.
282
- #
283
- # @private
284
- #
285
- def include_path(path, words, remaining_words, priority)
286
- handle_path(check_path(path), words, remaining_words, priority)
287
- end
288
-
289
- ##
290
- # Determine the next setting for remaining_words, given a word.
291
- #
292
- # @private
293
- #
294
- def self.next_remaining_words(remaining_words, word)
295
- if remaining_words.nil?
296
- nil
297
- elsif remaining_words.empty?
298
- remaining_words
299
- elsif remaining_words.first == word
300
- remaining_words.slice(1..-1)
301
- end
302
- end
303
-
304
- private
305
-
306
- def check_init_options
307
- if @config_dir_name && ::File.extname(@config_dir_name) == ".rb"
308
- raise LookupError, "Illegal config dir name #{@config_dir_name.inspect}"
309
- end
310
- if @config_file_name && ::File.extname(@config_file_name) != ".rb"
311
- raise LookupError, "Illegal config file name #{@config_file_name.inspect}"
312
- end
313
- if @index_file_name && ::File.extname(@index_file_name) != ".rb"
314
- raise LookupError, "Illegal index file name #{@index_file_name.inspect}"
315
- end
316
- if @preload_file_name && ::File.extname(@preload_file_name) != ".rb"
317
- raise LookupError, "Illegal preload file name #{@preload_file_name.inspect}"
318
- end
319
- end
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
-
326
- def load_for_prefix(prefix)
327
- cur_worklist = @load_worklist
328
- @load_worklist = []
329
- cur_worklist.each do |path, words, priority|
330
- handle_path(path, words, calc_remaining_words(prefix, words), priority)
331
- end
332
- end
333
-
334
- def handle_path(path, words, remaining_words, priority)
335
- if remaining_words
336
- load_path(path, words, remaining_words, priority)
337
- else
338
- @load_worklist << [path, words, priority]
339
- end
340
- end
341
-
342
- def load_path(path, words, remaining_words, priority)
343
- if ::File.extname(path) == ".rb"
344
- tool = get_or_create_tool(words, priority)
345
- if tool
346
- ConfigDSL.evaluate(path, tool, remaining_words, priority, self, :tool, ::IO.read(path))
347
- end
348
- else
349
- require_preload_in(path)
350
- load_index_in(path, words, remaining_words, priority)
351
- ::Dir.entries(path).each do |child|
352
- load_child_in(path, child, words, remaining_words, priority)
353
- end
354
- end
355
- end
356
-
357
- def require_preload_in(path)
358
- return unless @preload_file_name
359
- preload_path = ::File.join(path, @preload_file_name)
360
- preload_path = check_path(preload_path, type: :file, lenient: true)
361
- require preload_path if preload_path
362
- end
363
-
364
- def load_index_in(path, words, remaining_words, priority)
365
- return unless @index_file_name
366
- index_path = ::File.join(path, @index_file_name)
367
- index_path = check_path(index_path, type: :file, lenient: true)
368
- load_path(index_path, words, remaining_words, priority) if index_path
369
- end
370
-
371
- def load_child_in(path, child, words, remaining_words, priority)
372
- return if child.start_with?(".")
373
- return if [@preload_file_name, @index_file_name].include?(child)
374
- child_path = check_path(::File.join(path, child))
375
- child_word = ::File.basename(child, ".rb")
376
- next_words = words + [child_word]
377
- next_remaining = Loader.next_remaining_words(remaining_words, child_word)
378
- handle_path(child_path, next_words, next_remaining, priority)
379
- end
380
-
381
- def check_path(path, lenient: false, type: nil)
382
- path = ::File.expand_path(path)
383
- type ||= ::File.extname(path) == ".rb" ? :file : :dir
384
- case type
385
- when :file
386
- if ::File.directory?(path) || !::File.readable?(path)
387
- return nil if lenient
388
- raise LookupError, "Cannot read file #{path}"
389
- end
390
- when :dir
391
- if !::File.directory?(path) || !::File.readable?(path)
392
- return nil if lenient
393
- raise LookupError, "Cannot read directory #{path}"
394
- end
395
- else
396
- raise ArgumentError, "Illegal type #{type}"
397
- end
398
- path
399
- end
400
-
401
- def sort_tools_by_name(tools)
402
- tools.sort do |a, b|
403
- a = a.full_name
404
- b = b.full_name
405
- while !a.empty? && !b.empty? && a.first == b.first
406
- a = a.slice(1..-1)
407
- b = b.slice(1..-1)
408
- end
409
- a.first.to_s <=> b.first.to_s
410
- end
411
- end
412
-
413
- def calc_remaining_words(words1, words2)
414
- index = 0
415
- lengths = [words1.length, words2.length]
416
- loop do
417
- return words1.slice(index..-1) if lengths.include?(index)
418
- return nil if words1[index] != words2[index]
419
- index += 1
420
- end
421
- end
422
- end
423
- end