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.
- checksums.yaml +7 -0
- data/lib/configuratrix/errors.rb +101 -0
- data/lib/configuratrix/initialize.rb +5 -0
- data/lib/configuratrix/language-util.rb +65 -0
- data/lib/configuratrix/language.rb +639 -0
- data/lib/configuratrix/schema-util.rb +314 -0
- data/lib/configuratrix/schema.rb +970 -0
- data/lib/configuratrix/sources/command_line.rb +585 -0
- data/lib/configuratrix/sources/environment.rb +60 -0
- data/lib/configuratrix/sources/util.rb +303 -0
- data/lib/configuratrix/sources/yaml_file.rb +506 -0
- data/lib/configuratrix/sources.rb +64 -0
- data/lib/configuratrix/types.rb +121 -0
- data/lib/configuratrix.rb +12 -0
- metadata +59 -0
|
@@ -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
|