configuratrix 0.0.1.alpha

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.
@@ -0,0 +1,970 @@
1
+ require_relative 'language'
2
+ require_relative 'schema-util'
3
+
4
+ module Configuratrix
5
+
6
+ # A config is a collection of named values.
7
+ class Config
8
+ # This is where you want to look (language.rb) for the statements used to
9
+ # define a config.
10
+ extend Internal::ConfigSchemaLanguage
11
+
12
+
13
+ #
14
+ # Instance Methods:
15
+ # These methods are available on the actual config object resulting from
16
+ # parsing script args etc.
17
+ #
18
+
19
+ # Plumbing such as #schema.
20
+ include Internal::NestingSchema::InstanceMethods
21
+
22
+ # This is where the methods for getting the fields' values live. We don't
23
+ # know what they are yet!
24
+ #
25
+ # To find the methods defined by a given config
26
+ # schema, use explicit scoping (e.g. #schema::FieldAttributes or
27
+ # ConfigClass::FieldAttributes). Otherwise, you'll always get
28
+ # Config::FieldAttributes itself which should generally be empty.
29
+ #
30
+ # The module isn't even included into the class until a schema puts something
31
+ # into it via #mutable_instance_field_module! And even then it will most
32
+ # likely be an independent copy of this module that belongs to a subclass.
33
+ module FieldAttributes
34
+ # Add a method to objects of this schema to access the value of field by
35
+ # its attribute_name. Call only on modules received via
36
+ # #mutable_instance_field_module!
37
+ def self.register(field)
38
+ attr_name = field.attribute_name
39
+ define_method attr_name do
40
+ self[attr_name].value
41
+ end
42
+ end
43
+ end
44
+
45
+ # Get the Field object for the given name.
46
+ def [](field_name)
47
+ unless @field_map.include? field_name
48
+ raise Err::ValuelessKey, <<~END
49
+ #{self} has no field `#{field_name}` (or, field added to schema after
50
+ config object initialized)
51
+ END
52
+ end
53
+ @field_map.fetch field_name
54
+ end
55
+
56
+ private
57
+
58
+ def initialize
59
+ schema.validate
60
+ schema_init
61
+ config_init
62
+ end
63
+
64
+ module Innards
65
+ # Do only the initialization that does not require the actual sources to be
66
+ # ready.
67
+ def schema_init
68
+ schema_field_init
69
+ schema_source_init
70
+ end
71
+ def schema_field_init
72
+ @field_map = (schema
73
+ .fields_schema
74
+ .map { [ _1.attribute_name, _1.new(self) ] }
75
+ .to_h)
76
+ end
77
+ def schema_source_init
78
+ @sources = schema.sources_schema.map(&:new)
79
+ end
80
+
81
+ # Parse and prepare a usable config.
82
+ def config_init
83
+ config_source_init
84
+ config_field_init
85
+ end
86
+ def config_source_init
87
+ if source_objects.empty?
88
+ raise Err::PathologicalSchema, <<~END
89
+ #{schema.name} has no sources to load from: use source :dummy if
90
+ desired
91
+ END
92
+ end
93
+ @source_map = Internal::SourceUtil.parse_all(
94
+ source_objects, within: schema)
95
+ end
96
+ def config_field_init
97
+ # It seems like you could use `from: @source_map.values` since they are
98
+ # the ready sources, but order is significant.
99
+ available_sources = source_objects .select { _1.ready?(source_objects) }
100
+ @field_map.values.each do |field|
101
+ field.send :take_value_from, available_sources
102
+ end
103
+ end
104
+
105
+ def source_map; @source_map; end
106
+ def source_objects; @sources; end
107
+ def subconfig_path; []; end
108
+ end
109
+ include Innards
110
+
111
+ public
112
+
113
+
114
+ #
115
+ # Class Methods:
116
+ # These methods modify the class itself (the schema of the config) and
117
+ # affect how args etc. will be parsed into a configuration.
118
+ #
119
+
120
+ extend Internal::NestingSchema
121
+
122
+ # Get the names of all the fields in this config schema. Inherited fields
123
+ # are included if `true` is passed.
124
+ def self.fields(...); fields_schema(...).map(&:attribute_name); end
125
+
126
+ # Get the list of all Field classes relevant to this config, including
127
+ # inherited fields if `inherit`.
128
+ def self.fields_schema(inherit=true)
129
+ ultimate = inherit ? Config : self
130
+ Internal::NestingSchemaUtil.roll_up(
131
+ :local_fields,
132
+ self..ultimate,
133
+ ) .uniq &:basename
134
+ end
135
+
136
+ def self.field_defined?(name,inherit=true)
137
+ fields(inherit).include?(name)
138
+ end
139
+
140
+ # Add a new field into the config schema.
141
+ def self.field_add(field)
142
+ name = field.attribute_name
143
+
144
+ if field_defined? name
145
+ raise Err::NameCollision, <<~END
146
+ #{name} already names a field in #{self.name}
147
+ END
148
+ end
149
+
150
+ local_fields << field
151
+ field
152
+ end
153
+
154
+ # Clone the named field and re-add it, shadowing the ancestor's version.
155
+ def self.field_lift(attribute_name)
156
+ unless field_defined? attribute_name
157
+ raise Err::ValuelessKey, <<~END
158
+ `#{attribute_name}` names no field in #{name}
159
+ END
160
+ end
161
+ lifted = field_get(attribute_name).clone
162
+ lifted.alias! attribute_name, under: self
163
+
164
+ lifted.send :detatch_config
165
+ local_fields << lifted
166
+ lifted
167
+ end
168
+
169
+ # Get the schema (Field subclass) for the given field name.
170
+ def self.field_get(name, inherit=true)
171
+ unless field_defined? name
172
+ raise Err::ValuelessKey, <<~END
173
+ #{self.name} has no field `#{name}`
174
+ END
175
+ end
176
+ fields_schema(inherit) .find { _1.attribute_name == name }
177
+ end
178
+
179
+ # Get the schema for the given field path, entering subconfigs as necessary.
180
+ def self.[](*field_path)
181
+ case field_path
182
+ in [ field_name ]
183
+ field_get field_name
184
+ in [ subconfig_name, *sub_path ]
185
+ field_get(subconfig_name)::Config[*sub_path]
186
+ end
187
+ end
188
+
189
+ # Enumerate all the fields not only in this config, but in all subconfigs as
190
+ # well. Each enumeration includes the field schema, the schema of the config
191
+ # containing the field, and the string path to that subconfig (or empty
192
+ # string for root config).
193
+ def self.fields_universe
194
+ this_config = self
195
+ Enumerator.new do |output|
196
+ fields_schema.each do |field_schema|
197
+ output.yield(field_schema, this_config)
198
+
199
+ # Recur on any subconfig.
200
+ if field_schema.const_defined?(:Config, NO_INHERIT)
201
+ subenum = field_schema::Config.fields_universe
202
+
203
+ subenum.each do |field_schema,subconfig_schema|
204
+ output.yield field_schema, subconfig_schema
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ # Get the names of all the sources in this config schema. Inherited and
212
+ # default sources are included if `true` is passed.
213
+ def self.sources(...); sources_schema(...).map(&:attribute_name); end
214
+
215
+ # Get the list of all Source classes relevant to this config. If `inherit`
216
+ # is true, then inherited sources as well as those of
217
+ # Configuratrix::Mutable::DefaultSources are included.
218
+ def self.sources_schema(inherit=true)
219
+ if inherit
220
+ ultimate = Config
221
+ base_case = -> {
222
+ mod = Mutable::DefaultSources
223
+ mod .constants .map { mod.const_get _1 }
224
+ }
225
+ else
226
+ ultimate = self
227
+ base_case = nil
228
+ end
229
+ Internal::NestingSchemaUtil.roll_up(
230
+ :local_sources,
231
+ self..ultimate,
232
+ base_case: base_case
233
+ ).uniq &:basename
234
+ end
235
+
236
+ def self.source_defined?(name,inherit=true)
237
+ sources(inherit).include?(name)
238
+ end
239
+
240
+ # Add a new source into the config schema.
241
+ def self.source_add(source)
242
+ name = source.attribute_name
243
+
244
+ if source_defined? name
245
+ raise Err::NameCollision, <<~END
246
+ #{name} already names a source in #{self.name}
247
+ END
248
+ end
249
+
250
+ local_sources << source
251
+ source
252
+ end
253
+
254
+ # Clone the named source and re-add it, shadowing the ancestor's version.
255
+ def self.source_lift(name)
256
+ unless source_defined? name
257
+ raise Err::ValuelessKey, <<~END
258
+ `#{name}` names no source in #{self.name}
259
+ END
260
+ end
261
+ lifted = source_get(name).clone
262
+ lifted.alias! name, under: self
263
+
264
+ local_sources << lifted
265
+ lifted
266
+ end
267
+
268
+ # Get the schema (Source subclass) for the given source name.
269
+ def self.source_get(name)
270
+ unless source_defined? name
271
+ raise Err::ValuelessKey, <<~END
272
+ #{self.name} has no source `#{name}`
273
+ END
274
+ end
275
+ sources_schema .find { _1.attribute_name == name }
276
+ end
277
+
278
+ # Get the names of all the types in this config schema. Inherited and
279
+ # default types are included if `true` is passed.
280
+ def self.types(...); types_schema(...).map(&:attribute_name); end
281
+
282
+ # Get the list of all Type classes relevant to this config. If `inherit` is
283
+ # true, then inherited types as well as those of
284
+ # Configuratrix::Mutable::DefaultTypes are included.
285
+ def self.types_schema(inherit=true)
286
+ if inherit
287
+ ultimate = Config
288
+ base_case = -> {
289
+ mod = Mutable::DefaultTypes
290
+ mod .constants .map { mod.const_get _1 }
291
+ }
292
+ else
293
+ ultimate = self
294
+ base_case = nil
295
+ end
296
+ Internal::NestingSchemaUtil.roll_up(
297
+ :local_types,
298
+ self..ultimate,
299
+ base_case: base_case
300
+ ).uniq &:basename
301
+ end
302
+
303
+ def self.type_defined?(name,inherit=true); types(inherit).include?(name); end
304
+
305
+ # Add a new type into the config schema.
306
+ def self.type_add(type)
307
+ name = type.attribute_name
308
+
309
+ if type_defined? name
310
+ raise Err::NameCollision, <<~END
311
+ #{name} already names a value type in #{self.name}
312
+ END
313
+ end
314
+
315
+ local_types << type
316
+ type
317
+ end
318
+
319
+ # Clone the named type and re-add it, shadowing the ancestor's version.
320
+ def self.type_lift(name)
321
+ unless type_defined? name
322
+ raise Err::ValuelessKey, <<~END
323
+ `#{name}` names no type in #{self.name}
324
+ END
325
+ end
326
+ lifted = type_get(name).clone
327
+ lifted.alias! name, under: self
328
+
329
+ local_types << lifted
330
+ lifted
331
+ end
332
+
333
+ # Get the schema (Type subclass) for the given value type name.
334
+ def self.type_get(name)
335
+ unless type_defined? name
336
+ raise Err::ValuelessKey, <<~END
337
+ #{self.name} has no value type `#{name}`
338
+ END
339
+ end
340
+ types_schema .find { _1.attribute_name == name }
341
+ end
342
+
343
+ # Where is this config located among the config system?
344
+ def self.subconfig_path
345
+ segments = []
346
+ cursor = self
347
+ while cursor.const_defined?(:SUBCONFIG_NAME, NO_INHERIT)
348
+ segments.prepend cursor::SUBCONFIG_NAME
349
+ cursor = cursor.superclass
350
+ end
351
+ segments
352
+ end
353
+ def self.subconfig_tree
354
+ local_fields.select {
355
+ _1.const_defined?(:Config, NO_INHERIT)
356
+ }.map {
357
+ [ _1.attribute_name, _1::Config.subconfig_tree ]
358
+ }.to_h
359
+ end
360
+
361
+ # Get an unloaded config object. Throws on field access (because unloaded)
362
+ # while still differentiating nonexistent fields.
363
+ #
364
+ # Because an unloaded config has no object state (as underscored by the use
365
+ # of #allocate over #new in this method), it is even safe to set a
366
+ # new_unloaded value as a subconfig default before the subconfig's schema is
367
+ # finalized.
368
+ def self.new_unloaded
369
+ obj = allocate
370
+ obj.singleton_class.prepend Internal::UnloadedConfig
371
+ obj
372
+ end
373
+
374
+ # Get an unloaded config that will JIT initialize the appropraite field
375
+ # objects, but will not load any data into them. This results in a config
376
+ # that will give defaults or throw on reference to values of fields that have
377
+ # no defaults.
378
+ #
379
+ # Because the config's Field objects are initialized JIT, it is safe to set a
380
+ # new_defaulty value as a subconfig default before the subconfig's schema is
381
+ # finalized.
382
+ def self.new_defaulty
383
+ obj = allocate
384
+ obj.singleton_class.prepend Internal::DefaultyConfig
385
+ obj
386
+ end
387
+
388
+ # Check for any structural incompatibilities in the schema that can only be
389
+ # checked when we're sure the schema classes are done being edited (which is
390
+ # to say we can't check them until just before building an actual config
391
+ # object).
392
+ def self.validate
393
+ fields_universe.each do |field_schema|
394
+ sources_schema.each { _1.approve_field field_schema }
395
+ end
396
+ end
397
+
398
+ #
399
+ # Private Class Methods
400
+ #
401
+
402
+ def self.local_fields
403
+ @fields ||= Internal::HookedArray.new do |field_schema|
404
+ if field_schema.config_attached?
405
+ raise "BUG: adding field still attached to another config"
406
+ end
407
+ field_schema.const_set :CONTAINING_CONFIG, self
408
+ mutable_instance_field_module!.register field_schema
409
+ end
410
+ end
411
+ private_class_method :local_fields
412
+
413
+ def self.local_sources
414
+ @sources ||= Internal::HookedArray.new do |source_schema|
415
+ # enforce reserved words
416
+ if [ :any ].include? source_schema.attribute_name
417
+ raise Err::WordIsReserved, <<~END
418
+ no source can be named :#{source_schema.attribute_name}
419
+ END
420
+ end
421
+ end
422
+ end
423
+ private_class_method :local_sources
424
+
425
+ def self.local_types; @types ||= []; end
426
+ private_class_method :local_types
427
+
428
+ # Get the module for this particular class that keeps the instance methods
429
+ # for accessing field values. If this class doesn't have one, copy it from
430
+ # Config::FieldAttriutes so self::FieldAttributes#register exists. to this
431
+ # class.
432
+ def self.mutable_instance_field_module!
433
+ unless constants(false).include? :FieldAttributes
434
+ const_set :FieldAttributes, Config::FieldAttributes.clone
435
+ end
436
+ mod = self::FieldAttributes
437
+
438
+ unless ancestors.include? mod
439
+ prepend mod
440
+ end
441
+
442
+ mod
443
+ end
444
+ private_class_method :mutable_instance_field_module!
445
+
446
+ end
447
+
448
+
449
+ # This module is prepended onto the singleton class of each subconfig to
450
+ # reflect the semantic differences between the root config and subconfigs.
451
+ module Subconfig
452
+ # If subconfigs inherited fields, then there would be no separation of the
453
+ # namespaces. But, to avoid breaking the interface, discard an inherit
454
+ # parameter anyway.
455
+ def field_defined?(name,inherit=nil); super(name,false); end
456
+ def field_get(name,inherit=nil); super(name,false); end
457
+ def fields(inherit=nil); super(false); end
458
+ def fields_schema(inherit=nil); super(false); end
459
+
460
+ # Sources, on the other hand, have no business in subconfigs. The same
461
+ # source objects built from the sources_schema of the root config will simply
462
+ # change namespaces when encountering inputs for a subconfig. So, any
463
+ # modifications to sources in subconfigs are meaningless. Note that fields
464
+ # in subconfigs are still free to use #loads_from.
465
+ def self.no_sourcing!(method,schema)
466
+ raise Err::PathologicalSchema, <<~END
467
+ #{method}: source definitions in subconfigs (#{schema}) have no effect
468
+ END
469
+ end
470
+ def source_add(...); Subconfig.no_sourcing!(__method__,self); end
471
+ def source_lift(...); Subconfig.no_sourcing!(__method__,self); end
472
+
473
+ private
474
+
475
+ def new_source(...); Subconfig.no_sourcing!(__method__,self); end
476
+ def source(...); Subconfig.no_sourcing!(__method__,self); end
477
+
478
+ # This module is prepended to the instance methods of Subconfigs.
479
+ module InstanceMethods
480
+ def initialize(superconfig)
481
+ @superconfig = superconfig
482
+ super()
483
+ end
484
+
485
+ def schema_source_init; end
486
+ def config_source_init; end
487
+
488
+ def source_objects; @superconfig.source_objects; end
489
+ def subconfig_path
490
+ [ *@superconfig.subconfig_path, self::SUBCONFIG_NAME ]
491
+ end
492
+ end
493
+ end
494
+
495
+
496
+ # A field is one named value.
497
+ class Field
498
+ TOGGLE_TYPE = :FieldToggles
499
+
500
+ # This is where you want to look (language.rb) for the statements used to
501
+ # define a field.
502
+ extend Internal::FieldSchemaLanguage
503
+
504
+
505
+ #
506
+ # Instance Methods:
507
+ # These methods are available on the actual field object resulting from
508
+ # parsing script args etc.
509
+ #
510
+
511
+ include Internal::NestingSchema::InstanceMethods
512
+
513
+ def initialize(containing_config=nil)
514
+ @containing_config = containing_config if containing_config
515
+ end
516
+
517
+ def value
518
+ # instance_variable_defined? is used over @value.nil? to distinguish
519
+ # between an actual nil value for the field and Ruby just giving back nil
520
+ # for any undefined @variable.
521
+ if instance_variable_defined? :@value
522
+ @value
523
+ else
524
+ default
525
+ end
526
+ end
527
+
528
+ def default; schema.default_value; end
529
+
530
+ def containing_config; @containing_config; end
531
+
532
+ private
533
+
534
+ # Take up the value from the first acceptable source that contains the
535
+ # appropriate key. Sources must already be parsed.
536
+ def take_value_from(sources)
537
+ case schema.value_type.take_value_for(self, from: sources)
538
+ in [ value ]
539
+ @value = value
540
+ in []
541
+ if schema.required?
542
+ key = [ *schema.subconfig_path, name ]
543
+ raise Err::RequirementUnmet, <<~END
544
+ #{key.join '.'} is required
545
+ END
546
+ end
547
+ end
548
+ end
549
+
550
+ public
551
+
552
+
553
+ #
554
+ # Class Methods:
555
+ # These methods modify the class itself (the schema of the field) and
556
+ # affect how args etc. will be parsed into this field.
557
+ #
558
+
559
+ extend Internal::NestingSchema
560
+
561
+ # Whether this field must have a value for a config to be valid.
562
+ def self.required?; list_of_default.empty?; end
563
+
564
+ # Get the value for this field when none is given explicitly. To allow any
565
+ # value as default without ambiguity, an error is raised if this is called on
566
+ # a required field.
567
+ def self.default_value
568
+ case list_of_default
569
+ in []
570
+ raise Err::NoSuchValue, <<~END
571
+ required field `#{self}` has no default
572
+ END
573
+ in [value]
574
+ value
575
+ end
576
+ end
577
+
578
+ # Get an Array that either solely contains the field's default value, or
579
+ # contains nothing if the field is required.
580
+ def self.list_of_default; @list_of_default || []; end
581
+
582
+ # It is sometimes useful to discern between a field that's required by
583
+ # default and a field that was explicitly marked required.
584
+ def self.default_explicit?; @list_of_default != nil; end
585
+
586
+ # Arity specifies how many values a field must get in order to be complete.
587
+ #
588
+ # The default Unspec arity means that the most recent value given to a field
589
+ # is its value. This mirrors common command line behavior. An explicit
590
+ # arity specifies a number or a range of numbers of values acceptable to the
591
+ # field. See also Arity.
592
+ #
593
+ # Arity is separate from a field being required or not. Fields with arity
594
+ # other than nil or 1 require their value_type to have a non-nil #plurality.
595
+ def self.arity; @arity || Internal::Arity::Unspec; end
596
+
597
+ def self.arity_explicit?; @arity != nil; end
598
+
599
+ # Get the list of attribute names of sources this field is willing to load
600
+ # from (e.g. :command_line). nil indicates any source is acceptable.
601
+ def self.acceptable_sources; @acceptable_sources.dup; end
602
+
603
+ # Get the Type subclass that corresponds to values of this Field type.
604
+ def self.value_type; @value_type || Mutable::DefaultType; end
605
+
606
+ # Whether the field has an explicit value_type.
607
+ def self.typed?; @value_type != nil; end
608
+
609
+ def self.config_attached?; const_defined?(:CONTAINING_CONFIG, NO_INHERIT); end
610
+
611
+ def self.subconfig_path
612
+ if config_attached?
613
+ self::CONTAINING_CONFIG.subconfig_path
614
+ else
615
+ []
616
+ end
617
+ end
618
+
619
+ def self.to_s; [ *subconfig_path, attribute_name ] .join('.'); end
620
+ def self.inspect; "#{name}(#{value_type.name})"; end
621
+
622
+ #
623
+ # Private Class Methods
624
+ #
625
+
626
+ # Directly set @list_of_default: emtpy list means required field.
627
+ def self.raw_set_default(list_of_value)
628
+ @list_of_default = list_of_value
629
+ assert_consistent_default!
630
+ @list_of_default
631
+ end
632
+ private_class_method :raw_set_default
633
+
634
+ # See #arity.
635
+ def self.set_arity(value)
636
+ @arity = case value
637
+ in nil
638
+ Internal::Arity::Unspec
639
+ in Range
640
+ Internal::Arity.from_range value
641
+ in Integer
642
+ Internal::Arity.from_int value
643
+ end
644
+ # the default value of a field must satisfy the field's arity.
645
+ assert_consistent_default!
646
+ @arity
647
+ end
648
+ private_class_method :set_arity
649
+
650
+ # Specify a list of :source_names specifying the only sources this field may
651
+ # be loaded from.
652
+ def self.set_acceptable_sources(list); @acceptable_sources = list; end
653
+ private_class_method :set_acceptable_sources
654
+
655
+ # The Type subclass that dictates the form of values appropriate for this
656
+ # field.
657
+ def self.set_value_type(typeclass)
658
+ @value_type = typeclass
659
+ assert_consistent_default!
660
+ @value_type
661
+ end
662
+ private_class_method :set_value_type
663
+
664
+ # Get the highest priority type that recognizes value, or nil.
665
+ def self.infer_value_type(value)
666
+ (self::CONTAINING_CONFIG
667
+ .types_schema
668
+ .select { _1.respond_to? :assert_claim }
669
+ .sort_by(&:inference_priority)
670
+ .find { _1.assert_claim value }
671
+ )
672
+ end
673
+ private_class_method :infer_value_type
674
+
675
+ # The default value must always agree with the value_type and the arity of
676
+ # the field.
677
+ def self.assert_consistent_default!
678
+ # required fields have no default to check
679
+ return if required?
680
+
681
+ case problem_with_value? default_value
682
+ in nil
683
+ in :type
684
+ raise Err::ValueUnrecognized, <<~END
685
+ default value #{default_value.inspect} is not a #{value_type}: set a
686
+ compatible `type` or clear the default with `required`
687
+ END
688
+ in :arity, count
689
+ raise Err::ArityMismatch, <<~END
690
+ field accepting #{arity} value(s) cannot have a default with
691
+ #{count} values: set a compatible `count` or clear the default with
692
+ `required`
693
+ END
694
+ in :must_wrap
695
+ raise Err::ValueUnrecognized, <<~END
696
+ plural field demands default value be
697
+ #{value_type.plurality.description}, not `#{default_value}`
698
+ END
699
+ in :must_unwrap
700
+ raise Err::ValueUnrecognized, <<~END
701
+ non-plural field demands default value be singular, not
702
+ #{value_type.plurality.description}
703
+ END
704
+ end
705
+ end
706
+ private_class_method :assert_consistent_default!
707
+
708
+ def self.problem_with_value?(value)
709
+ type = value_type
710
+ plurality = type.plurality
711
+
712
+ if type.discerning?
713
+ unless type === value
714
+ return :type
715
+ end
716
+ end
717
+
718
+ unless plurality.nil?
719
+ count = plurality.atoms_of(value).count
720
+ unless arity.satisfied_by? count
721
+ return [:arity, count]
722
+ end
723
+ if arity.demands_plurality? and !plurality.can_be?.(value)
724
+ return :must_wrap
725
+ end
726
+ if !arity.demands_plurality? and plurality.must_be?.(value)
727
+ return :must_unwrap
728
+ end
729
+ end
730
+
731
+ nil
732
+ end
733
+ private_class_method :problem_with_value?
734
+
735
+ def self.detatch_config
736
+ remove_const :CONTAINING_CONFIG
737
+ end
738
+ private_class_method :detatch_config
739
+ end
740
+
741
+ # A source loads data and parses out keys and values for Fields to select and
742
+ # encapsulate.
743
+ class Source
744
+ TOGGLE_TYPE = :SourceToggles
745
+
746
+ # This is where you want to look (language.rb) for the statements used to
747
+ # define a source.
748
+ extend Internal::SourceSchemaLanguage
749
+
750
+
751
+ #
752
+ # Instance Methods:
753
+ # These methods are available on the actual source object resulting from
754
+ # parsing script args etc.
755
+ #
756
+
757
+ include Internal::NestingSchema::InstanceMethods
758
+
759
+ # Throw an error indicating a request for a value cannot be fulfilled.
760
+ def undefined!(key=nil)
761
+ suffix = unless key.nil?
762
+ " for #{key}"
763
+ else
764
+ ""
765
+ end
766
+ raise Err::ValuelessKey, <<~END
767
+ #{schema.name} cannot return undefined value#{suffix}
768
+ END
769
+ end
770
+
771
+ # These are the methods meant for overriding in subclasses. See
772
+ # SourceSchemaLanguage for the details of their behavior and use.
773
+ module BaseBehaviors
774
+ def ready?(other_sources); true; end
775
+
776
+ def cross_configure(other_sources); end
777
+
778
+ def parse(field_schemas); @value_map = Hash.new; end
779
+
780
+ def key?(key)
781
+ get key
782
+ true
783
+ rescue KeyError
784
+ false
785
+ end
786
+
787
+ # Must raise if the key is not present: nil is a valid explicit value.
788
+ def get(key)
789
+ [ @value_map, *key ] .reduce { _1 .fetch _2 }
790
+ end
791
+ end
792
+ include BaseBehaviors
793
+
794
+
795
+ #
796
+ # Class Methods:
797
+ #
798
+
799
+ extend Internal::NestingSchema
800
+
801
+ # Individual kinds of sources may have quirks that make them unsafe or
802
+ # incompatible with certain schemas. This function must raise an error under
803
+ # Err::BadSchema if the given field cannot be reconciled while this source is
804
+ # enabled.
805
+ def self.approve_field(field_schema); end
806
+
807
+ end
808
+
809
+ # A Type marshals individual values from source inputs into Ruby values. A
810
+ # type may also be able to recognize values of its own kind, allowing for field
811
+ # type inference from a provided default value.
812
+ class Type
813
+ # This is where you want to look (language.rb) for the statements used to
814
+ # define a type.
815
+ extend Internal::TypeSchemaLanguage
816
+
817
+ # Since types do not have per-config state, they have no instance methods and
818
+ # it is not necessary to instantiate Type objects.
819
+ private_class_method :new
820
+
821
+ extend Internal::NestingSchema
822
+
823
+ # To allow user-defined types to preempt recognition of common values like
824
+ # true or false when necessary, Field classes are sorted by this value (low
825
+ # to high) before finding the first one that recognizes a given default
826
+ # value. Builtins all have priority=0, so any negative value will preempt
827
+ # them.
828
+ def self.inference_priority; @priority || 0; end
829
+
830
+ # Helper function to raise the appropriate exception when a value can't be
831
+ # parsed as the Type. The caller of the parsing function is expected to
832
+ # craft the exception message since it already knows both the Type and the
833
+ # value as well.
834
+ def self.nope!(str=nil); raise Err::MarshalUnacceptable.new(str); end
835
+
836
+ # A discerning type can tell if an unmarshalled value would be an appropriate
837
+ # value of the type. Undiscerning types are asking for trouble.
838
+ def self.discerning?; respond_to?(:recognize_atom?); end
839
+
840
+ # Whether a value (singular or plural) falls into the type.
841
+ def self.===(value)
842
+ raise NotImplementedError unless respond_to? :recognize_atom?
843
+
844
+ must_be_atom = (plurality.nil? or
845
+ !plurality.can_be?.(value))
846
+ if must_be_atom
847
+ recognize_atom? value
848
+ else
849
+ plurality.deconstruct.(value).all? { recognize_atom? _1 }
850
+ end
851
+ end
852
+
853
+ def self.take_value_for(field, from:)
854
+ sources = if field.schema.acceptable_sources.nil?
855
+ from
856
+ else
857
+ from.select {
858
+ field.schema.acceptable_sources.include? _1.name
859
+ }
860
+ end
861
+
862
+ if sources.empty?
863
+ raise Err::PathologicalSchema, <<~END
864
+ field #{field.schema} has no viable sources, accepts:
865
+ [#{field.schema.acceptable_sources.map(&:name).join(', ')}]
866
+ available: [#{from.map(&:name).join(', ')}]
867
+ END
868
+ end
869
+
870
+ key = [ *field.schema.subconfig_path, field.schema.attribute_name ]
871
+ sources.each do |source|
872
+ next unless source.key? key
873
+ return [ source.get(key) ]
874
+ end
875
+
876
+ []
877
+ end
878
+
879
+ class << self
880
+ # The contexts in which a type can be extracted depends on which of the
881
+ # following methods are actually defined on a Type class. See
882
+ # TypeSchemaLanguage (language.rb) for how to set those up and more details
883
+ # about their usage.
884
+
885
+ # def unmarshal(serialized)
886
+
887
+ # def affirmative(default_value)
888
+
889
+ # def negatory(default_value)
890
+
891
+ # def recognize_atom?(singular_candidate)
892
+
893
+ def plurality; Plurality::AS_ARRAY; end
894
+
895
+ # def assert_claim(value)
896
+ end
897
+
898
+ # A Plurality object dictates how a type can be applied to a field that may
899
+ # take multiple values. See Field#arity for more info on multi-value fields
900
+ # and when a plural type may be required. Types are plural by default by
901
+ # allowing multiple values to be represented as an Array.
902
+ Plurality = Struct.new(
903
+ # proc that prepares an individual value to be combineable
904
+ :wrap_atom,
905
+ # Proc that produces the combination of a plural value in progress and a
906
+ # newly-wrapped atom.
907
+ :combine,
908
+ # Proc that returns whether a given value can safely be treated as plural.
909
+ :can_be?,
910
+ # Proc that returns whether a given value is unambiguously plural.
911
+ :must_be?,
912
+ # Proc that returns an enumeration of all the subvalues of a plural value.
913
+ :deconstruct,
914
+ # A human friendly description of what it means for a value to be plural,
915
+ # like "in an array".
916
+ :description,
917
+ )
918
+ class Plurality
919
+ # If a plural value can't be deconstructed into atoms, then it must be an
920
+ # atom itself.
921
+ def atoms_of(value)
922
+ return deconstruct.(value) if can_be?.(value)
923
+ [value].each
924
+ end
925
+
926
+ class << self
927
+ # Define Plurality::NAME and give it a useful #inspect. (A struct full
928
+ # of procs just has an extremely verbose but useless #inspect.)
929
+ def constant(name)
930
+ -> (**struct_pairs) {
931
+ raise unless struct_pairs.keys.sort == members.sort
932
+ obj = new(*(members.map { struct_pairs.fetch _1 } ))
933
+ obj.define_singleton_method(:inspect) { "Plurality::#{name}" }
934
+ obj.freeze
935
+ const_set name, obj
936
+ }
937
+ end
938
+ private :constant
939
+ end
940
+
941
+ constant(:AS_ARRAY)[
942
+ wrap_atom: -> { [_1] },
943
+ combine: -> { _1 + _2 },
944
+ can_be?: -> { Array === _1 },
945
+ must_be?: -> { Array === _1 },
946
+ deconstruct: -> { _1.each },
947
+ description: "in an array",
948
+ ]
949
+
950
+ constant(:AS_SUM)[
951
+ wrap_atom: -> { _1 },
952
+ combine: -> { _1 + _2 },
953
+ can_be?: -> { _1 .respond_to? :+ },
954
+ must_be?: -> { false },
955
+ deconstruct: -> { [ _1 ] .each },
956
+ description: nil,
957
+ ]
958
+
959
+ constant(:AS_PRODUCT)[
960
+ wrap_atom: -> { _1 },
961
+ combine: -> { _1 * _2 },
962
+ can_be?: -> { _1 .respond_to? :* },
963
+ must_be?: -> { false },
964
+ deconstruct: -> { [ _1 ] .each },
965
+ description: nil,
966
+ ]
967
+ end
968
+ end
969
+
970
+ end