toys 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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