toys-core 0.11.5 → 0.13.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 +62 -0
- data/LICENSE.md +1 -1
- data/README.md +5 -2
- data/docs/guide.md +1 -1
- data/lib/toys/acceptor.rb +13 -4
- data/lib/toys/arg_parser.rb +7 -7
- data/lib/toys/cli.rb +170 -120
- data/lib/toys/compat.rb +71 -23
- data/lib/toys/completion.rb +18 -6
- data/lib/toys/context.rb +24 -15
- data/lib/toys/core.rb +6 -2
- data/lib/toys/dsl/base.rb +87 -0
- data/lib/toys/dsl/flag.rb +26 -20
- data/lib/toys/dsl/flag_group.rb +18 -14
- data/lib/toys/dsl/internal.rb +206 -0
- data/lib/toys/dsl/positional_arg.rb +26 -16
- data/lib/toys/dsl/tool.rb +180 -218
- data/lib/toys/errors.rb +64 -8
- data/lib/toys/flag.rb +662 -656
- data/lib/toys/flag_group.rb +24 -10
- data/lib/toys/input_file.rb +13 -7
- data/lib/toys/loader.rb +293 -140
- data/lib/toys/middleware.rb +46 -22
- data/lib/toys/mixin.rb +10 -8
- data/lib/toys/positional_arg.rb +21 -20
- data/lib/toys/settings.rb +914 -0
- data/lib/toys/source_info.rb +147 -35
- data/lib/toys/standard_middleware/add_verbosity_flags.rb +2 -0
- data/lib/toys/standard_middleware/apply_config.rb +6 -4
- data/lib/toys/standard_middleware/handle_usage_errors.rb +1 -0
- data/lib/toys/standard_middleware/set_default_descriptions.rb +19 -18
- data/lib/toys/standard_middleware/show_help.rb +19 -5
- data/lib/toys/standard_middleware/show_root_version.rb +2 -0
- data/lib/toys/standard_mixins/bundler.rb +24 -15
- data/lib/toys/standard_mixins/exec.rb +43 -34
- data/lib/toys/standard_mixins/fileutils.rb +3 -1
- data/lib/toys/standard_mixins/gems.rb +21 -17
- data/lib/toys/standard_mixins/git_cache.rb +46 -0
- data/lib/toys/standard_mixins/highline.rb +8 -8
- data/lib/toys/standard_mixins/terminal.rb +5 -5
- data/lib/toys/standard_mixins/xdg.rb +56 -0
- data/lib/toys/template.rb +11 -9
- data/lib/toys/{tool.rb → tool_definition.rb} +292 -226
- data/lib/toys/utils/completion_engine.rb +7 -2
- data/lib/toys/utils/exec.rb +162 -132
- data/lib/toys/utils/gems.rb +85 -60
- data/lib/toys/utils/git_cache.rb +813 -0
- data/lib/toys/utils/help_text.rb +117 -37
- data/lib/toys/utils/terminal.rb +11 -3
- data/lib/toys/utils/xdg.rb +293 -0
- data/lib/toys/wrappable_string.rb +9 -2
- data/lib/toys-core.rb +18 -6
- metadata +14 -7
@@ -0,0 +1,914 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module Toys
|
6
|
+
##
|
7
|
+
# A settings class defines the structure of application settings, i.e. the
|
8
|
+
# various fields that can be set, and their types. You can define a settings
|
9
|
+
# structure by subclassing this base class, and using the provided methods.
|
10
|
+
#
|
11
|
+
# ### Attributes
|
12
|
+
#
|
13
|
+
# To define an attribute, use the {Settings.settings_attr} declaration.
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
#
|
17
|
+
# class ServiceSettings < Toys::Settings
|
18
|
+
# settings_attr :endpoint, default: "api.example.com"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# my_settings = ServiceSettings.new
|
22
|
+
# my_settings.endpoint_set? # => false
|
23
|
+
# my_settings.endpoint # => "api.example.com"
|
24
|
+
# my_settings.endpoint = "rest.example.com"
|
25
|
+
# my_settings.endpoint_set? # => true
|
26
|
+
# my_settings.endpoint # => "rest.example.com"
|
27
|
+
# my_settings.endpoint_unset!
|
28
|
+
# my_settings.endpoint_set? # => false
|
29
|
+
# my_settings.endpoint # => "api.example.com"
|
30
|
+
#
|
31
|
+
# An attribute has a name, a default value, and a type specification. The
|
32
|
+
# name is used to define methods for getting and setting the attribute. The
|
33
|
+
# default is returned if no value is set. (See the section below on parents
|
34
|
+
# and defaults for more information.) The type specification governs what
|
35
|
+
# values are allowed. (See the section below on type specifications.)
|
36
|
+
#
|
37
|
+
# Attribute names must start with an ascii letter, and may contain only ascii
|
38
|
+
# letters, digits, and underscores. Unlike method names, they may not include
|
39
|
+
# non-ascii unicode characters, nor may they end with `!` or `?`.
|
40
|
+
# Additionally, the name `method_missing` is not allowed because of its
|
41
|
+
# special behavior in Ruby.
|
42
|
+
#
|
43
|
+
# Each attribute defines four methods: a getter, a setter, an unsetter, and a
|
44
|
+
# set detector. In the above example, the attribute named `:endpoint` creates
|
45
|
+
# the following four methods:
|
46
|
+
#
|
47
|
+
# * `endpoint` - retrieves the attribute value, or a default if not set.
|
48
|
+
# * `endpoint=(value)` - sets a new attribute value.
|
49
|
+
# * `endpoint_unset!` - unsets the attribute, reverting to a default.
|
50
|
+
# * `endpoint_set?` - returns a boolean, whether the attribute is set.
|
51
|
+
#
|
52
|
+
# ### Groups
|
53
|
+
#
|
54
|
+
# A group is a settings field that itself is a Settings object. You can use
|
55
|
+
# it to group settings fields in a hierarchy.
|
56
|
+
#
|
57
|
+
# Example:
|
58
|
+
#
|
59
|
+
# class ServiceSettings < Toys::Settings
|
60
|
+
# settings_attr :endpoint, default: "api.example.com"
|
61
|
+
# settings_group :service_flags do
|
62
|
+
# settings_attr :verbose, default: false
|
63
|
+
# settings_attr :use_proxy, default: false
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# my_settings = ServiceSettings.new
|
68
|
+
# my_settings.service_flags.verbose # => false
|
69
|
+
# my_settings.service_flags.verbose = true
|
70
|
+
# my_settings.service_flags.verbose # => true
|
71
|
+
# my_settings.endpoint # => "api.example.com"
|
72
|
+
#
|
73
|
+
# You can define a group inline, as in the example above, or create an
|
74
|
+
# explicit settings class and use it for the group. For example:
|
75
|
+
#
|
76
|
+
# class Flags < Toys::Settings
|
77
|
+
# settings_attr :verbose, default: false
|
78
|
+
# settings_attr :use_proxy, default: false
|
79
|
+
# end
|
80
|
+
# class ServiceSettings < Toys::Settings
|
81
|
+
# settings_attr :endpoint, default: "api.example.com"
|
82
|
+
# settings_group :service_flags, Flags
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# my_settings = ServiceSettings.new
|
86
|
+
# my_settings.service_flags.verbose = true
|
87
|
+
#
|
88
|
+
# If the module enclosing a subclass of `Settings` is itself a subclass of
|
89
|
+
# `Settings`, then the class is automatically added to its enclosing class as
|
90
|
+
# a group. For example:
|
91
|
+
#
|
92
|
+
# class ServiceSettings < Toys::Settings
|
93
|
+
# settings_attr :endpoint, default: "api.example.com"
|
94
|
+
# # Automatically adds this as the group service_flags.
|
95
|
+
# # The name is inferred (snake_cased) from the class name.
|
96
|
+
# class ServiceFlags < Toys::Settings
|
97
|
+
# settings_attr :verbose, default: false
|
98
|
+
# settings_attr :use_proxy, default: false
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# my_settings = ServiceSettings.new
|
103
|
+
# my_settings.service_flags.verbose = true
|
104
|
+
#
|
105
|
+
# ### Type specifications
|
106
|
+
#
|
107
|
+
# A type specification is a restriction on the types of values allowed for a
|
108
|
+
# settings field. Every attribute has a type specification. You can set it
|
109
|
+
# explicitly by providing a `:type` argument or a block. If a type
|
110
|
+
# specification is not provided explicitly, it is inferred from the default
|
111
|
+
# value of the attribute.
|
112
|
+
#
|
113
|
+
# Type specifications can be any of the following:
|
114
|
+
#
|
115
|
+
# * A Module, restricting values to those that include the module.
|
116
|
+
#
|
117
|
+
# For example, a type specification of `Enumerable` would accept `[123]`
|
118
|
+
# but not `123`.
|
119
|
+
#
|
120
|
+
# * A Class, restricting values to that class or any subclass.
|
121
|
+
#
|
122
|
+
# For example, a type specification of `Time` would accept `Time.now` but
|
123
|
+
# not `DateTime.now`.
|
124
|
+
#
|
125
|
+
# Note that some classes will convert (i.e. parse) strings. For example,
|
126
|
+
# a type specification of `Integer` will accept the string `"-123"`` and
|
127
|
+
# convert it to the value `-123`. Classes that support parsing include:
|
128
|
+
#
|
129
|
+
# * `Date`
|
130
|
+
# * `DateTime`
|
131
|
+
# * `Float`
|
132
|
+
# * `Integer`
|
133
|
+
# * `Regexp`
|
134
|
+
# * `Symbol`
|
135
|
+
# * `Time`
|
136
|
+
#
|
137
|
+
# * A Regexp, restricting values to strings matching the regexp.
|
138
|
+
#
|
139
|
+
# For example, a type specification of `/^\w+$/` would match `"abc"` but
|
140
|
+
# not `"abc!"`.
|
141
|
+
#
|
142
|
+
# * A Range, restricting values to objects that fall in the range and are
|
143
|
+
# of the same class (or a subclass) as the endpoints. String values are
|
144
|
+
# accepted if they can be converted to the endpoint class as specified by
|
145
|
+
# a class type specification.
|
146
|
+
#
|
147
|
+
# For example, a type specification of `(1..5)` would match `5` but not
|
148
|
+
# `6`. It would also match `"5"` because the String can be parsed into an
|
149
|
+
# Integer in the range.
|
150
|
+
#
|
151
|
+
# * A specific value, any Symbol, String, Numeric, or the values `nil`,
|
152
|
+
# `true`, or `false`, restricting the value to only that given value.
|
153
|
+
#
|
154
|
+
# For example, a type specification of `:foo` would match `:foo` but not
|
155
|
+
# `:bar`.
|
156
|
+
#
|
157
|
+
# (It might not seem terribly useful to have an attribute that can take
|
158
|
+
# only one value, but this type is generally used as part of a union
|
159
|
+
# type, described below, to implement an enumeration.)
|
160
|
+
#
|
161
|
+
# * An Array representing a union type, each of whose elements is one of
|
162
|
+
# the above types. Values are accepted if they match any of the elements.
|
163
|
+
#
|
164
|
+
# For example, a type specification of `[:a, :b :c]` would match `:a` but
|
165
|
+
# not `"a"`. Similarly, a type specification of `[String, Integer, nil]`
|
166
|
+
# would match `"hello"`, `123`, or `nil`, but not `123.4`.
|
167
|
+
#
|
168
|
+
# * A Proc that takes the proposed value and returns either the value if it
|
169
|
+
# is legal, the converted value if it can be converted to a legal value,
|
170
|
+
# or the constant {Toys::Settings::ILLEGAL_VALUE} if it cannot be
|
171
|
+
# converted to a legal value. You may also pass a block to
|
172
|
+
# `settings_attr` to set a Proc type specification.
|
173
|
+
#
|
174
|
+
# * A {Toys::Settings::Type} that checks and converts values.
|
175
|
+
#
|
176
|
+
# If you do not explicitly provide a type specification, one is inferred from
|
177
|
+
# the attribute's default value. The rules are:
|
178
|
+
#
|
179
|
+
# * If the default value is `true` or `false`, then the type specification
|
180
|
+
# inferred is `[true, false]`.
|
181
|
+
#
|
182
|
+
# * If the default value is `nil` or not provided, then the type
|
183
|
+
# specification allows any object (i.e. is equivalent to `Object`).
|
184
|
+
#
|
185
|
+
# * Otherwise, the type specification allows any value of the same class as
|
186
|
+
# the default value. For example, if the default value is `""`, the
|
187
|
+
# effective type specification is `String`.
|
188
|
+
#
|
189
|
+
# Examples:
|
190
|
+
#
|
191
|
+
# class ServiceSettings < Toys::Settings
|
192
|
+
# # Allows only strings because the default is a string.
|
193
|
+
# settings_attr :endpoint, default: "example.com"
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# class ServiceSettings < Toys::Settings
|
197
|
+
# # Allows strings or nil.
|
198
|
+
# settings_attr :endpoint, default: "example.com", type: [String, nil]
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
# class ServiceSettings < Toys::Settings
|
202
|
+
# # Raises ArgumentError because the default is nil, which does not
|
203
|
+
# # match the type specification. (You should either allow nil
|
204
|
+
# # explicitly with `type: [String, nil]` or set the default to a
|
205
|
+
# # suitable string such as the empty string "".)
|
206
|
+
# settings_attr :endpoint, type: String
|
207
|
+
# end
|
208
|
+
#
|
209
|
+
# ### Settings parents
|
210
|
+
#
|
211
|
+
# A settings object can have a "parent" which provides the values if they are
|
212
|
+
# not set in the settings object. This lets you organize settings as
|
213
|
+
# "defaults" and "overrides". A parent settings object provides the defaults,
|
214
|
+
# and a child can selectively override certain values.
|
215
|
+
#
|
216
|
+
# To set the parent for a settings object, pass it as the argument to the
|
217
|
+
# Settings constructor. When a field in a settings object is queried, it
|
218
|
+
# looks up the value as follows:
|
219
|
+
#
|
220
|
+
# * If a field value is explicitly set in the settings object, that value
|
221
|
+
# is returned.
|
222
|
+
# * If the field is not set in the settings object, but the settings object
|
223
|
+
# has a parent, the parent is queried. If that parent also does not have
|
224
|
+
# a value for the field, it may query its parent in turn, and so forth.
|
225
|
+
# * If we encounter a root settings with no parent, and still no value is
|
226
|
+
# set for the field, the default is returned.
|
227
|
+
#
|
228
|
+
# Example:
|
229
|
+
#
|
230
|
+
# class MySettings < Toys::Settings
|
231
|
+
# settings_attr :str, default: "default"
|
232
|
+
# end
|
233
|
+
#
|
234
|
+
# root_settings = MySettings.new
|
235
|
+
# child_settings = MySettings.new(root_settings)
|
236
|
+
# child_settings.str # => "default"
|
237
|
+
# root_settings.str = "value_from_root"
|
238
|
+
# child_settings.str # => "value_from_root"
|
239
|
+
# child_settings.str = "value_from_child"
|
240
|
+
# child_settings.str # => "value_from_child"
|
241
|
+
# child_settings.str_unset!
|
242
|
+
# child_settings.str # => "value_from_root"
|
243
|
+
# root_settings.str_unset!
|
244
|
+
# child_settings.str # => "default"
|
245
|
+
#
|
246
|
+
# Parents are honored through groups as well. For example:
|
247
|
+
#
|
248
|
+
# class MySettings < Toys::Settings
|
249
|
+
# settings_group :flags do
|
250
|
+
# settings_attr :verbose, default: false
|
251
|
+
# settings_attr :force, default: false
|
252
|
+
# end
|
253
|
+
# end
|
254
|
+
#
|
255
|
+
# root_settings = MySettings.new
|
256
|
+
# child_settings = MySettings.new(root_settings)
|
257
|
+
# child_settings.flags.verbose # => false
|
258
|
+
# root_settings.flags.verbose = true
|
259
|
+
# child_settings.flags.verbose # => true
|
260
|
+
#
|
261
|
+
# Usually, a settings and its parent (and its parent, and so forth) should
|
262
|
+
# have the same class. This guarantees that they define the same fields with
|
263
|
+
# the same type specifications. However, this is not required. If a parent
|
264
|
+
# does not define a particular field, it is treated as if that field is
|
265
|
+
# unset, and lookup proceeds to its parent. To illustrate:
|
266
|
+
#
|
267
|
+
# class Settings1 < Toys::Settings
|
268
|
+
# settings_attr :str, default: "default"
|
269
|
+
# end
|
270
|
+
# class Settings2 < Toys::Settings
|
271
|
+
# end
|
272
|
+
#
|
273
|
+
# root_settings = Settings1.new
|
274
|
+
# child_settings = Settings2.new(root_settings) # does not have str
|
275
|
+
# grandchild_settings = Settings1.new(child_settings)
|
276
|
+
#
|
277
|
+
# grandchild_settings.str # => "default"
|
278
|
+
# root_settings.str = "value_from_root"
|
279
|
+
# grandchild_settings.str # => "value_from_root"
|
280
|
+
#
|
281
|
+
# Type specifications are enforced when falling back to parent values. If a
|
282
|
+
# parent provides a value that is not allowed, it is treated as if the field
|
283
|
+
# is unset, and lookup proceeds to its parent.
|
284
|
+
#
|
285
|
+
# class Settings1 < Toys::Settings
|
286
|
+
# settings_attr :str, default: "default" # type spec is String
|
287
|
+
# end
|
288
|
+
# class Settings2 < Toys::Settings
|
289
|
+
# settings_attr :str, default: 0 # type spec is Integer
|
290
|
+
# end
|
291
|
+
#
|
292
|
+
# root_settings = Settings1.new
|
293
|
+
# child_settings = Settings2.new(root_settings)
|
294
|
+
# grandchild_settings = Settings1.new(child_settings)
|
295
|
+
#
|
296
|
+
# grandchild_settings.str # => "default"
|
297
|
+
# child_settings.str = 123 # does not match grandchild's type
|
298
|
+
# root_settings.str = "value_from_root"
|
299
|
+
# grandchild_settings.str # => "value_from_root"
|
300
|
+
#
|
301
|
+
class Settings
|
302
|
+
# A special value indicating a type check failure.
|
303
|
+
ILLEGAL_VALUE = ::Object.new.freeze
|
304
|
+
|
305
|
+
# A special type specification indicating infer from the default value.
|
306
|
+
DEFAULT_TYPE = ::Object.new.freeze
|
307
|
+
|
308
|
+
##
|
309
|
+
# Error raised when a value does not match the type constraint.
|
310
|
+
#
|
311
|
+
class FieldError < ::StandardError
|
312
|
+
##
|
313
|
+
# The value that did not match
|
314
|
+
# @return [Object]
|
315
|
+
#
|
316
|
+
attr_reader :value
|
317
|
+
|
318
|
+
##
|
319
|
+
# The settings class that rejected the value
|
320
|
+
# @return [Class]
|
321
|
+
#
|
322
|
+
attr_reader :settings_class
|
323
|
+
|
324
|
+
##
|
325
|
+
# The field that rejected the value
|
326
|
+
# @return [Symbol]
|
327
|
+
#
|
328
|
+
attr_reader :field_name
|
329
|
+
|
330
|
+
##
|
331
|
+
# A description of the type constraint, or nil if the field didn't exist.
|
332
|
+
# @return [String, nil]
|
333
|
+
#
|
334
|
+
attr_reader :type_description
|
335
|
+
|
336
|
+
##
|
337
|
+
# @private
|
338
|
+
#
|
339
|
+
def initialize(value, settings_class, field_name, type_description)
|
340
|
+
@value = value
|
341
|
+
@settings_class = settings_class
|
342
|
+
@field_name = field_name
|
343
|
+
@type_description = type_description
|
344
|
+
message = "unable to set #{settings_class}##{field_name}"
|
345
|
+
message =
|
346
|
+
if type_description
|
347
|
+
"#{message}: value #{value.inspect} does not match type #{type_description}"
|
348
|
+
else
|
349
|
+
"#{message}: field does not exist"
|
350
|
+
end
|
351
|
+
super(message)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
##
|
356
|
+
# A type object that checks values.
|
357
|
+
#
|
358
|
+
# A Type includes a description string and a testing function. The testing
|
359
|
+
# function takes a proposed value and returns either the value itself if it
|
360
|
+
# is valid, a converted value if the value can be converted to a valid
|
361
|
+
# value, or {ILLEGAL_VALUE} if the type check failed.
|
362
|
+
#
|
363
|
+
class Type
|
364
|
+
##
|
365
|
+
# Create a new Type.
|
366
|
+
#
|
367
|
+
# @param description [String] Name of the type.
|
368
|
+
# @param block [Proc] A testing function.
|
369
|
+
#
|
370
|
+
def initialize(description, &block)
|
371
|
+
@description = description.freeze
|
372
|
+
@tester = block
|
373
|
+
end
|
374
|
+
|
375
|
+
##
|
376
|
+
# The name of the type.
|
377
|
+
# @return [String]
|
378
|
+
#
|
379
|
+
attr_reader :description
|
380
|
+
|
381
|
+
##
|
382
|
+
# Test a value, possibly converting to a legal value.
|
383
|
+
#
|
384
|
+
# @param val [Object] The value to be tested.
|
385
|
+
# @return [Object] The validated value, the value converted to a legal
|
386
|
+
# value, or {ILLEGAL_VALUE} if the type check is unsuccessful.
|
387
|
+
#
|
388
|
+
def call(val)
|
389
|
+
@tester.call(val)
|
390
|
+
end
|
391
|
+
|
392
|
+
class << self
|
393
|
+
##
|
394
|
+
# Create and return a Type given a type specification. See the
|
395
|
+
# {Settings} class documentation for valid type specifications.
|
396
|
+
#
|
397
|
+
# @param type_spec [Object]
|
398
|
+
# @return [Type]
|
399
|
+
# @raise [ArgumentError] if the type specification is invalid.
|
400
|
+
#
|
401
|
+
def for_type_spec(type_spec)
|
402
|
+
case type_spec
|
403
|
+
when Type
|
404
|
+
type_spec
|
405
|
+
when ::Module
|
406
|
+
for_module(type_spec)
|
407
|
+
when ::Range
|
408
|
+
for_range(type_spec)
|
409
|
+
when ::Regexp
|
410
|
+
for_regexp(type_spec)
|
411
|
+
when ::Array
|
412
|
+
for_union(type_spec)
|
413
|
+
when ::Proc
|
414
|
+
new("(opaque proc)", &type_spec)
|
415
|
+
when nil, true, false, ::String, ::Symbol, ::Numeric
|
416
|
+
for_scalar(type_spec)
|
417
|
+
else
|
418
|
+
raise ::ArgumentError, "Illegal type spec: #{type_spec.inspect}"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
##
|
423
|
+
# Create and return a Type given a default value. See the {Settings}
|
424
|
+
# class documentation for the rules.
|
425
|
+
#
|
426
|
+
# @param value [Object]
|
427
|
+
# @return [Type]
|
428
|
+
#
|
429
|
+
def for_default_value(value)
|
430
|
+
case value
|
431
|
+
when nil
|
432
|
+
for_module(::Object)
|
433
|
+
when true, false
|
434
|
+
for_union([true, false])
|
435
|
+
else
|
436
|
+
for_module(value.class)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
private
|
441
|
+
|
442
|
+
def for_module(klass)
|
443
|
+
new(klass.to_s) do |val|
|
444
|
+
convert(val, klass)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def for_range(range)
|
449
|
+
range_class = (range.begin || range.end).class
|
450
|
+
new("(#{range})") do |val|
|
451
|
+
converted = convert(val, range_class)
|
452
|
+
range.member?(converted) ? converted : ILLEGAL_VALUE
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def for_regexp(regexp)
|
457
|
+
regexp_str = regexp.source.gsub("/", "\\/")
|
458
|
+
new("/#{regexp_str}/") do |val|
|
459
|
+
str = val.to_s
|
460
|
+
regexp.match(str) ? str : ILLEGAL_VALUE
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def for_union(array)
|
465
|
+
types = array.map { |elem| for_type_spec(elem) }
|
466
|
+
descriptions = types.map(&:description).join(", ")
|
467
|
+
new("[#{descriptions}]") do |val|
|
468
|
+
result = ILLEGAL_VALUE
|
469
|
+
types.each do |type|
|
470
|
+
converted = type.call(val)
|
471
|
+
if converted == val
|
472
|
+
result = val
|
473
|
+
break
|
474
|
+
elsif result == ILLEGAL_VALUE
|
475
|
+
result = converted
|
476
|
+
end
|
477
|
+
end
|
478
|
+
result
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
def for_scalar(value)
|
483
|
+
new(value.inspect) do |val|
|
484
|
+
val == value ? val : ILLEGAL_VALUE
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def convert(val, klass)
|
489
|
+
return val if val.is_a?(klass)
|
490
|
+
begin
|
491
|
+
CONVERTERS[klass].call(val)
|
492
|
+
rescue ::StandardError
|
493
|
+
ILLEGAL_VALUE
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
date_converter = proc do |val|
|
499
|
+
case val
|
500
|
+
when ::String
|
501
|
+
::Date.parse(val)
|
502
|
+
when ::Numeric
|
503
|
+
::Time.at(val, in: "UTC").to_date
|
504
|
+
else
|
505
|
+
ILLEGAL_VALUE
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
datetime_converter = proc do |val|
|
510
|
+
case val
|
511
|
+
when ::String
|
512
|
+
::DateTime.parse(val)
|
513
|
+
when ::Numeric
|
514
|
+
::Time.at(val, in: "UTC").to_datetime
|
515
|
+
else
|
516
|
+
ILLEGAL_VALUE
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
float_converter = proc do |val|
|
521
|
+
case val
|
522
|
+
when ::String
|
523
|
+
val.to_f
|
524
|
+
when ::Numeric
|
525
|
+
converted = val.to_f
|
526
|
+
converted == val ? converted : ILLEGAL_VALUE
|
527
|
+
else
|
528
|
+
ILLEGAL_VALUE
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
integer_converter = proc do |val|
|
533
|
+
case val
|
534
|
+
when ::String
|
535
|
+
val.to_i
|
536
|
+
when ::Numeric
|
537
|
+
converted = val.to_i
|
538
|
+
converted == val ? converted : ILLEGAL_VALUE
|
539
|
+
else
|
540
|
+
ILLEGAL_VALUE
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
regexp_converter = proc do |val|
|
545
|
+
val.is_a?(::String) ? ::Regexp.new(val) : ILLEGAL_VALUE
|
546
|
+
end
|
547
|
+
|
548
|
+
symbol_converter = proc do |val|
|
549
|
+
val.is_a?(::String) ? val.to_sym : ILLEGAL_VALUE
|
550
|
+
end
|
551
|
+
|
552
|
+
time_converter = proc do |val|
|
553
|
+
case val
|
554
|
+
when ::String
|
555
|
+
::DateTime.parse(val).to_time
|
556
|
+
when ::Numeric
|
557
|
+
::Time.at(val, in: "UTC")
|
558
|
+
else
|
559
|
+
ILLEGAL_VALUE
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
##
|
564
|
+
# @private
|
565
|
+
#
|
566
|
+
CONVERTERS = {
|
567
|
+
::Date => date_converter,
|
568
|
+
::DateTime => datetime_converter,
|
569
|
+
::Float => float_converter,
|
570
|
+
::Integer => integer_converter,
|
571
|
+
::Regexp => regexp_converter,
|
572
|
+
::Symbol => symbol_converter,
|
573
|
+
::Time => time_converter,
|
574
|
+
}.freeze
|
575
|
+
end
|
576
|
+
|
577
|
+
##
|
578
|
+
# Create a settings instance.
|
579
|
+
#
|
580
|
+
# @param parent [Settings,nil] Optional parent settings.
|
581
|
+
#
|
582
|
+
def initialize(parent: nil)
|
583
|
+
unless parent.nil? || parent.is_a?(Settings)
|
584
|
+
raise ::ArgumentError, "parent must be a Settings object, if given"
|
585
|
+
end
|
586
|
+
@parent = parent
|
587
|
+
@fields = self.class.fields
|
588
|
+
@mutex = ::Mutex.new
|
589
|
+
@values = {}
|
590
|
+
end
|
591
|
+
|
592
|
+
##
|
593
|
+
# Load the given hash of data into this settings object.
|
594
|
+
#
|
595
|
+
# @param data [Hash] The data as a hash of key-value pairs.
|
596
|
+
# @param raise_on_failure [boolean] If `true`, raises an exception on the
|
597
|
+
# first error encountered. If `false`, continues parsing and returns an
|
598
|
+
# array of the errors raised.
|
599
|
+
# @return [Array<FieldError>] An array of errors.
|
600
|
+
#
|
601
|
+
def load_data!(data, raise_on_failure: false)
|
602
|
+
errors = []
|
603
|
+
data.each do |name, value|
|
604
|
+
name = name.to_sym
|
605
|
+
field = @fields[name]
|
606
|
+
begin
|
607
|
+
raise FieldError.new(value, self.class, name, nil) unless field
|
608
|
+
if field.group?
|
609
|
+
raise FieldError.new(value, self.class, name, "Hash") unless value.is_a?(::Hash)
|
610
|
+
get!(field).load_data!(value)
|
611
|
+
else
|
612
|
+
set!(field, value)
|
613
|
+
end
|
614
|
+
rescue FieldError => e
|
615
|
+
raise e if raise_on_failure
|
616
|
+
errors << e
|
617
|
+
end
|
618
|
+
end
|
619
|
+
errors
|
620
|
+
end
|
621
|
+
|
622
|
+
##
|
623
|
+
# Parse the given YAML string and load the data into this settings object.
|
624
|
+
#
|
625
|
+
# @param str [String] The YAML-formatted string.
|
626
|
+
# @param raise_on_failure [boolean] If `true`, raises an exception on the
|
627
|
+
# first error encountered. If `false`, continues parsing and returns an
|
628
|
+
# array of the errors raised.
|
629
|
+
# @return [Array<FieldError>] An array of errors.
|
630
|
+
#
|
631
|
+
def load_yaml!(str, raise_on_failure: false)
|
632
|
+
require "psych"
|
633
|
+
load_data!(::Psych.load(str), raise_on_failure: raise_on_failure)
|
634
|
+
end
|
635
|
+
|
636
|
+
##
|
637
|
+
# Parse the given YAML file and load the data into this settings object.
|
638
|
+
#
|
639
|
+
# @param filename [String] The path to the YAML-formatted file.
|
640
|
+
# @param raise_on_failure [boolean] If `true`, raises an exception on the
|
641
|
+
# first error encountered. If `false`, continues parsing and returns an
|
642
|
+
# array of the errors raised.
|
643
|
+
# @return [Array<FieldError>] An array of errors.
|
644
|
+
#
|
645
|
+
def load_yaml_file!(filename, raise_on_failure: false)
|
646
|
+
load_yaml!(File.read(filename), raise_on_failure: raise_on_failure)
|
647
|
+
end
|
648
|
+
|
649
|
+
##
|
650
|
+
# Parse the given JSON string and load the data into this settings object.
|
651
|
+
#
|
652
|
+
# @param str [String] The JSON-formatted string.
|
653
|
+
# @param raise_on_failure [boolean] If `true`, raises an exception on the
|
654
|
+
# first error encountered. If `false`, continues parsing and returns an
|
655
|
+
# array of the errors raised.
|
656
|
+
# @return [Array<FieldError>] An array of errors.
|
657
|
+
#
|
658
|
+
def load_json!(str, raise_on_failure: false, **json_opts)
|
659
|
+
require "json"
|
660
|
+
load_data!(::JSON.parse(str, json_opts), raise_on_failure: raise_on_failure)
|
661
|
+
end
|
662
|
+
|
663
|
+
##
|
664
|
+
# Parse the given JSON file and load the data into this settings object.
|
665
|
+
#
|
666
|
+
# @param filename [String] The path to the JSON-formatted file.
|
667
|
+
# @param raise_on_failure [boolean] If `true`, raises an exception on the
|
668
|
+
# first error encountered. If `false`, continues parsing and returns an
|
669
|
+
# array of the errors raised.
|
670
|
+
# @return [Array<FieldError>] An array of errors.
|
671
|
+
#
|
672
|
+
def load_json_file!(filename, raise_on_failure: false, **json_opts)
|
673
|
+
load_json!(File.read(filename), raise_on_failure: raise_on_failure, **json_opts)
|
674
|
+
end
|
675
|
+
|
676
|
+
##
|
677
|
+
# @private
|
678
|
+
#
|
679
|
+
# Internal get field value, with fallback to parents.
|
680
|
+
#
|
681
|
+
def get!(field)
|
682
|
+
result = @mutex.synchronize do
|
683
|
+
@values.fetch(field.name, ILLEGAL_VALUE)
|
684
|
+
end
|
685
|
+
if result != ILLEGAL_VALUE && field.container != self.class
|
686
|
+
result = field.type.call(result)
|
687
|
+
end
|
688
|
+
return result unless result == ILLEGAL_VALUE
|
689
|
+
|
690
|
+
if field.group?
|
691
|
+
inherited = @parent.get!(field) if @parent
|
692
|
+
if @fields[field.name]&.group?
|
693
|
+
@mutex.synchronize do
|
694
|
+
@values[field.name] ||= field.group_class.new(parent: inherited)
|
695
|
+
end
|
696
|
+
else
|
697
|
+
inherited
|
698
|
+
end
|
699
|
+
else
|
700
|
+
@parent ? @parent.get!(field) : field.default
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
##
|
705
|
+
# @private
|
706
|
+
#
|
707
|
+
# Internal set field value, with validation.
|
708
|
+
#
|
709
|
+
def set!(field, value)
|
710
|
+
converted = field.validate(value)
|
711
|
+
@mutex.synchronize do
|
712
|
+
@values[field.name] = converted
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
##
|
717
|
+
# @private
|
718
|
+
#
|
719
|
+
# Internal determine if the field is set locally.
|
720
|
+
#
|
721
|
+
def set?(field)
|
722
|
+
@mutex.synchronize do
|
723
|
+
@values.key?(field.name)
|
724
|
+
end
|
725
|
+
end
|
726
|
+
|
727
|
+
##
|
728
|
+
# @private
|
729
|
+
#
|
730
|
+
# Internal unset field value.
|
731
|
+
#
|
732
|
+
def unset!(field)
|
733
|
+
@mutex.synchronize do
|
734
|
+
@values.delete(field.name)
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
##
|
739
|
+
# @private
|
740
|
+
#
|
741
|
+
SETTINGS_TYPE = Type.new("(settings object)") do |val|
|
742
|
+
val.nil? || val.is_a?(Settings) ? val : ILLEGAL_VALUE
|
743
|
+
end
|
744
|
+
|
745
|
+
##
|
746
|
+
# @private
|
747
|
+
#
|
748
|
+
class Field
|
749
|
+
def initialize(container, name, type_spec, default_or_group_class)
|
750
|
+
@container = container
|
751
|
+
@name = name
|
752
|
+
if type_spec == SETTINGS_TYPE
|
753
|
+
@default = nil
|
754
|
+
@group_class = default_or_group_class
|
755
|
+
@type = type_spec
|
756
|
+
else
|
757
|
+
@group_class = nil
|
758
|
+
if type_spec == DEFAULT_TYPE
|
759
|
+
@default = default_or_group_class
|
760
|
+
@type = Type.for_default_value(@default)
|
761
|
+
else
|
762
|
+
@type = Type.for_type_spec(type_spec)
|
763
|
+
@default = validate(default_or_group_class)
|
764
|
+
end
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
attr_reader :container
|
769
|
+
attr_reader :name
|
770
|
+
attr_reader :type
|
771
|
+
attr_reader :default
|
772
|
+
attr_reader :group_class
|
773
|
+
|
774
|
+
def group?
|
775
|
+
!@group_class.nil?
|
776
|
+
end
|
777
|
+
|
778
|
+
def validate(value)
|
779
|
+
validated_value = @type.call(value)
|
780
|
+
if validated_value == ILLEGAL_VALUE
|
781
|
+
raise FieldError.new(value, container, name, @type.description)
|
782
|
+
end
|
783
|
+
validated_value
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
class << self
|
788
|
+
##
|
789
|
+
# Add an attribute field.
|
790
|
+
#
|
791
|
+
# @param name [Symbol,String] The name of the attribute.
|
792
|
+
# @param default [Object] Optional. The final default value if the field
|
793
|
+
# is not set in this settings object or any of its ancestors. If not
|
794
|
+
# provided, `nil` is used.
|
795
|
+
# @param type [Object] Optional. The type specification. If not provided,
|
796
|
+
# one is inferred from the default value.
|
797
|
+
#
|
798
|
+
def settings_attr(name, default: nil, type: DEFAULT_TYPE, &block)
|
799
|
+
name = interpret_name(name)
|
800
|
+
type = block if type == DEFAULT_TYPE && block
|
801
|
+
@fields[name] = field = Field.new(self, name, type, default)
|
802
|
+
create_getter(field)
|
803
|
+
create_setter(field)
|
804
|
+
create_set_detect(field)
|
805
|
+
create_unsetter(field)
|
806
|
+
self
|
807
|
+
end
|
808
|
+
|
809
|
+
##
|
810
|
+
# Add a group field.
|
811
|
+
#
|
812
|
+
# Specify the group's structure by passing either a class (which must
|
813
|
+
# subclass Settings) or a block (which will be called on the group's
|
814
|
+
# class.)
|
815
|
+
#
|
816
|
+
# @param name [Symbol, String] The name of the group.
|
817
|
+
# @param klass [Class] Optional. The class of the group (which must
|
818
|
+
# subclass Settings). If not present, an anonymous subclass will be
|
819
|
+
# created, and you must provide a block to configure it.
|
820
|
+
#
|
821
|
+
def settings_group(name, klass = nil, &block)
|
822
|
+
name = interpret_name(name)
|
823
|
+
if klass.nil? == block.nil?
|
824
|
+
raise ::ArgumentError, "A group field requires a class or a block, but not both."
|
825
|
+
end
|
826
|
+
unless klass
|
827
|
+
klass = ::Class.new(Settings)
|
828
|
+
klass_name = to_class_name(name.to_s)
|
829
|
+
const_set(klass_name, klass)
|
830
|
+
klass.class_eval(&block)
|
831
|
+
end
|
832
|
+
@fields[name] = field = Field.new(self, name, SETTINGS_TYPE, klass)
|
833
|
+
create_getter(field)
|
834
|
+
self
|
835
|
+
end
|
836
|
+
|
837
|
+
##
|
838
|
+
# @private
|
839
|
+
#
|
840
|
+
# Returns the fields hash. This is shared between the settings class and
|
841
|
+
# all its instances.
|
842
|
+
#
|
843
|
+
def fields
|
844
|
+
@fields ||= {}
|
845
|
+
end
|
846
|
+
|
847
|
+
##
|
848
|
+
# @private
|
849
|
+
#
|
850
|
+
# When this base class is inherited, if its enclosing module is also a
|
851
|
+
# Settings, add the new class as a group in the enclosing class.
|
852
|
+
#
|
853
|
+
def inherited(subclass)
|
854
|
+
super
|
855
|
+
subclass.fields
|
856
|
+
path = subclass.name.to_s.split("::")
|
857
|
+
namespace = path[0...-1].reduce(::Object) { |mod, name| mod.const_get(name.to_sym) }
|
858
|
+
if namespace.ancestors.include?(Settings)
|
859
|
+
name = to_field_name(path.last)
|
860
|
+
namespace.settings_group(name, subclass)
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
private
|
865
|
+
|
866
|
+
def to_field_name(str)
|
867
|
+
str = str.to_s.sub(/^_/, "").sub(/_$/, "").gsub(/_+/, "_")
|
868
|
+
while str.sub!(/([^_])([A-Z])/, "\\1_\\2") do end
|
869
|
+
str.downcase
|
870
|
+
end
|
871
|
+
|
872
|
+
def to_class_name(str)
|
873
|
+
str.split("_").map(&:capitalize).join
|
874
|
+
end
|
875
|
+
|
876
|
+
def interpret_name(name)
|
877
|
+
name = name.to_s
|
878
|
+
if name !~ /^[a-zA-Z]\w*$/ || name == "method_missing"
|
879
|
+
raise ::ArgumentError, "Illegal settings field name: #{name}"
|
880
|
+
end
|
881
|
+
existing = public_instance_methods(false)
|
882
|
+
if existing.include?(name.to_sym) || existing.include?("#{name}=".to_sym) ||
|
883
|
+
existing.include?("#{name}_set?".to_sym) || existing.include?("#{name}_unset!".to_sym)
|
884
|
+
raise ::ArgumentError, "Settings field already exists: #{name}"
|
885
|
+
end
|
886
|
+
name.to_sym
|
887
|
+
end
|
888
|
+
|
889
|
+
def create_getter(field)
|
890
|
+
define_method(field.name) do
|
891
|
+
get!(field)
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
def create_setter(field)
|
896
|
+
define_method("#{field.name}=") do |val|
|
897
|
+
set!(field, val)
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
def create_set_detect(field)
|
902
|
+
define_method("#{field.name}_set?") do
|
903
|
+
set?(field)
|
904
|
+
end
|
905
|
+
end
|
906
|
+
|
907
|
+
def create_unsetter(field)
|
908
|
+
define_method("#{field.name}_unset!") do
|
909
|
+
unset!(field)
|
910
|
+
end
|
911
|
+
end
|
912
|
+
end
|
913
|
+
end
|
914
|
+
end
|