toys-core 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.md +16 -24
  4. data/README.md +307 -59
  5. data/docs/guide.md +44 -4
  6. data/lib/toys-core.rb +58 -49
  7. data/lib/toys/acceptor.rb +672 -0
  8. data/lib/toys/alias.rb +106 -0
  9. data/lib/toys/arg_parser.rb +624 -0
  10. data/lib/toys/cli.rb +422 -181
  11. data/lib/toys/compat.rb +83 -0
  12. data/lib/toys/completion.rb +442 -0
  13. data/lib/toys/context.rb +354 -0
  14. data/lib/toys/core_version.rb +18 -26
  15. data/lib/toys/dsl/flag.rb +213 -56
  16. data/lib/toys/dsl/flag_group.rb +237 -51
  17. data/lib/toys/dsl/positional_arg.rb +210 -0
  18. data/lib/toys/dsl/tool.rb +968 -317
  19. data/lib/toys/errors.rb +46 -28
  20. data/lib/toys/flag.rb +821 -0
  21. data/lib/toys/flag_group.rb +282 -0
  22. data/lib/toys/input_file.rb +18 -26
  23. data/lib/toys/loader.rb +110 -100
  24. data/lib/toys/middleware.rb +24 -31
  25. data/lib/toys/mixin.rb +90 -59
  26. data/lib/toys/module_lookup.rb +125 -0
  27. data/lib/toys/positional_arg.rb +184 -0
  28. data/lib/toys/source_info.rb +192 -0
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
  32. data/lib/toys/standard_middleware/show_help.rb +130 -113
  33. data/lib/toys/standard_middleware/show_root_version.rb +29 -35
  34. data/lib/toys/standard_mixins/exec.rb +116 -78
  35. data/lib/toys/standard_mixins/fileutils.rb +16 -24
  36. data/lib/toys/standard_mixins/gems.rb +29 -30
  37. data/lib/toys/standard_mixins/highline.rb +34 -41
  38. data/lib/toys/standard_mixins/terminal.rb +72 -26
  39. data/lib/toys/template.rb +51 -35
  40. data/lib/toys/tool.rb +1161 -206
  41. data/lib/toys/utils/completion_engine.rb +171 -0
  42. data/lib/toys/utils/exec.rb +279 -182
  43. data/lib/toys/utils/gems.rb +58 -49
  44. data/lib/toys/utils/help_text.rb +117 -111
  45. data/lib/toys/utils/terminal.rb +69 -62
  46. data/lib/toys/wrappable_string.rb +162 -0
  47. metadata +24 -22
  48. data/lib/toys/definition/acceptor.rb +0 -191
  49. data/lib/toys/definition/alias.rb +0 -112
  50. data/lib/toys/definition/arg.rb +0 -140
  51. data/lib/toys/definition/flag.rb +0 -370
  52. data/lib/toys/definition/flag_group.rb +0 -205
  53. data/lib/toys/definition/source_info.rb +0 -190
  54. data/lib/toys/definition/tool.rb +0 -842
  55. data/lib/toys/dsl/arg.rb +0 -132
  56. data/lib/toys/runner.rb +0 -188
  57. data/lib/toys/standard_middleware.rb +0 -47
  58. data/lib/toys/utils/module_lookup.rb +0 -135
  59. data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -2,15 +2,55 @@
2
2
 
3
3
  # Toys-Core User Guide
4
4
 
5
- Toys-Core is the command line tool framework underlying Toys. It can be used
6
- to create command line binaries using the internal Toys APIs.
5
+ Toys-Core is the command line framework underlying Toys. It implements most of
6
+ the core functionality of Toys, including the tool DSL, argument parsing,
7
+ loading Toys files, online help, subprocess control, and so forth. It can be
8
+ used to create custom command line executables using the same facilities. You
9
+ can generally write
7
10
 
8
11
  This user's guide covers everything you need to know to build your own command
9
- line binaries in Ruby using the Toys-Core framework.
12
+ line executables in Ruby using the Toys-Core framework.
10
13
 
11
14
  This guide assumes you are already familiar with Toys itself, including how to
12
15
  define tools by writing Toys files, parsing arguments and flags, and how tools
13
16
  are executed. For background, please see the
14
17
  [Toys User's Guide](https://www.rubydoc.info/gems/toys/file/docs/guide.md).
15
18
 
16
- (To be written)
19
+ **(This user's guide is still under construction.)**
20
+
21
+ ## Conceptual overview
22
+
23
+ Toys-Core is a command line *framework* in the traditional sense. It is
24
+ intended to be used to write custom command line executables in Ruby. It
25
+ provides libraries to handle basic functions such as argumet parsing and online
26
+ help, and you provide the actual behavior.
27
+
28
+ The entry point for Toys-Core is the **cli object**. Typically your executable
29
+ script instantiates a CLI, configures it with the desired tool implementations,
30
+ and runs it.
31
+
32
+ An executable defines its functionality using the **Toys DSL** which can be
33
+ written in **toys files** or in **blocks** passed to the CLI. It uses the same
34
+ DSL used by Toys itself, and supports tools, subtools, flags, arguments, help
35
+ text, and all the other features of Toys.
36
+
37
+ An executable may customize its own facilities for writing tools by providing
38
+ **built-in mixins** and **built-in templates**, and can implement default
39
+ behavior across all tools by providing **middleware**.
40
+
41
+ Most executables will provide a set of **static tools**, but it is possible to
42
+ support user-provided tools as Toys does. Executables can customize how tool
43
+ definitions are searched and loaded from the file system.
44
+
45
+ Finally, an executable may customize many aspects of its behavior, such as the
46
+ **logging output**, **error handling**, and even shell **tab completion**.
47
+
48
+ ## Using the CLI object
49
+
50
+ ## Writing tools
51
+
52
+ ## Customizing the tool environment
53
+
54
+ ## Customizing default behavior
55
+
56
+ ## Packaging your executable
@@ -1,52 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2018 Daniel Azuma
3
+ # Copyright 2019 Daniel Azuma
4
4
  #
5
- # All rights reserved.
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
6
11
  #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
9
14
  #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
30
22
  ;
31
23
 
32
24
  ##
33
- # Toys is a Ruby library and command line tool that lets you build your own
34
- # command line suite of tools (with commands and subcommands) using a Ruby DSL.
35
- # You can define commands globally or configure special commands scoped to
36
- # individual directories.
25
+ # Toys is a configurable command line tool. Write commands in config files
26
+ # using a simple DSL, and Toys will provide the command line executable and
27
+ # take care of all the details such as argument parsing, online help, and error
28
+ # reporting. Toys is designed for software developers, IT professionals, and
29
+ # other power users who want to write and organize scripts to automate their
30
+ # workflows. It can also be used as a Rake replacement, providing a more
31
+ # natural command line interface for your project's build tasks.
32
+ #
33
+ # This module contains the command line framework underlying Toys. It can be
34
+ # used to create command line executables using the Toys DSL and classes.
37
35
  #
38
36
  module Toys
39
- ##
40
- # Namespace for object definition classes.
41
- #
42
- module Definition; end
43
-
44
37
  ##
45
38
  # Namespace for DSL classes. These classes provide the directives that can be
46
39
  # used in configuration files. Most are defined in {Toys::DSL::Tool}.
47
40
  #
48
41
  module DSL; end
49
42
 
43
+ ##
44
+ # Namespace for standard middleware classes.
45
+ #
46
+ module StandardMiddleware
47
+ ## @private
48
+ COMMON_FLAG_GROUP = :__common
49
+
50
+ ## @private
51
+ def self.append_common_flag_group(tool)
52
+ tool.add_flag_group(type: :optional, name: COMMON_FLAG_GROUP,
53
+ desc: "Common Flags", report_collisions: false)
54
+ COMMON_FLAG_GROUP
55
+ end
56
+ end
57
+
50
58
  ##
51
59
  # Namespace for standard mixin classes.
52
60
  #
@@ -55,34 +63,35 @@ module Toys
55
63
  ##
56
64
  # Namespace for common utility classes.
57
65
  #
66
+ # These classes are not loaded by default, and must be required explicitly.
67
+ # For example, before using {Toys::Utils::Exec}, you must
68
+ # `require "toys/utils/exec"`.
69
+ #
58
70
  module Utils; end
59
71
  end
60
72
 
73
+ require "toys/acceptor"
74
+ require "toys/alias"
75
+ require "toys/arg_parser"
61
76
  require "toys/cli"
77
+ require "toys/compat"
78
+ require "toys/completion"
79
+ require "toys/context"
62
80
  require "toys/core_version"
63
- require "toys/definition/acceptor"
64
- require "toys/definition/alias"
65
- require "toys/definition/arg"
66
- require "toys/definition/flag"
67
- require "toys/definition/flag_group"
68
- require "toys/definition/source_info"
69
- require "toys/definition/tool"
70
- require "toys/dsl/arg"
71
81
  require "toys/dsl/flag"
72
82
  require "toys/dsl/flag_group"
83
+ require "toys/dsl/positional_arg"
73
84
  require "toys/dsl/tool"
74
85
  require "toys/errors"
86
+ require "toys/flag"
87
+ require "toys/flag_group"
75
88
  require "toys/input_file"
76
89
  require "toys/loader"
77
90
  require "toys/middleware"
78
91
  require "toys/mixin"
79
- require "toys/runner"
80
- require "toys/standard_middleware"
92
+ require "toys/module_lookup"
93
+ require "toys/positional_arg"
94
+ require "toys/source_info"
81
95
  require "toys/template"
82
96
  require "toys/tool"
83
- require "toys/utils/exec"
84
- require "toys/utils/gems"
85
- require "toys/utils/help_text"
86
- require "toys/utils/module_lookup"
87
- require "toys/utils/terminal"
88
- require "toys/utils/wrappable_string"
97
+ require "toys/wrappable_string"
@@ -0,0 +1,672 @@
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
+ # An Acceptor validates and converts arguments. It is designed to be
27
+ # compatible with the OptionParser accept mechanism.
28
+ #
29
+ # First, an acceptor validates the argument via its
30
+ # {Toys::Acceptor::Base#match} method. This method should determine whether
31
+ # the argument is valid, and return information that will help with
32
+ # conversion of the argument.
33
+ #
34
+ # Second, an acceptor converts the argument to its final form via the
35
+ # {Toys::Acceptor::Base#convert} method.
36
+ #
37
+ # Finally, an acceptor has a name that may appear in help text for flags and
38
+ # arguments that use it.
39
+ #
40
+ module Acceptor
41
+ ##
42
+ # A sentinel that may be returned from a function-based acceptor to
43
+ # indicate invalid input.
44
+ # @return [Object]
45
+ #
46
+ REJECT = ::Object.new.freeze
47
+
48
+ ##
49
+ # The default type description.
50
+ # @return [String]
51
+ #
52
+ DEFAULT_TYPE_DESC = "string"
53
+
54
+ ##
55
+ # A base class for acceptors.
56
+ #
57
+ # The base acceptor does not do any validation (i.e. it accepts all
58
+ # arguments) or conversion (i.e. it returns the original string). You can
59
+ # subclass this base class and override the {#match} and {#convert} methods
60
+ # to implement an acceptor.
61
+ #
62
+ class Base
63
+ ##
64
+ # Create a base acceptor.
65
+ #
66
+ # @param type_desc [String] Type description string, shown in help.
67
+ # Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
68
+ # @param well_known_spec [Object] The well-known acceptor spec associated
69
+ # with this acceptor, or `nil` for none.
70
+ #
71
+ def initialize(type_desc: nil, well_known_spec: nil)
72
+ @type_desc = type_desc || DEFAULT_TYPE_DESC
73
+ @well_known_spec = well_known_spec
74
+ end
75
+
76
+ ##
77
+ # Type description string, shown in help.
78
+ # @return [String]
79
+ #
80
+ attr_reader :type_desc
81
+
82
+ ##
83
+ # The well-known acceptor spec associated with this acceptor, if any.
84
+ # This generally identifies an OptionParser-compatible acceptor spec. For
85
+ # example, the acceptor object that corresponds to `Integer` will return
86
+ # `Integer` from this attribute.
87
+ #
88
+ # @return [Object] the well-known acceptor
89
+ # @return [nil] if there is no corresponding well-known acceptor
90
+ #
91
+ attr_reader :well_known_spec
92
+
93
+ ##
94
+ # Type description string, shown in help.
95
+ # @return [String]
96
+ #
97
+ def to_s
98
+ type_desc.to_s
99
+ end
100
+
101
+ ##
102
+ # Validate the given input.
103
+ #
104
+ # When given a valid input, return an array in which the first element is
105
+ # the original input string, and the remaining elements (which may be
106
+ # empty) comprise any additional information that may be useful during
107
+ # conversion. If there is no additional information, you may return the
108
+ # original input string by itself without wrapping in an array.
109
+ #
110
+ # When given an invalid input, return a falsy value such as `nil`.
111
+ #
112
+ # Note that a `MatchData` object is a legitimate return value because it
113
+ # duck-types the appropriate array.
114
+ #
115
+ # This default implementation simply returns the original input string,
116
+ # as the only array element, indicating all inputs are valid. You can
117
+ # override this method to provide a different validation function.
118
+ #
119
+ # @param str [String,nil] The input argument string. May be `nil` if the
120
+ # value is optional and not provided.
121
+ # @return [String,Array,nil]
122
+ #
123
+ def match(str)
124
+ [str]
125
+ end
126
+
127
+ ##
128
+ # Convert the given input.
129
+ #
130
+ # This method is passed the results of a successful match, including the
131
+ # original input string and any other values returned from {#match}. It
132
+ # must return the final converted value to use.
133
+ #
134
+ # @param str [String,nil] Original argument string. May be `nil` if the
135
+ # value is optional and not provided.
136
+ # @param extra [Object...] Zero or more additional arguments comprising
137
+ # additional elements returned from the match function.
138
+ # @return [Object] The converted argument as it should be stored in the
139
+ # context data.
140
+ #
141
+ def convert(str, *extra) # rubocop:disable Lint/UnusedMethodArgument
142
+ str.nil? ? true : str
143
+ end
144
+
145
+ ##
146
+ # Return suggestions for a given non-matching string.
147
+ #
148
+ # This method may be called when a match fails. It should return a
149
+ # (possibly empty) array of suggestions that could be displayed to the
150
+ # user as "did you mean..."
151
+ #
152
+ # The default implementation returns the empty list.
153
+ #
154
+ # @param str [String] A string that failed matching.
155
+ # @return [Array<String>] A possibly empty array of alternative
156
+ # suggestions that could be displayed with "did you mean..."
157
+ #
158
+ def suggestions(str) # rubocop:disable Lint/UnusedMethodArgument
159
+ []
160
+ end
161
+ end
162
+
163
+ ##
164
+ # The default acceptor. Corresponds to the well-known acceptor for
165
+ # `Object`.
166
+ # @return [Toys::Acceptor::Base]
167
+ #
168
+ DEFAULT = Base.new(type_desc: "string", well_known_spec: ::Object)
169
+
170
+ ##
171
+ # An acceptor that uses a simple function to validate and convert input.
172
+ # The function must take the input string as its argument, and either
173
+ # return the converted object to indicate success, or raise an exception or
174
+ # return the sentinel {Toys::Acceptor::REJECT} to indicate invalid input.
175
+ #
176
+ class Simple < Base
177
+ ##
178
+ # Create a simple acceptor.
179
+ #
180
+ # You should provide an acceptor function, either as a proc in the
181
+ # `function` argument, or as a block. The function must take as its one
182
+ # argument the input string. If the string is valid, the function must
183
+ # return the value to store in the tool's data. If the string is invalid,
184
+ # the function may either raise an exception (which must descend from
185
+ # `StandardError`) or return {Toys::Acceptor::REJECT}.
186
+ #
187
+ # @param function [Proc] The acceptor function
188
+ # @param type_desc [String] Type description string, shown in help.
189
+ # Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
190
+ # @param well_known_spec [Object] The well-known acceptor spec associated
191
+ # with this acceptor, or `nil` for none.
192
+ # @param block [Proc] The acceptor function, if not provided as a normal
193
+ # parameter.
194
+ #
195
+ def initialize(function = nil, type_desc: nil, well_known_spec: nil, &block)
196
+ super(type_desc: type_desc, well_known_spec: well_known_spec)
197
+ @function = function || block || proc { |s| s }
198
+ end
199
+
200
+ ##
201
+ # Overrides {Toys::Acceptor::Base#match} to use the given function.
202
+ # @private
203
+ #
204
+ def match(str)
205
+ result = @function.call(str) rescue REJECT # rubocop:disable Style/RescueModifier
206
+ result.equal?(REJECT) ? nil : [str, result]
207
+ end
208
+
209
+ ##
210
+ # Overrides {Toys::Acceptor::Base#convert} to use the given function's
211
+ # result.
212
+ # @private
213
+ #
214
+ def convert(_str, result)
215
+ result
216
+ end
217
+ end
218
+
219
+ ##
220
+ # An acceptor that uses a regex to validate input. It also supports a
221
+ # custom conversion function that generates the final value from the match
222
+ # results.
223
+ #
224
+ class Pattern < Base
225
+ ##
226
+ # Create a pattern acceptor.
227
+ #
228
+ # You must provide a regular expression (or any object that duck-types
229
+ # `Regexp#match`) as a validator.
230
+ #
231
+ # You may also optionally provide a converter, either as a proc or a
232
+ # block. A converter must take as its arguments the values in the
233
+ # `MatchData` returned from a successful regex match. That is, the first
234
+ # argument is the original input string, and the remaining arguments are
235
+ # the captures. The converter must return the final converted value.
236
+ # If no converter is provided, no conversion is done and the input string
237
+ # is returned.
238
+ #
239
+ # @param regex [Regexp] Regular expression defining value values.
240
+ # @param converter [Proc] An optional converter function. May also be
241
+ # given as a block. Note that the converter will be passed all
242
+ # elements of the `MatchData`.
243
+ # @param type_desc [String] Type description string, shown in help.
244
+ # Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
245
+ # @param well_known_spec [Object] The well-known acceptor spec associated
246
+ # with this acceptor, or `nil` for none.
247
+ # @param block [Proc] A converter function, if not provided as a normal
248
+ # parameter.
249
+ #
250
+ def initialize(regex, converter = nil, type_desc: nil, well_known_spec: nil, &block)
251
+ super(type_desc: type_desc, well_known_spec: well_known_spec)
252
+ @regex = regex
253
+ @converter = converter || block
254
+ end
255
+
256
+ ##
257
+ # Overrides {Toys::Acceptor::Base#match} to use the given regex.
258
+ # @private
259
+ #
260
+ def match(str)
261
+ str.nil? ? [nil] : @regex.match(str)
262
+ end
263
+
264
+ ##
265
+ # Overrides {Toys::Acceptor::Base#convert} to use the given converter.
266
+ # @private
267
+ #
268
+ def convert(str, *extra)
269
+ @converter ? @converter.call(str, *extra) : str
270
+ end
271
+ end
272
+
273
+ ##
274
+ # An acceptor that recognizes a fixed set of values.
275
+ #
276
+ # You provide a list of valid values. The input argument string will be
277
+ # matched against the string forms of these valid values. If it matches,
278
+ # the converter will return the actual value from the valid list.
279
+ #
280
+ # For example, you could pass `[:one, :two, 3]` as the set of values. If
281
+ # an argument of `"two"` is passed in, the converter will yield a final
282
+ # value of the symbol `:two`. If an argument of "3" is passed in, the
283
+ # converter will yield the integer `3`. If an argument of "three" is
284
+ # passed in, the match will fail.
285
+ #
286
+ class Enum < Base
287
+ ##
288
+ # Create an acceptor.
289
+ #
290
+ # @param values [Array<Object>] Valid values.
291
+ # @param type_desc [String] Type description string, shown in help.
292
+ # Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
293
+ # @param well_known_spec [Object] The well-known acceptor spec associated
294
+ # with this acceptor, or `nil` for none.
295
+ #
296
+ def initialize(values, type_desc: nil, well_known_spec: nil)
297
+ super(type_desc: type_desc, well_known_spec: well_known_spec)
298
+ @values = Array(values).map { |v| [v.to_s, v] }
299
+ end
300
+
301
+ ##
302
+ # The array of enum values.
303
+ # @return [Array<Object>]
304
+ #
305
+ attr_reader :values
306
+
307
+ ##
308
+ # Overrides {Toys::Acceptor::Base#match} to find the value.
309
+ # @private
310
+ #
311
+ def match(str)
312
+ str.nil? ? [nil, nil] : @values.find { |s, _e| s == str }
313
+ end
314
+
315
+ ##
316
+ # Overrides {Toys::Acceptor::Base#convert} to return the actual enum
317
+ # element.
318
+ # @private
319
+ #
320
+ def convert(_str, elem)
321
+ elem
322
+ end
323
+
324
+ ##
325
+ # Overrides {Toys::Acceptor::Base#suggestions} to return close matches
326
+ # from the enum.
327
+ # @private
328
+ #
329
+ def suggestions(str)
330
+ Compat.suggestions(str, @values.map(&:first))
331
+ end
332
+ end
333
+
334
+ ##
335
+ # An acceptor that recognizes a range of values.
336
+ #
337
+ # The input argument is matched against the given range. For example, you
338
+ # can match against the integers from 1 to 10 by passing the range
339
+ # `(1..10)`.
340
+ #
341
+ # You can also provide a conversion function that takes the input string
342
+ # and converts it an object that can be compared by the range. If you do
343
+ # not provide a converter, a default converter will be provided depending
344
+ # on the types of objects serving as the range limits. Specifically:
345
+ #
346
+ # * If the range beginning and end are both `Integer`, then input strings
347
+ # are likewise converted to `Integer` when matched against the range.
348
+ # Accepted values are returned as integers.
349
+ # * If the range beginning and end are both `Float`, then input strings
350
+ # are likewise converted to `Float`.
351
+ # * If the range beginning and end are both `Rational`, then input
352
+ # strings are likewise converted to `Rational`.
353
+ # * If the range beginning and end are both `Numeric` types but different
354
+ # subtypes (e.g. an `Integer` and a `Float`), then any type of numeric
355
+ # input (integer, float, rational) is accepted and matched against the
356
+ # range.
357
+ # * If the range beginning and/or end are not numeric types, then no
358
+ # conversion is done by default.
359
+ #
360
+ class Range < Simple
361
+ ##
362
+ # Create an acceptor.
363
+ #
364
+ # @param range [Range] The range of acceptable values
365
+ # @param converter [Proc] A converter proc that takes an input string and
366
+ # attempts to convert it to a type comparable by the range. For
367
+ # numeric ranges, this can be omitted because one is provided by
368
+ # default. You should provide a converter for other types of ranges.
369
+ # You can also pass the converter as a block.
370
+ # @param type_desc [String] Type description string, shown in help.
371
+ # Defaults to {Toys::Acceptor::DEFAULT_TYPE_DESC}.
372
+ # @param well_known_spec [Object] The well-known acceptor spec associated
373
+ # with this acceptor, or `nil` for none.
374
+ # @param block [Proc] Converter function, if not provided as a normal
375
+ # parameter.
376
+ #
377
+ def initialize(range, converter = nil, type_desc: nil, well_known_spec: nil, &block)
378
+ converter ||= block || make_converter(range.begin, range.end)
379
+ super(type_desc: type_desc, well_known_spec: well_known_spec) do |val|
380
+ val = converter.call(val) if converter
381
+ val.nil? || range.include?(val) ? val : REJECT
382
+ end
383
+ @range = range
384
+ end
385
+
386
+ ##
387
+ # The range being checked.
388
+ # @return [Range]
389
+ #
390
+ attr_reader :range
391
+
392
+ private
393
+
394
+ def make_converter(val1, val2)
395
+ if val1.is_a?(::Integer) && val2.is_a?(::Integer)
396
+ INTEGER_CONVERTER
397
+ elsif val1.is_a?(::Float) && val2.is_a?(::Float)
398
+ FLOAT_CONVERTER
399
+ elsif val1.is_a?(::Rational) && val2.is_a?(::Rational)
400
+ RATIONAL_CONVERTER
401
+ elsif val1.is_a?(::Numeric) && val2.is_a?(::Numeric)
402
+ NUMERIC_CONVERTER
403
+ end
404
+ end
405
+ end
406
+
407
+ ##
408
+ # A converter proc that handles integers. Useful in Simple and Range
409
+ # acceptors.
410
+ # @return [Proc]
411
+ #
412
+ INTEGER_CONVERTER = proc { |s| s.nil? ? nil : Integer(s) }
413
+
414
+ ##
415
+ # A converter proc that handles floats. Useful in Simple and Range
416
+ # acceptors.
417
+ # @return [Proc]
418
+ #
419
+ FLOAT_CONVERTER = proc { |s| s.nil? ? nil : Float(s) }
420
+
421
+ ##
422
+ # A converter proc that handles rationals. Useful in Simple and Range
423
+ # acceptors.
424
+ # @return [Proc]
425
+ #
426
+ RATIONAL_CONVERTER = proc { |s| s.nil? ? nil : Rational(s) }
427
+
428
+ ##
429
+ # A converter proc that handles any numeric value. Useful in Simple and
430
+ # Range acceptors.
431
+ # @return [Proc]
432
+ #
433
+ NUMERIC_CONVERTER =
434
+ proc do |s|
435
+ if s.nil?
436
+ nil
437
+ elsif s.include?("/")
438
+ Rational(s)
439
+ elsif s.include?(".") || (s.include?("e") && s !~ /\A-?0x/)
440
+ Float(s)
441
+ else
442
+ Integer(s)
443
+ end
444
+ end
445
+
446
+ class << self
447
+ ##
448
+ # Lookup a standard acceptor name recognized by OptionParser.
449
+ #
450
+ # @param spec [Object] A well-known acceptor specification, such as
451
+ # `String`, `Integer`, `Array`, `OptionParser::DecimalInteger`, etc.
452
+ # @return [Toys::Acceptor::Base] The corresponding Acceptor object
453
+ # @return [nil] if the given standard acceptor was not recognized.
454
+ #
455
+ def lookup_well_known(spec)
456
+ result = standard_well_knowns[spec]
457
+ if result.nil? && defined?(::OptionParser)
458
+ result = optparse_well_knowns[spec]
459
+ end
460
+ result
461
+ end
462
+
463
+ ##
464
+ # Create an acceptor from a variety of specification formats. The
465
+ # acceptor is constructed from the given specification object and/or the
466
+ # given block. Additionally, some acceptors can take an optional type
467
+ # description string used to describe the type of data in online help.
468
+ #
469
+ # Recognized specs include:
470
+ #
471
+ # * Any well-known acceptor recognized by OptionParser, such as
472
+ # `Integer`, `Array`, or `OptionParser::DecimalInteger`. Any block
473
+ # and type description you provide are ignored.
474
+ #
475
+ # * Any **regular expression**. The returned acceptor validates only if
476
+ # the regex matches the *entire string parameter*.
477
+ #
478
+ # You can also provide an optional conversion function as a block. If
479
+ # provided, the block must take a variable number of arguments, the
480
+ # first being the matched string and the remainder being the captures
481
+ # from the regular expression. It should return the converted object
482
+ # that will be stored in the context data. If you do not provide a
483
+ # block, no conversion takes place, and the original string is used.
484
+ #
485
+ # * An **array** of possible values. The acceptor validates if the
486
+ # string parameter matches the *string form* of one of the array
487
+ # elements (i.e. the results of calling `to_s` on the element.)
488
+ #
489
+ # An array acceptor automatically converts the string parameter to
490
+ # the actual array element that it matched. For example, if the
491
+ # symbol `:foo` is in the array, it will match the string `"foo"`,
492
+ # and then store the symbol `:foo` in the tool data. You may not
493
+ # further customize the conversion function; any block is ignored.
494
+ #
495
+ # * A **range** of possible values. The acceptor validates if the
496
+ # string parameter, after conversion to the range type, lies within
497
+ # the range. The final value stored in context data is the converted
498
+ # value. For numeric ranges, conversion is provided, but if the range
499
+ # has a different type, you must provide the conversion function as
500
+ # a block.
501
+ #
502
+ # * A **function** as a Proc (where the block is ignored) or a block
503
+ # (if the spec is nil). This function performs *both* validation and
504
+ # conversion. It should take the string parameter as its argument,
505
+ # and it must either return the object that should be stored in the
506
+ # tool data, or raise an exception (descended from `StandardError`)
507
+ # to indicate that the string parameter is invalid. You may also
508
+ # return the sentinel value {Toys::Acceptor::REJECT} to indicate that
509
+ # the string is invalid.
510
+ #
511
+ # * The value `nil` or `:default` with no block, to indicate the
512
+ # default pass-through acceptor {Toys::Acceptor::DEFAULT}. Any type
513
+ # description you provide is ignored.
514
+ #
515
+ # @param spec [Object] See the description for recognized values.
516
+ # @param type_desc [String] The type description for interpolating into
517
+ # help text. Ignored if the spec indicates the default acceptor or a
518
+ # well-known acceptor.
519
+ # @param block [Proc] See the description for recognized forms.
520
+ # @return [Toys::Acceptor::Base,Proc]
521
+ #
522
+ def create(spec = nil, type_desc: nil, &block)
523
+ well_known = lookup_well_known(spec)
524
+ return well_known if well_known
525
+ case spec
526
+ when Base
527
+ spec
528
+ when ::Regexp
529
+ Pattern.new(spec, type_desc: type_desc, &block)
530
+ when ::Array
531
+ Enum.new(spec, type_desc: type_desc)
532
+ when ::Proc
533
+ Simple.new(spec, type_desc: type_desc)
534
+ when ::Range
535
+ Range.new(spec, type_desc: type_desc, &block)
536
+ when nil, :default
537
+ block ? Simple.new(type_desc: type_desc, &block) : DEFAULT
538
+ else
539
+ raise ToolDefinitionError, "Illegal acceptor spec: #{spec.inspect}"
540
+ end
541
+ end
542
+
543
+ private
544
+
545
+ def standard_well_knowns
546
+ @standard_well_knowns ||= {
547
+ ::Object => DEFAULT,
548
+ ::NilClass => build_nil,
549
+ ::String => build_string,
550
+ ::Integer => build_integer,
551
+ ::Float => build_float,
552
+ ::Rational => build_rational,
553
+ ::Numeric => build_numeric,
554
+ ::TrueClass => build_boolean(::TrueClass, true),
555
+ ::FalseClass => build_boolean(::FalseClass, false),
556
+ ::Array => build_array,
557
+ ::Regexp => build_regexp,
558
+ }
559
+ end
560
+
561
+ def optparse_well_knowns
562
+ @optparse_well_knowns ||= {
563
+ ::OptionParser::DecimalInteger => build_decimal_integer,
564
+ ::OptionParser::OctalInteger => build_octal_integer,
565
+ ::OptionParser::DecimalNumeric => build_decimal_numeric,
566
+ }
567
+ end
568
+
569
+ def build_nil
570
+ Simple.new(type_desc: "string", well_known_spec: ::NilClass) { |s| s }
571
+ end
572
+
573
+ def build_string
574
+ Pattern.new(/.+/m, type_desc: "nonempty string", well_known_spec: ::String)
575
+ end
576
+
577
+ def build_integer
578
+ Simple.new(INTEGER_CONVERTER, type_desc: "integer", well_known_spec: ::Integer)
579
+ end
580
+
581
+ def build_float
582
+ Simple.new(FLOAT_CONVERTER, type_desc: "floating point number", well_known_spec: ::Float)
583
+ end
584
+
585
+ def build_rational
586
+ Simple.new(RATIONAL_CONVERTER, type_desc: "rational number", well_known_spec: ::Rational)
587
+ end
588
+
589
+ def build_numeric
590
+ Simple.new(NUMERIC_CONVERTER, type_desc: "number", well_known_spec: ::Numeric)
591
+ end
592
+
593
+ TRUE_STRINGS = ["+", "true", "yes"].freeze
594
+ FALSE_STRINGS = ["-", "false", "no", "nil"].freeze
595
+ private_constant :TRUE_STRINGS, :FALSE_STRINGS
596
+
597
+ def build_boolean(spec, default)
598
+ Simple.new(type_desc: "boolean", well_known_spec: spec) do |s|
599
+ if s.nil?
600
+ default
601
+ else
602
+ s = s.downcase
603
+ if s.empty?
604
+ REJECT
605
+ elsif TRUE_STRINGS.any? { |t| t.start_with?(s) }
606
+ true
607
+ elsif FALSE_STRINGS.any? { |f| f.start_with?(s) }
608
+ false
609
+ else
610
+ REJECT
611
+ end
612
+ end
613
+ end
614
+ end
615
+
616
+ def build_array
617
+ Simple.new(type_desc: "string array", well_known_spec: ::Array) do |s|
618
+ if s.nil?
619
+ nil
620
+ else
621
+ s.split(",").collect { |elem| elem unless elem.empty? }
622
+ end
623
+ end
624
+ end
625
+
626
+ def build_regexp
627
+ Simple.new(type_desc: "regular expression", well_known_spec: ::Regexp) do |s|
628
+ if s.nil?
629
+ nil
630
+ else
631
+ flags = 0
632
+ if (match = %r{\A/((?:\\.|[^\\])*)/([[:alpha:]]*)\z}.match(s))
633
+ s = match[1]
634
+ opts = match[2] || ""
635
+ flags |= ::Regexp::IGNORECASE if opts.include?("i")
636
+ flags |= ::Regexp::MULTILINE if opts.include?("m")
637
+ flags |= ::Regexp::EXTENDED if opts.include?("x")
638
+ end
639
+ ::Regexp.new(s, flags)
640
+ end
641
+ end
642
+ end
643
+
644
+ def build_decimal_integer
645
+ Simple.new(type_desc: "decimal integer",
646
+ well_known_spec: ::OptionParser::DecimalInteger) do |s|
647
+ s.nil? ? nil : Integer(s, 10)
648
+ end
649
+ end
650
+
651
+ def build_octal_integer
652
+ Simple.new(type_desc: "octal integer",
653
+ well_known_spec: ::OptionParser::OctalInteger) do |s|
654
+ s.nil? ? nil : Integer(s, 8)
655
+ end
656
+ end
657
+
658
+ def build_decimal_numeric
659
+ Simple.new(type_desc: "decimal number",
660
+ well_known_spec: ::OptionParser::DecimalNumeric) do |s|
661
+ if s.nil?
662
+ nil
663
+ elsif s.include?(".") || (s.include?("e") && s !~ /\A-?0x/)
664
+ Float(s)
665
+ else
666
+ Integer(s, 10)
667
+ end
668
+ end
669
+ end
670
+ end
671
+ end
672
+ end