toys-core 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.
- checksums.yaml +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE.md +29 -0
- data/README.md +30 -0
- data/lib/toys-core.rb +54 -0
- data/lib/toys/alias.rb +94 -0
- data/lib/toys/cli.rb +268 -0
- data/lib/toys/config_dsl.rb +356 -0
- data/lib/toys/context.rb +278 -0
- data/lib/toys/core_version.rb +36 -0
- data/lib/toys/errors.rb +42 -0
- data/lib/toys/helpers.rb +52 -0
- data/lib/toys/helpers/exec.rb +469 -0
- data/lib/toys/helpers/file_utils.rb +39 -0
- data/lib/toys/loader.rb +381 -0
- data/lib/toys/middleware.rb +124 -0
- data/lib/toys/middleware/add_verbosity_switches.rb +99 -0
- data/lib/toys/middleware/base.rb +51 -0
- data/lib/toys/middleware/handle_usage_errors.rb +67 -0
- data/lib/toys/middleware/set_default_descriptions.rb +131 -0
- data/lib/toys/middleware/show_usage.rb +170 -0
- data/lib/toys/middleware/show_version.rb +99 -0
- data/lib/toys/template.rb +123 -0
- data/lib/toys/templates.rb +55 -0
- data/lib/toys/templates/clean.rb +82 -0
- data/lib/toys/templates/gem_build.rb +121 -0
- data/lib/toys/templates/minitest.rb +126 -0
- data/lib/toys/templates/rubocop.rb +86 -0
- data/lib/toys/templates/yardoc.rb +101 -0
- data/lib/toys/tool.rb +749 -0
- data/lib/toys/utils/module_lookup.rb +101 -0
- data/lib/toys/utils/usage.rb +196 -0
- metadata +146 -0
@@ -0,0 +1,39 @@
|
|
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
ADDED
@@ -0,0 +1,381 @@
|
|
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] index_file_name A file with this name that appears
|
40
|
+
# in any configuration directory (not just a toplevel directory) is
|
41
|
+
# loaded first as a standalone configuration file. If not provided,
|
42
|
+
# standalone configuration files are disabled.
|
43
|
+
# @param [String,nil] preload_file_name A file with this name that appears
|
44
|
+
# in any configuration directory (not just a toplevel directory) is
|
45
|
+
# loaded before any configuration files. It is not treated as a
|
46
|
+
# configuration file in that the configuration DSL is not honored. You
|
47
|
+
# may use such a file to define auxiliary Ruby modules and classes that
|
48
|
+
# used by the tools defined in that directory.
|
49
|
+
# @param [Array] middleware_stack An array of middleware that will be used
|
50
|
+
# by default for all tools loaded by this loader.
|
51
|
+
#
|
52
|
+
def initialize(index_file_name: nil, preload_file_name: nil, middleware_stack: [])
|
53
|
+
if index_file_name && ::File.extname(index_file_name) != ".rb"
|
54
|
+
raise LoaderError, "Illegal index file name #{index_file_name.inspect}"
|
55
|
+
end
|
56
|
+
if preload_file_name && ::File.extname(preload_file_name) != ".rb"
|
57
|
+
raise LoaderError, "Illegal preload file name #{preload_file_name.inspect}"
|
58
|
+
end
|
59
|
+
@index_file_name = index_file_name
|
60
|
+
@preload_file_name = preload_file_name
|
61
|
+
@middleware_stack = middleware_stack
|
62
|
+
@load_worklist = []
|
63
|
+
@tools = {}
|
64
|
+
@max_priority = @min_priority = 0
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Add a configuration file/directory to the loader.
|
69
|
+
#
|
70
|
+
# @param [String,Array<String>] path One or more paths to add.
|
71
|
+
# @param [Boolean] high_priority If true, add this path at the top of the
|
72
|
+
# priority list. Defaults to false, indicating the new path should be
|
73
|
+
# at the bottom of the priority list.
|
74
|
+
#
|
75
|
+
def add_path(path, high_priority: false)
|
76
|
+
paths = Array(path)
|
77
|
+
priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
|
78
|
+
paths.each do |p|
|
79
|
+
@load_worklist << [check_path(p), [], priority]
|
80
|
+
end
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Given a list of command line arguments, find the appropriate tool to
|
86
|
+
# handle the command, loading it from the configuration if necessary.
|
87
|
+
# This always returns a tool. If the specific tool path is not defined and
|
88
|
+
# cannot be found in any configuration, it returns the nearest group that
|
89
|
+
# _would_ contain that tool, up to the root tool.
|
90
|
+
#
|
91
|
+
# @param [String] args Command line arguments
|
92
|
+
# @return [Toys::Tool]
|
93
|
+
#
|
94
|
+
def lookup(args)
|
95
|
+
orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
|
96
|
+
cur_prefix = orig_prefix.dup
|
97
|
+
loop do
|
98
|
+
load_for_prefix(cur_prefix)
|
99
|
+
p = orig_prefix.dup
|
100
|
+
while p.length >= cur_prefix.length
|
101
|
+
tool = get_tool(p, [])
|
102
|
+
return tool if tool
|
103
|
+
p.pop
|
104
|
+
end
|
105
|
+
break unless cur_prefix.pop
|
106
|
+
end
|
107
|
+
get_or_create_tool([])
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Returns a list of subtools for the given path, loading from the
|
112
|
+
# configuration if necessary.
|
113
|
+
#
|
114
|
+
# @param [Array<String>] words The name of the parent tool
|
115
|
+
# @param [Boolean] recursive If true, return all subtools recursively
|
116
|
+
# rather than just the immediate children (the default)
|
117
|
+
# @return [Array<Toys::Tool,Tool::Alias>]
|
118
|
+
#
|
119
|
+
def list_subtools(words, recursive: false)
|
120
|
+
load_for_prefix(words)
|
121
|
+
found_tools = []
|
122
|
+
len = words.length
|
123
|
+
@tools.each do |n, tp|
|
124
|
+
next if n.empty?
|
125
|
+
if recursive
|
126
|
+
next if n.length <= len || n.slice(0, len) != words
|
127
|
+
else
|
128
|
+
next unless n.slice(0..-2) == words
|
129
|
+
end
|
130
|
+
found_tools << tp.first
|
131
|
+
end
|
132
|
+
sort_tools_by_name(found_tools)
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Execute the tool given by the given arguments, in the given context.
|
137
|
+
#
|
138
|
+
# @param [Toys::Context::Base] context_base The context in which to
|
139
|
+
# execute.
|
140
|
+
# @param [String] args Command line arguments
|
141
|
+
# @param [Integer] verbosity Starting verbosity. Defaults to 0.
|
142
|
+
# @return [Integer] The exit code
|
143
|
+
#
|
144
|
+
# @private
|
145
|
+
#
|
146
|
+
def execute(context_base, args, verbosity: 0)
|
147
|
+
tool = lookup(args)
|
148
|
+
tool.execute(context_base, args.slice(tool.full_name.length..-1), verbosity: verbosity)
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Returns a tool specified by the given words, with the given priority.
|
153
|
+
# Does not do any loading. If the tool is not present, creates it.
|
154
|
+
#
|
155
|
+
# @param [Array<String>] words The name of the tool.
|
156
|
+
# @param [Integer,nil] priority The priority of the request.
|
157
|
+
# @return [Toys::Tool,Toys::Alias,nil] The tool or alias, or `nil` if the
|
158
|
+
# given priority is insufficient for modification
|
159
|
+
#
|
160
|
+
# @private
|
161
|
+
#
|
162
|
+
def get_or_create_tool(words, priority: nil)
|
163
|
+
if @tools.key?(words)
|
164
|
+
tool, tool_priority = @tools[words]
|
165
|
+
if !priority || !tool_priority || tool_priority == priority
|
166
|
+
if priority && tool.is_a?(Alias)
|
167
|
+
raise LoaderError, "Cannot modify #{@words.inspect} because it is already an alias"
|
168
|
+
end
|
169
|
+
return tool
|
170
|
+
end
|
171
|
+
return nil if tool_priority > priority
|
172
|
+
end
|
173
|
+
get_or_create_tool(words[0..-2]) unless words.empty?
|
174
|
+
tool = Tool.new(words)
|
175
|
+
tool.middleware_stack.concat(Middleware.resolve_stack(@middleware_stack))
|
176
|
+
@tools[words] = [tool, priority]
|
177
|
+
tool
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Sets the given name as an alias to the given target.
|
182
|
+
#
|
183
|
+
# @param [Array<String>] words The alias name
|
184
|
+
# @param [Array<String>] target The alias target name
|
185
|
+
# @param [Integer] priority The priority of the request
|
186
|
+
#
|
187
|
+
# @return [Toys::Alias] The alias created
|
188
|
+
#
|
189
|
+
# @private
|
190
|
+
#
|
191
|
+
def make_alias(words, target, priority)
|
192
|
+
if @tools.key?(words)
|
193
|
+
tool_priority = @tools[words].last
|
194
|
+
if tool_priority
|
195
|
+
if tool_priority == priority
|
196
|
+
raise LoaderError, "Cannot make #{words.inspect} an alias because it is already defined"
|
197
|
+
elsif tool_priority > priority
|
198
|
+
return nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
a = Alias.new(words, target)
|
203
|
+
@tools[words] = [a, priority]
|
204
|
+
a
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Adds a tool directly to the loader.
|
209
|
+
# This should be used only for testing, as it overrides normal priority
|
210
|
+
# checking.
|
211
|
+
#
|
212
|
+
# @param [Toys::Tool] tool Tool to add.
|
213
|
+
# @param [Integer,nil] priority Priority for the tool.
|
214
|
+
#
|
215
|
+
# @private
|
216
|
+
#
|
217
|
+
def put_tool!(tool, priority = nil)
|
218
|
+
@tools[tool.full_name] = [tool, priority]
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# Returns true if the given tool name currently exists in the loader.
|
224
|
+
# Does not load the tool if not found.
|
225
|
+
#
|
226
|
+
# @param [Array<String>] words The name of the tool.
|
227
|
+
# @return [Boolean]
|
228
|
+
#
|
229
|
+
# @private
|
230
|
+
#
|
231
|
+
def tool_defined?(words)
|
232
|
+
@tools.key?(words)
|
233
|
+
end
|
234
|
+
|
235
|
+
##
|
236
|
+
# Returns a tool given a name. Resolves any aliases.
|
237
|
+
#
|
238
|
+
# @param [Array<String>] words Name of the tool
|
239
|
+
# @param [Array<Array<String>>] looked_up List of names that have already
|
240
|
+
# been traversed during alias resolution. Used to detect circular
|
241
|
+
# alias references.
|
242
|
+
# @return [Toys::Tool,nil] The tool, or `nil` if not found
|
243
|
+
#
|
244
|
+
# @private
|
245
|
+
#
|
246
|
+
def get_tool(words, looked_up = [])
|
247
|
+
return nil unless @tools.key?(words)
|
248
|
+
result = @tools[words].first
|
249
|
+
if result.is_a?(Alias)
|
250
|
+
words = result.target_name
|
251
|
+
if looked_up.include?(words)
|
252
|
+
raise LoaderError, "Circular alias references: #{looked_up.inspect}"
|
253
|
+
end
|
254
|
+
looked_up << words
|
255
|
+
get_tool(words, looked_up)
|
256
|
+
else
|
257
|
+
result
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
##
|
262
|
+
# Load configuration from the given path.
|
263
|
+
#
|
264
|
+
# @private
|
265
|
+
#
|
266
|
+
def include_path(path, words, remaining_words, priority)
|
267
|
+
handle_path(check_path(path), words, remaining_words, priority)
|
268
|
+
end
|
269
|
+
|
270
|
+
##
|
271
|
+
# Determine the next setting for remaining_words, given a word.
|
272
|
+
#
|
273
|
+
# @private
|
274
|
+
#
|
275
|
+
def self.next_remaining_words(remaining_words, word)
|
276
|
+
if remaining_words.nil?
|
277
|
+
nil
|
278
|
+
elsif remaining_words.empty?
|
279
|
+
remaining_words
|
280
|
+
elsif remaining_words.first == word
|
281
|
+
remaining_words.slice(1..-1)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def load_for_prefix(prefix)
|
288
|
+
cur_worklist = @load_worklist
|
289
|
+
@load_worklist = []
|
290
|
+
cur_worklist.each do |path, words, priority|
|
291
|
+
handle_path(path, words, calc_remaining_words(prefix, words), priority)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def handle_path(path, words, remaining_words, priority)
|
296
|
+
if remaining_words
|
297
|
+
load_path(path, words, remaining_words, priority)
|
298
|
+
else
|
299
|
+
@load_worklist << [path, words, priority]
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def load_path(path, words, remaining_words, priority)
|
304
|
+
if ::File.extname(path) == ".rb"
|
305
|
+
ConfigDSL.evaluate(words, remaining_words, priority, self, path, ::IO.read(path))
|
306
|
+
else
|
307
|
+
require_preload_in(path)
|
308
|
+
load_index_in(path, words, remaining_words, priority)
|
309
|
+
::Dir.entries(path).each do |child|
|
310
|
+
load_child_in(path, child, words, remaining_words, priority)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def require_preload_in(path)
|
316
|
+
return unless @preload_file_name
|
317
|
+
preload_path = ::File.join(path, @preload_file_name)
|
318
|
+
preload_path = check_path(preload_path, type: :file, lenient: true)
|
319
|
+
require preload_path if preload_path
|
320
|
+
end
|
321
|
+
|
322
|
+
def load_index_in(path, words, remaining_words, priority)
|
323
|
+
return unless @index_file_name
|
324
|
+
index_path = ::File.join(path, @index_file_name)
|
325
|
+
index_path = check_path(index_path, type: :file, lenient: true)
|
326
|
+
load_path(index_path, words, remaining_words, priority) if index_path
|
327
|
+
end
|
328
|
+
|
329
|
+
def load_child_in(path, child, words, remaining_words, priority)
|
330
|
+
return if child.start_with?(".")
|
331
|
+
return if [@preload_file_name, @index_file_name].include?(child)
|
332
|
+
child_path = check_path(::File.join(path, child))
|
333
|
+
child_word = ::File.basename(child, ".rb")
|
334
|
+
next_words = words + [child_word]
|
335
|
+
next_remaining = Loader.next_remaining_words(remaining_words, child_word)
|
336
|
+
handle_path(child_path, next_words, next_remaining, priority)
|
337
|
+
end
|
338
|
+
|
339
|
+
def check_path(path, lenient: false, type: nil)
|
340
|
+
path = ::File.expand_path(path)
|
341
|
+
type ||= ::File.extname(path) == ".rb" ? :file : :dir
|
342
|
+
case type
|
343
|
+
when :file
|
344
|
+
if ::File.directory?(path) || !::File.readable?(path)
|
345
|
+
return nil if lenient
|
346
|
+
raise LoaderError, "Cannot read file #{path}"
|
347
|
+
end
|
348
|
+
when :dir
|
349
|
+
if !::File.directory?(path) || !::File.readable?(path)
|
350
|
+
return nil if lenient
|
351
|
+
raise LoaderError, "Cannot read directory #{path}"
|
352
|
+
end
|
353
|
+
else
|
354
|
+
raise ArgumentError, "Illegal type #{type}"
|
355
|
+
end
|
356
|
+
path
|
357
|
+
end
|
358
|
+
|
359
|
+
def sort_tools_by_name(tools)
|
360
|
+
tools.sort do |a, b|
|
361
|
+
a = a.full_name
|
362
|
+
b = b.full_name
|
363
|
+
while !a.empty? && !b.empty? && a.first == b.first
|
364
|
+
a = a.slice(1..-1)
|
365
|
+
b = b.slice(1..-1)
|
366
|
+
end
|
367
|
+
a.first.to_s <=> b.first.to_s
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def calc_remaining_words(words1, words2)
|
372
|
+
index = 0
|
373
|
+
lengths = [words1.length, words2.length]
|
374
|
+
loop do
|
375
|
+
return words1.slice(index..-1) if lengths.include?(index)
|
376
|
+
return nil if words1[index] != words2[index]
|
377
|
+
index += 1
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
@@ -0,0 +1,124 @@
|
|
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 "toys/utils/module_lookup"
|
31
|
+
|
32
|
+
module Toys
|
33
|
+
##
|
34
|
+
# Middleware lets you define common behavior across many tools.
|
35
|
+
#
|
36
|
+
module Middleware
|
37
|
+
class << self
|
38
|
+
##
|
39
|
+
# Return a well-known middleware class by name.
|
40
|
+
#
|
41
|
+
# Currently recognized middleware names are:
|
42
|
+
#
|
43
|
+
# * `:add_verbosity_switches` : Adds switches for affecting verbosity.
|
44
|
+
# * `:handle_usage_errors` : Displays the usage error if one occurs.
|
45
|
+
# * `:set_default_descriptions` : Sets default descriptions for tools
|
46
|
+
# that do not have them set explicitly.
|
47
|
+
# * `:show_usage` : Teaches tools to print their usage documentation.
|
48
|
+
# * `:show_version` : Teaches tools to print their version.
|
49
|
+
#
|
50
|
+
# @param [String,Symbol] name Name of the middleware class to return
|
51
|
+
# @return [Class,nil] The class, or `nil` if not found
|
52
|
+
#
|
53
|
+
def lookup(name)
|
54
|
+
Utils::ModuleLookup.lookup(:middleware, name)
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Resolves a single middleware. You may pass an instance already
|
59
|
+
# constructed, a middleware class, the name of a well-known middleware
|
60
|
+
# class, or an array where the first element is the lookup name or class,
|
61
|
+
# and subsequent elements are arguments to be passed to the constructor.
|
62
|
+
#
|
63
|
+
# @param [String,Symbol,Array,Object] input The middleware spec
|
64
|
+
# @return [Object] Constructed middleware
|
65
|
+
#
|
66
|
+
def resolve(input)
|
67
|
+
input = Array(input)
|
68
|
+
raise "No middleware found" if input.empty?
|
69
|
+
cls = input.first
|
70
|
+
args = input[1..-1]
|
71
|
+
if cls.is_a?(::String) || cls.is_a?(::Symbol)
|
72
|
+
cls = lookup(cls)
|
73
|
+
end
|
74
|
+
if cls.is_a?(::Class)
|
75
|
+
cls.new(*args)
|
76
|
+
else
|
77
|
+
raise "Unrecognized middleware class #{cls.class}" unless args.empty?
|
78
|
+
cls
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Resolves an array of middleware specs. See {Toys::Middleware.resolve}.
|
84
|
+
#
|
85
|
+
# @param [Array] input An array of middleware specs
|
86
|
+
# @return [Array] An array of constructed middleware
|
87
|
+
#
|
88
|
+
def resolve_stack(input)
|
89
|
+
input.map { |e| resolve(e) }
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Resolves a typical switches specification. Used often in middleware.
|
94
|
+
#
|
95
|
+
# You may provide any of the following for the `switches` parameter:
|
96
|
+
# * A string, which becomes the single switch
|
97
|
+
# * An array of strings
|
98
|
+
# * The value `false` or `nil` which resolves to no switches
|
99
|
+
# * The value `true` or `:default` which resolves to the given defaults
|
100
|
+
# * A proc that takes a tool as argument and returns any of the above.
|
101
|
+
#
|
102
|
+
# Always returns an array of switch strings, even if empty.
|
103
|
+
#
|
104
|
+
# @param [Boolean,String,Array<String>,Proc] switches Switch spec
|
105
|
+
# @param [Toys::Tool] tool The tool
|
106
|
+
# @param [Array<String>] defaults The defaults to use for `true`.
|
107
|
+
# @return [Array<String>] An array of switches
|
108
|
+
#
|
109
|
+
def resolve_switches_spec(switches, tool, defaults)
|
110
|
+
switches = switches.call(tool) if switches.respond_to?(:call)
|
111
|
+
case switches
|
112
|
+
when true, :default
|
113
|
+
Array(defaults)
|
114
|
+
when ::String
|
115
|
+
[switches]
|
116
|
+
when ::Array
|
117
|
+
switches
|
118
|
+
else
|
119
|
+
[]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|