toys-core 0.11.5 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  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/exec.rb +12 -14
  32. data/lib/toys/standard_mixins/git_cache.rb +48 -0
  33. data/lib/toys/standard_mixins/xdg.rb +56 -0
  34. data/lib/toys/template.rb +2 -2
  35. data/lib/toys/{tool.rb → tool_definition.rb} +100 -41
  36. data/lib/toys/utils/exec.rb +4 -5
  37. data/lib/toys/utils/gems.rb +8 -7
  38. data/lib/toys/utils/git_cache.rb +184 -0
  39. data/lib/toys/utils/help_text.rb +90 -34
  40. data/lib/toys/utils/terminal.rb +1 -1
  41. data/lib/toys/utils/xdg.rb +293 -0
  42. metadata +14 -7
@@ -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