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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -0
- data/LICENSE.md +16 -24
- data/README.md +307 -59
- data/docs/guide.md +44 -4
- data/lib/toys-core.rb +58 -49
- data/lib/toys/acceptor.rb +672 -0
- data/lib/toys/alias.rb +106 -0
- data/lib/toys/arg_parser.rb +624 -0
- data/lib/toys/cli.rb +422 -181
- data/lib/toys/compat.rb +83 -0
- data/lib/toys/completion.rb +442 -0
- data/lib/toys/context.rb +354 -0
- data/lib/toys/core_version.rb +18 -26
- data/lib/toys/dsl/flag.rb +213 -56
- data/lib/toys/dsl/flag_group.rb +237 -51
- data/lib/toys/dsl/positional_arg.rb +210 -0
- data/lib/toys/dsl/tool.rb +968 -317
- data/lib/toys/errors.rb +46 -28
- data/lib/toys/flag.rb +821 -0
- data/lib/toys/flag_group.rb +282 -0
- data/lib/toys/input_file.rb +18 -26
- data/lib/toys/loader.rb +110 -100
- data/lib/toys/middleware.rb +24 -31
- data/lib/toys/mixin.rb +90 -59
- data/lib/toys/module_lookup.rb +125 -0
- data/lib/toys/positional_arg.rb +184 -0
- data/lib/toys/source_info.rb +192 -0
- data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
- data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
- data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
- data/lib/toys/standard_middleware/show_help.rb +130 -113
- data/lib/toys/standard_middleware/show_root_version.rb +29 -35
- data/lib/toys/standard_mixins/exec.rb +116 -78
- data/lib/toys/standard_mixins/fileutils.rb +16 -24
- data/lib/toys/standard_mixins/gems.rb +29 -30
- data/lib/toys/standard_mixins/highline.rb +34 -41
- data/lib/toys/standard_mixins/terminal.rb +72 -26
- data/lib/toys/template.rb +51 -35
- data/lib/toys/tool.rb +1161 -206
- data/lib/toys/utils/completion_engine.rb +171 -0
- data/lib/toys/utils/exec.rb +279 -182
- data/lib/toys/utils/gems.rb +58 -49
- data/lib/toys/utils/help_text.rb +117 -111
- data/lib/toys/utils/terminal.rb +69 -62
- data/lib/toys/wrappable_string.rb +162 -0
- metadata +24 -22
- data/lib/toys/definition/acceptor.rb +0 -191
- data/lib/toys/definition/alias.rb +0 -112
- data/lib/toys/definition/arg.rb +0 -140
- data/lib/toys/definition/flag.rb +0 -370
- data/lib/toys/definition/flag_group.rb +0 -205
- data/lib/toys/definition/source_info.rb +0 -190
- data/lib/toys/definition/tool.rb +0 -842
- data/lib/toys/dsl/arg.rb +0 -132
- data/lib/toys/runner.rb +0 -188
- data/lib/toys/standard_middleware.rb +0 -47
- data/lib/toys/utils/module_lookup.rb +0 -135
- 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
|
data/lib/toys/input_file.rb
CHANGED
@@ -1,32 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
4
|
#
|
5
|
-
#
|
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
|
-
#
|
8
|
-
#
|
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
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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::
|
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,
|
50
|
+
eval(str, __binding, path, -2)
|
59
51
|
# rubocop:enable Security/Eval
|
60
52
|
end
|
61
53
|
end
|
data/lib/toys/loader.rb
CHANGED
@@ -1,32 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
4
|
#
|
5
|
-
#
|
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
|
-
#
|
8
|
-
#
|
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
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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]
|
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]
|
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]
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# @param [String,nil]
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# @param [Array]
|
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]
|
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::
|
67
|
+
# @param mixin_lookup [Toys::ModuleLookup] A lookup for well-known
|
74
68
|
# mixin modules. Defaults to an empty lookup.
|
75
|
-
# @param [Toys::
|
69
|
+
# @param middleware_lookup [Toys::ModuleLookup] A lookup for
|
76
70
|
# well-known middleware classes. Defaults to an empty lookup.
|
77
|
-
# @param [Toys::
|
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,
|
81
|
-
|
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 ||
|
87
|
-
@middleware_lookup = middleware_lookup ||
|
88
|
-
@template_lookup = template_lookup ||
|
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
|
-
@
|
92
|
-
@
|
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
|
-
|
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>]
|
105
|
-
# @param [Boolean]
|
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 =
|
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]
|
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]
|
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 =
|
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
|
-
#
|
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>]
|
149
|
-
# @return [Array(Toys::
|
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
|
-
|
159
|
-
if
|
160
|
-
finish_definitions_in_tree(
|
161
|
-
return [
|
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>]
|
176
|
-
# @param [Boolean]
|
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]
|
176
|
+
# @param include_hidden [Boolean] If true, include hidden subtools,
|
179
177
|
# e.g. names beginning with underscores.
|
180
|
-
# @return [Array<Toys::
|
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>]
|
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.
|
211
|
-
|
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>]
|
224
|
-
# @param [Integer]
|
225
|
-
#
|
226
|
-
#
|
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
|
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
|
-
|
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>]
|
242
|
-
# @param [Array<String>]
|
243
|
-
# @param [Integer]
|
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::
|
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 =
|
258
|
+
alias_def = Alias.new(self, words, target, priority)
|
256
259
|
tool_data.definitions[priority] = alias_def
|
257
|
-
|
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>]
|
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
|
281
|
-
parent = words.empty? ? nil :
|
282
|
-
if parent.is_a?(
|
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
|
-
|
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]
|
300
|
-
# @return [Module
|
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]
|
310
|
-
# @return [Class
|
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
|
391
|
+
when Alias
|
382
392
|
resolve_alias(result, looked_up)
|
383
|
-
when
|
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?(
|
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 =
|
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 =
|
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, @
|
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 == @
|
491
|
-
child == @
|
492
|
-
child_source = source.relative_child(child, @
|
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 @
|
507
|
-
preload_dir = ::File.join(path, @
|
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?
|
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
|
|