toys-core 0.11.3 → 0.12.1

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -2
  3. data/README.md +1 -1
  4. data/lib/toys-core.rb +4 -1
  5. data/lib/toys/acceptor.rb +3 -3
  6. data/lib/toys/arg_parser.rb +6 -7
  7. data/lib/toys/cli.rb +44 -14
  8. data/lib/toys/compat.rb +19 -22
  9. data/lib/toys/completion.rb +3 -1
  10. data/lib/toys/context.rb +2 -2
  11. data/lib/toys/core.rb +1 -1
  12. data/lib/toys/dsl/base.rb +85 -0
  13. data/lib/toys/dsl/flag.rb +3 -3
  14. data/lib/toys/dsl/flag_group.rb +7 -7
  15. data/lib/toys/dsl/internal.rb +206 -0
  16. data/lib/toys/dsl/positional_arg.rb +3 -3
  17. data/lib/toys/dsl/tool.rb +174 -216
  18. data/lib/toys/errors.rb +1 -0
  19. data/lib/toys/flag.rb +15 -18
  20. data/lib/toys/flag_group.rb +5 -4
  21. data/lib/toys/input_file.rb +4 -4
  22. data/lib/toys/loader.rb +189 -50
  23. data/lib/toys/middleware.rb +1 -1
  24. data/lib/toys/mixin.rb +2 -2
  25. data/lib/toys/positional_arg.rb +3 -3
  26. data/lib/toys/settings.rb +900 -0
  27. data/lib/toys/source_info.rb +121 -18
  28. data/lib/toys/standard_middleware/apply_config.rb +5 -4
  29. data/lib/toys/standard_middleware/set_default_descriptions.rb +18 -18
  30. data/lib/toys/standard_middleware/show_help.rb +17 -5
  31. data/lib/toys/standard_mixins/bundler.rb +7 -2
  32. data/lib/toys/standard_mixins/exec.rb +22 -15
  33. data/lib/toys/standard_mixins/git_cache.rb +48 -0
  34. data/lib/toys/standard_mixins/xdg.rb +56 -0
  35. data/lib/toys/template.rb +2 -2
  36. data/lib/toys/{tool.rb → tool_definition.rb} +100 -41
  37. data/lib/toys/utils/exec.rb +12 -10
  38. data/lib/toys/utils/gems.rb +48 -14
  39. data/lib/toys/utils/git_cache.rb +184 -0
  40. data/lib/toys/utils/help_text.rb +90 -34
  41. data/lib/toys/utils/terminal.rb +1 -1
  42. data/lib/toys/utils/xdg.rb +293 -0
  43. metadata +15 -8
@@ -37,7 +37,7 @@ module Toys
37
37
  # This basic implementation does nothing and simply yields to the next
38
38
  # middleware.
39
39
  #
40
- # @param tool [Toys::Tool] The tool definition to modify.
40
+ # @param tool [Toys::ToolDefinition] The tool definition to modify.
41
41
  # @param loader [Toys::Loader] The loader that loaded this tool.
42
42
  # @return [void]
43
43
  #
data/lib/toys/mixin.rb CHANGED
@@ -9,7 +9,7 @@ module Toys
9
9
  # class, so it has access to the same methods that can be called by the tool,
10
10
  # such as {Toys::Context#get}.
11
11
  #
12
- # ## Usage
12
+ # ### Usage
13
13
  #
14
14
  # To create a mixin, define a module, and include this module. Then define
15
15
  # the methods you want to be available.
@@ -34,7 +34,7 @@ module Toys
34
34
  # methods specific to the mixin. Define the inclusion block by calling
35
35
  # {Toys::Mixin::ModuleMethods#on_include}.
36
36
  #
37
- # ## Example
37
+ # ### Example
38
38
  #
39
39
  # This is an example that implements a simple counter. Whenever the counter
40
40
  # is incremented, a log message is emitted. The tool can also retrieve the
@@ -37,11 +37,11 @@ module Toys
37
37
  # @param display_name [String] A name to use for display (in help text and
38
38
  # error reports). Defaults to the key in upper case.
39
39
  # @param desc [String,Array<String>,Toys::WrappableString] Short
40
- # description for the flag. See {Toys::DSL::Tool#desc} for a
40
+ # description for the flag. See {Toys::ToolDefintion#desc} for a
41
41
  # description of the allowed formats. Defaults to the empty string.
42
42
  # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
43
- # Long description for the flag. See {Toys::DSL::Tool#long_desc} for
44
- # a description of the allowed formats. (But note that this param
43
+ # Long description for the flag. See {Toys::ToolDefintion#long_desc}
44
+ # for a description of the allowed formats. (But note that this param
45
45
  # takes an Array of description lines, rather than a series of
46
46
  # arguments.) Defaults to the empty array.
47
47
  # @return [Toys::PositionalArg]
@@ -0,0 +1,900 @@
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
+ ##
303
+ # Error raised when a value does not match the type constraint.
304
+ #
305
+ class FieldError < ::StandardError
306
+ # @private
307
+ def initialize(value, settings_class, field_name, type_description)
308
+ @value = value
309
+ @settings_class = settings_class
310
+ @field_name = field_name
311
+ @type_description = type_description
312
+ message = "unable to set #{settings_class}##{field_name}"
313
+ message =
314
+ if type_description
315
+ "#{message}: value #{value.inspect} does not match type #{type_description}"
316
+ else
317
+ "#{message}: field does not exist"
318
+ end
319
+ super(message)
320
+ end
321
+
322
+ ##
323
+ # The value that did not match
324
+ # @return [Object]
325
+ #
326
+ attr_reader :value
327
+
328
+ ##
329
+ # The settings class that rejected the value
330
+ # @return [Class]
331
+ #
332
+ attr_reader :settings_class
333
+
334
+ ##
335
+ # The field that rejected the value
336
+ # @return [Symbol]
337
+ #
338
+ attr_reader :field_name
339
+
340
+ ##
341
+ # A description of the type constraint, or nil if the field didn't exist.
342
+ # @return [String, nil]
343
+ #
344
+ attr_reader :type_description
345
+ end
346
+
347
+ # A special value indicating a type check failure.
348
+ ILLEGAL_VALUE = ::Object.new.freeze
349
+
350
+ # A special type specification indicating infer from the default value.
351
+ DEFAULT_TYPE = ::Object.new.freeze
352
+
353
+ ##
354
+ # A type object that checks values.
355
+ #
356
+ # A Type includes a description string and a testing function. The testing
357
+ # function takes a proposed value and returns either the value itself if it
358
+ # is valid, a converted value if the value can be converted to a valid
359
+ # value, or {ILLEGAL_VALUE} if the type check failed.
360
+ #
361
+ class Type
362
+ ##
363
+ # Create a new Type.
364
+ #
365
+ # @param description [String] Name of the type.
366
+ # @param block [Proc] A testing function.
367
+ #
368
+ def initialize(description, &block)
369
+ @description = description.freeze
370
+ @tester = block
371
+ end
372
+
373
+ ##
374
+ # The name of the type.
375
+ # @return [String]
376
+ #
377
+ attr_reader :description
378
+
379
+ ##
380
+ # Test a value, possibly converting to a legal value.
381
+ #
382
+ # @param val [Object] The value to be tested.
383
+ # @return [Object] The validated value, the value converted to a legal
384
+ # value, or {ILLEGAL_VALUE} if the type check is unsuccessful.
385
+ #
386
+ def call(val)
387
+ @tester.call(val)
388
+ end
389
+
390
+ class << self
391
+ ##
392
+ # Create and return a Type given a type specification. See the
393
+ # {Settings} class documentation for valid type specifications.
394
+ #
395
+ # @param type_spec [Object]
396
+ # @return [Type]
397
+ # @raise [ArgumentError] if the type specification is invalid.
398
+ #
399
+ def for_type_spec(type_spec)
400
+ case type_spec
401
+ when Type
402
+ type_spec
403
+ when ::Module
404
+ for_module(type_spec)
405
+ when ::Range
406
+ for_range(type_spec)
407
+ when ::Regexp
408
+ for_regexp(type_spec)
409
+ when ::Array
410
+ for_union(type_spec)
411
+ when ::Proc
412
+ new("(opaque proc)", &type_spec)
413
+ when nil, true, false, ::String, ::Symbol, ::Numeric
414
+ for_scalar(type_spec)
415
+ else
416
+ raise ::ArgumentError, "Illegal type spec: #{type_spec.inspect}"
417
+ end
418
+ end
419
+
420
+ ##
421
+ # Create and return a Type given a default value. See the {Settings}
422
+ # class documentation for the rules.
423
+ #
424
+ # @param value [Object]
425
+ # @return [Type]
426
+ #
427
+ def for_default_value(value)
428
+ case value
429
+ when nil
430
+ for_module(::Object)
431
+ when true, false
432
+ for_union([true, false])
433
+ else
434
+ for_module(value.class)
435
+ end
436
+ end
437
+
438
+ private
439
+
440
+ def for_module(klass)
441
+ new(klass.to_s) do |val|
442
+ convert(val, klass)
443
+ end
444
+ end
445
+
446
+ def for_range(range)
447
+ range_class = (range.begin || range.end).class
448
+ new("(#{range})") do |val|
449
+ converted = convert(val, range_class)
450
+ range.member?(converted) ? converted : ILLEGAL_VALUE
451
+ end
452
+ end
453
+
454
+ def for_regexp(regexp)
455
+ regexp_str = regexp.source.gsub("/", "\\/")
456
+ new("/#{regexp_str}/") do |val|
457
+ str = val.to_s
458
+ regexp.match(str) ? str : ILLEGAL_VALUE
459
+ end
460
+ end
461
+
462
+ def for_union(array)
463
+ types = array.map { |elem| for_type_spec(elem) }
464
+ descriptions = types.map(&:description).join(", ")
465
+ new("[#{descriptions}]") do |val|
466
+ result = ILLEGAL_VALUE
467
+ types.each do |type|
468
+ converted = type.call(val)
469
+ if converted == val
470
+ result = val
471
+ break
472
+ elsif result == ILLEGAL_VALUE
473
+ result = converted
474
+ end
475
+ end
476
+ result
477
+ end
478
+ end
479
+
480
+ def for_scalar(value)
481
+ new(value.inspect) do |val|
482
+ val == value ? val : ILLEGAL_VALUE
483
+ end
484
+ end
485
+
486
+ def convert(val, klass)
487
+ return val if val.is_a?(klass)
488
+ begin
489
+ CONVERTERS[klass].call(val)
490
+ rescue ::StandardError
491
+ ILLEGAL_VALUE
492
+ end
493
+ end
494
+ end
495
+
496
+ date_converter = proc do |val|
497
+ case val
498
+ when ::String
499
+ ::Date.parse(val)
500
+ when ::Numeric
501
+ ::Time.at(val, in: "UTC").to_date
502
+ else
503
+ ILLEGAL_VALUE
504
+ end
505
+ end
506
+
507
+ datetime_converter = proc do |val|
508
+ case val
509
+ when ::String
510
+ ::DateTime.parse(val)
511
+ when ::Numeric
512
+ ::Time.at(val, in: "UTC").to_datetime
513
+ else
514
+ ILLEGAL_VALUE
515
+ end
516
+ end
517
+
518
+ float_converter = proc do |val|
519
+ case val
520
+ when ::String
521
+ val.to_f
522
+ when ::Numeric
523
+ converted = val.to_f
524
+ converted == val ? converted : ILLEGAL_VALUE
525
+ else
526
+ ILLEGAL_VALUE
527
+ end
528
+ end
529
+
530
+ integer_converter = proc do |val|
531
+ case val
532
+ when ::String
533
+ val.to_i
534
+ when ::Numeric
535
+ converted = val.to_i
536
+ converted == val ? converted : ILLEGAL_VALUE
537
+ else
538
+ ILLEGAL_VALUE
539
+ end
540
+ end
541
+
542
+ regexp_converter = proc do |val|
543
+ val.is_a?(::String) ? ::Regexp.new(val) : ILLEGAL_VALUE
544
+ end
545
+
546
+ symbol_converter = proc do |val|
547
+ val.is_a?(::String) ? val.to_sym : ILLEGAL_VALUE
548
+ end
549
+
550
+ time_converter = proc do |val|
551
+ case val
552
+ when ::String
553
+ ::DateTime.parse(val).to_time
554
+ when ::Numeric
555
+ ::Time.at(val, in: "UTC")
556
+ else
557
+ ILLEGAL_VALUE
558
+ end
559
+ end
560
+
561
+ # @private
562
+ CONVERTERS = {
563
+ ::Date => date_converter,
564
+ ::DateTime => datetime_converter,
565
+ ::Float => float_converter,
566
+ ::Integer => integer_converter,
567
+ ::Regexp => regexp_converter,
568
+ ::Symbol => symbol_converter,
569
+ ::Time => time_converter,
570
+ }.freeze
571
+ end
572
+
573
+ # @private
574
+ SETTINGS_TYPE = Type.new("(settings object)") do |val|
575
+ val.nil? || val.is_a?(Settings) ? val : ILLEGAL_VALUE
576
+ end
577
+
578
+ # @private
579
+ class Field
580
+ def initialize(container, name, type_spec, default_or_group_class)
581
+ @container = container
582
+ @name = name
583
+ if type_spec == SETTINGS_TYPE
584
+ @default = nil
585
+ @group_class = default_or_group_class
586
+ @type = type_spec
587
+ else
588
+ @group_class = nil
589
+ if type_spec == DEFAULT_TYPE
590
+ @default = default_or_group_class
591
+ @type = Type.for_default_value(@default)
592
+ else
593
+ @type = Type.for_type_spec(type_spec)
594
+ @default = validate(default_or_group_class)
595
+ end
596
+ end
597
+ end
598
+
599
+ attr_reader :container
600
+ attr_reader :name
601
+ attr_reader :type
602
+ attr_reader :default
603
+ attr_reader :group_class
604
+
605
+ def group?
606
+ !@group_class.nil?
607
+ end
608
+
609
+ def validate(value)
610
+ validated_value = @type.call(value)
611
+ if validated_value == ILLEGAL_VALUE
612
+ raise FieldError.new(value, container, name, @type.description)
613
+ end
614
+ validated_value
615
+ end
616
+ end
617
+
618
+ ##
619
+ # Create a settings instance.
620
+ #
621
+ # @param parent [Settings,nil] Optional parent settings.
622
+ #
623
+ def initialize(parent: nil)
624
+ unless parent.nil? || parent.is_a?(Settings)
625
+ raise ::ArgumentError, "parent must be a Settings object, if given"
626
+ end
627
+ @parent = parent
628
+ @fields = self.class.fields
629
+ @mutex = ::Mutex.new
630
+ @values = {}
631
+ end
632
+
633
+ ##
634
+ # Load the given hash of data into this settings object.
635
+ #
636
+ # @param data [Hash] The data as a hash of key-value pairs.
637
+ # @param raise_on_failure [boolean] If `true`, raises an exception on the
638
+ # first error encountered. If `false`, continues parsing and returns an
639
+ # array of the errors raised.
640
+ # @return [Array<FieldError>] An array of errors.
641
+ #
642
+ def load_data!(data, raise_on_failure: false)
643
+ errors = []
644
+ data.each do |name, value|
645
+ name = name.to_sym
646
+ field = @fields[name]
647
+ begin
648
+ raise FieldError.new(value, self.class, name, nil) unless field
649
+ if field.group?
650
+ raise FieldError.new(value, self.class, name, "Hash") unless value.is_a?(::Hash)
651
+ get!(field).load_data!(value)
652
+ else
653
+ set!(field, value)
654
+ end
655
+ rescue FieldError => e
656
+ raise e if raise_on_failure
657
+ errors << e
658
+ end
659
+ end
660
+ errors
661
+ end
662
+
663
+ ##
664
+ # Parse the given YAML string and load the data into this settings object.
665
+ #
666
+ # @param str [String] The YAML-formatted string.
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_yaml!(str, raise_on_failure: false)
673
+ require "psych"
674
+ load_data!(::Psych.load(str), raise_on_failure: raise_on_failure)
675
+ end
676
+
677
+ ##
678
+ # Parse the given YAML file and load the data into this settings object.
679
+ #
680
+ # @param filename [String] The path to the YAML-formatted file.
681
+ # @param raise_on_failure [boolean] If `true`, raises an exception on the
682
+ # first error encountered. If `false`, continues parsing and returns an
683
+ # array of the errors raised.
684
+ # @return [Array<FieldError>] An array of errors.
685
+ #
686
+ def load_yaml_file!(filename, raise_on_failure: false)
687
+ load_yaml!(File.read(filename), raise_on_failure: raise_on_failure)
688
+ end
689
+
690
+ ##
691
+ # Parse the given JSON string and load the data into this settings object.
692
+ #
693
+ # @param str [String] The JSON-formatted string.
694
+ # @param raise_on_failure [boolean] If `true`, raises an exception on the
695
+ # first error encountered. If `false`, continues parsing and returns an
696
+ # array of the errors raised.
697
+ # @return [Array<FieldError>] An array of errors.
698
+ #
699
+ def load_json!(str, raise_on_failure: false, **json_opts)
700
+ require "json"
701
+ load_data!(::JSON.parse(str, json_opts), raise_on_failure: raise_on_failure)
702
+ end
703
+
704
+ ##
705
+ # Parse the given JSON file and load the data into this settings object.
706
+ #
707
+ # @param filename [String] The path to the JSON-formatted file.
708
+ # @param raise_on_failure [boolean] If `true`, raises an exception on the
709
+ # first error encountered. If `false`, continues parsing and returns an
710
+ # array of the errors raised.
711
+ # @return [Array<FieldError>] An array of errors.
712
+ #
713
+ def load_json_file!(filename, raise_on_failure: false, **json_opts)
714
+ load_json!(File.read(filename), raise_on_failure: raise_on_failure, **json_opts)
715
+ end
716
+
717
+ ##
718
+ # @private
719
+ # Internal get field value, with fallback to parents.
720
+ #
721
+ def get!(field)
722
+ result = @mutex.synchronize do
723
+ @values.fetch(field.name, ILLEGAL_VALUE)
724
+ end
725
+ if result != ILLEGAL_VALUE && field.container != self.class
726
+ result = field.type.call(result)
727
+ end
728
+ return result unless result == ILLEGAL_VALUE
729
+
730
+ if field.group?
731
+ inherited = @parent.get!(field) if @parent
732
+ if @fields[field.name]&.group?
733
+ @mutex.synchronize do
734
+ @values[field.name] ||= field.group_class.new(parent: inherited)
735
+ end
736
+ else
737
+ inherited
738
+ end
739
+ else
740
+ @parent ? @parent.get!(field) : field.default
741
+ end
742
+ end
743
+
744
+ ##
745
+ # @private
746
+ # Internal set field value, with validation.
747
+ #
748
+ def set!(field, value)
749
+ converted = field.validate(value)
750
+ @mutex.synchronize do
751
+ @values[field.name] = converted
752
+ end
753
+ end
754
+
755
+ ##
756
+ # @private
757
+ # Internal determine if the field is set locally.
758
+ #
759
+ def set?(field)
760
+ @mutex.synchronize do
761
+ @values.key?(field.name)
762
+ end
763
+ end
764
+
765
+ ##
766
+ # @private
767
+ # Internal unset field value.
768
+ #
769
+ def unset!(field)
770
+ @mutex.synchronize do
771
+ @values.delete(field.name)
772
+ end
773
+ end
774
+
775
+ class << self
776
+ ##
777
+ # Add an attribute field.
778
+ #
779
+ # @param name [Symbol,String] The name of the attribute.
780
+ # @param default [Object] Optional. The final default value if the field
781
+ # is not set in this settings object or any of its ancestors. If not
782
+ # provided, `nil` is used.
783
+ # @param type [Object] Optional. The type specification. If not provided,
784
+ # one is inferred from the default value.
785
+ #
786
+ def settings_attr(name, default: nil, type: DEFAULT_TYPE, &block)
787
+ name = interpret_name(name)
788
+ type = block if type == DEFAULT_TYPE && block
789
+ @fields[name] = field = Field.new(self, name, type, default)
790
+ create_getter(field)
791
+ create_setter(field)
792
+ create_set_detect(field)
793
+ create_unsetter(field)
794
+ self
795
+ end
796
+
797
+ ##
798
+ # Add a group field.
799
+ #
800
+ # Specify the group's structure by passing either a class (which must
801
+ # subclass Settings) or a block (which will be called on the group's
802
+ # class.)
803
+ #
804
+ # @param name [Symbol, String] The name of the group.
805
+ # @param klass [Class] Optional. The class of the group (which must
806
+ # subclass Settings). If not present, an anonymous subclass will be
807
+ # created, and you must provide a block to configure it.
808
+ #
809
+ def settings_group(name, klass = nil, &block)
810
+ name = interpret_name(name)
811
+ if klass.nil? == block.nil?
812
+ raise ::ArgumentError, "A group field requires a class or a block, but not both."
813
+ end
814
+ unless klass
815
+ klass = ::Class.new(Settings)
816
+ klass_name = to_class_name(name.to_s)
817
+ const_set(klass_name, klass)
818
+ klass.class_eval(&block)
819
+ end
820
+ @fields[name] = field = Field.new(self, name, SETTINGS_TYPE, klass)
821
+ create_getter(field)
822
+ self
823
+ end
824
+
825
+ ##
826
+ # @private
827
+ # Returns the fields hash. This is shared between the settings class and
828
+ # all its instances.
829
+ #
830
+ def fields
831
+ @fields ||= {}
832
+ end
833
+
834
+ ##
835
+ # @private
836
+ # When this base class is inherited, if its enclosing module is also a
837
+ # Settings, add the new class as a group in the enclosing class.
838
+ #
839
+ def inherited(subclass)
840
+ super
841
+ subclass.fields
842
+ path = subclass.name.to_s.split("::")
843
+ namespace = path[0...-1].reduce(::Object) { |mod, name| mod.const_get(name.to_sym) }
844
+ if namespace.ancestors.include?(Settings)
845
+ name = to_field_name(path.last)
846
+ namespace.settings_group(name, subclass)
847
+ end
848
+ end
849
+
850
+ private
851
+
852
+ def to_field_name(str)
853
+ str = str.to_s.sub(/^_/, "").sub(/_$/, "").gsub(/_+/, "_")
854
+ while str.sub!(/([^_])([A-Z])/, "\\1_\\2") do end
855
+ str.downcase
856
+ end
857
+
858
+ def to_class_name(str)
859
+ str.split("_").map(&:capitalize).join
860
+ end
861
+
862
+ def interpret_name(name)
863
+ name = name.to_s
864
+ if name !~ /^[a-zA-Z]\w*$/ || name == "method_missing"
865
+ raise ::ArgumentError, "Illegal settings field name: #{name}"
866
+ end
867
+ existing = public_instance_methods(false)
868
+ if existing.include?(name.to_sym) || existing.include?("#{name}=".to_sym) ||
869
+ existing.include?("#{name}_set?".to_sym) || existing.include?("#{name}_unset!".to_sym)
870
+ raise ::ArgumentError, "Settings field already exists: #{name}"
871
+ end
872
+ name.to_sym
873
+ end
874
+
875
+ def create_getter(field)
876
+ define_method(field.name) do
877
+ get!(field)
878
+ end
879
+ end
880
+
881
+ def create_setter(field)
882
+ define_method("#{field.name}=") do |val|
883
+ set!(field, val)
884
+ end
885
+ end
886
+
887
+ def create_set_detect(field)
888
+ define_method("#{field.name}_set?") do
889
+ set?(field)
890
+ end
891
+ end
892
+
893
+ def create_unsetter(field)
894
+ define_method("#{field.name}_unset!") do
895
+ unset!(field)
896
+ end
897
+ end
898
+ end
899
+ end
900
+ end