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.
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