toys-core 0.11.5 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +5 -2
  5. data/docs/guide.md +1 -1
  6. data/lib/toys/acceptor.rb +13 -4
  7. data/lib/toys/arg_parser.rb +7 -7
  8. data/lib/toys/cli.rb +170 -120
  9. data/lib/toys/compat.rb +71 -23
  10. data/lib/toys/completion.rb +18 -6
  11. data/lib/toys/context.rb +24 -15
  12. data/lib/toys/core.rb +6 -2
  13. data/lib/toys/dsl/base.rb +87 -0
  14. data/lib/toys/dsl/flag.rb +26 -20
  15. data/lib/toys/dsl/flag_group.rb +18 -14
  16. data/lib/toys/dsl/internal.rb +206 -0
  17. data/lib/toys/dsl/positional_arg.rb +26 -16
  18. data/lib/toys/dsl/tool.rb +180 -218
  19. data/lib/toys/errors.rb +64 -8
  20. data/lib/toys/flag.rb +662 -656
  21. data/lib/toys/flag_group.rb +24 -10
  22. data/lib/toys/input_file.rb +13 -7
  23. data/lib/toys/loader.rb +293 -140
  24. data/lib/toys/middleware.rb +46 -22
  25. data/lib/toys/mixin.rb +10 -8
  26. data/lib/toys/positional_arg.rb +21 -20
  27. data/lib/toys/settings.rb +914 -0
  28. data/lib/toys/source_info.rb +147 -35
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +2 -0
  30. data/lib/toys/standard_middleware/apply_config.rb +6 -4
  31. data/lib/toys/standard_middleware/handle_usage_errors.rb +1 -0
  32. data/lib/toys/standard_middleware/set_default_descriptions.rb +19 -18
  33. data/lib/toys/standard_middleware/show_help.rb +19 -5
  34. data/lib/toys/standard_middleware/show_root_version.rb +2 -0
  35. data/lib/toys/standard_mixins/bundler.rb +24 -15
  36. data/lib/toys/standard_mixins/exec.rb +43 -34
  37. data/lib/toys/standard_mixins/fileutils.rb +3 -1
  38. data/lib/toys/standard_mixins/gems.rb +21 -17
  39. data/lib/toys/standard_mixins/git_cache.rb +46 -0
  40. data/lib/toys/standard_mixins/highline.rb +8 -8
  41. data/lib/toys/standard_mixins/terminal.rb +5 -5
  42. data/lib/toys/standard_mixins/xdg.rb +56 -0
  43. data/lib/toys/template.rb +11 -9
  44. data/lib/toys/{tool.rb → tool_definition.rb} +292 -226
  45. data/lib/toys/utils/completion_engine.rb +7 -2
  46. data/lib/toys/utils/exec.rb +162 -132
  47. data/lib/toys/utils/gems.rb +85 -60
  48. data/lib/toys/utils/git_cache.rb +813 -0
  49. data/lib/toys/utils/help_text.rb +117 -37
  50. data/lib/toys/utils/terminal.rb +11 -3
  51. data/lib/toys/utils/xdg.rb +293 -0
  52. data/lib/toys/wrappable_string.rb +9 -2
  53. data/lib/toys-core.rb +18 -6
  54. 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