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,506 @@
1
+ require_relative '../schema'
2
+ require 'psych'
3
+
4
+ module Configuratrix
5
+ module Sources
6
+
7
+ class YamlFile < Source
8
+ ready? { |sources|
9
+ (@stream or
10
+ sources.any? { _1.schema.toggles(YamlFile).file_path_field }
11
+ )
12
+ }
13
+
14
+ cross_configure { |sources|
15
+ return if @stream
16
+ # Find the source that might have our path to the yaml file, as well as the
17
+ # config path to the specific field that holds the file path.
18
+ path_source = sources.find { _1.schema.toggles(YamlFile).file_path_field }
19
+ file_path_field_path = (
20
+ path_source.schema.toggles(YamlFile).file_path_field
21
+ .split('.').map(&:to_sym)
22
+ )
23
+ # If the source doesn't have a file path for us, we'll have to wait until
24
+ # we receive the config schema in the parse step to extract the default
25
+ # path.
26
+ @file_path = if path_source.key? file_path_field_path
27
+ path_source.get file_path_field_path
28
+ else
29
+ -> { _1[*file_path_field_path].default_value }
30
+ end
31
+ }
32
+
33
+ parse { |config_schema|
34
+ # If we didn't get the file path from an explicit value, extract the
35
+ # default from config_schema.
36
+ @file_path = @file_path.call(config_schema) if @file_path .respond_to? :call
37
+ @location = Location.new @file_path
38
+ @stream = File.open @file_path if @stream.nil?
39
+
40
+ @value_map = {}
41
+ # Let Psych handle the actual yaml parsing, we'll just follow along with
42
+ # the parser events.
43
+ begin
44
+ Psych::Parser.new(
45
+ EventHandler.new(
46
+ config_schema, @value_map, @location
47
+ )
48
+ ) .parse @stream
49
+ rescue Psych::SyntaxError
50
+ raise Err::BadSyntax, $!.to_s
51
+ rescue NoMatchingPatternError
52
+ raise Err::BadSyntax, <<~RANT_OVER
53
+ #{@location}: :#{$!} cannot appear in this part of a YamlFile
54
+ RANT_OVER
55
+ end
56
+ # Clean up.
57
+ @stream.close
58
+ @value_map
59
+ }
60
+
61
+ TOGGLES = [
62
+ # The config path to the field containing the yaml file path for YamlFile.
63
+ :file_path_field,
64
+ ]
65
+ class SourceToggles < Internal::ToggleStruct[*TOGGLES]
66
+ def file_path_field=(...); reject_unless super, matches: A::STRING; end
67
+ end
68
+
69
+ # Shell class for routing parser events to the appropriate State handlers.
70
+ class EventHandler < Psych::Handler
71
+ def initialize(config_schema, output, location)
72
+ @location = location
73
+ # Event recording and replay supports YAML aliases.
74
+ @macros = MacroMeister.new self, location
75
+ # The parse stack dictates which State the current event should be routed
76
+ # to.
77
+ @the_stack = [ State::Root.new(
78
+ config_schema,
79
+ output,
80
+ location,
81
+ @macros,
82
+ ) ]
83
+ end
84
+
85
+ private
86
+
87
+ # Psych provides many events, but we only need to hear about these.
88
+ RELEVANT_EVENTS = [ :scalar, :start_mapping, :end_mapping, :start_sequence,
89
+ :end_sequence, :alias, :start_document, :end_document ]
90
+ Event = Struct.new(:name, :data) do
91
+ # Ruby has no native deep copy mechanism, but failure to copy deeply
92
+ # results in seemingly dup'd Events actually sharing a data struct.
93
+ def dup; o = super; o.data = o.data.dup; o; end
94
+ end
95
+
96
+ # Each kind of event has its own set of data.
97
+ VectorData = Struct.new(:anchor, :tag, :implicit, :style)
98
+ ScalarData = Struct.new(:value, :anchor, :tag, :plain, :quoted, :style)
99
+ AliasData = Struct.new(:reference)
100
+ DocumentData = Struct.new(:version, :tag_directives, :implicit)
101
+ def self.no_data; []; end
102
+ def self.ignore_data(...); []; end
103
+
104
+ MAKE_EVENT_DATA = {
105
+ start_mapping: (VectorData.method :new),
106
+ end_mapping: (method :no_data),
107
+ start_sequence: (VectorData.method :new),
108
+ end_sequence: (method :no_data),
109
+ scalar: (ScalarData.method :new),
110
+ alias: (AliasData.method :new),
111
+ start_document: (DocumentData.method :new),
112
+ end_document: (method :ignore_data),
113
+ }
114
+
115
+ # Initial entry point for events handed down from Psych. Events replayed
116
+ # as part of macros don't pass through here, they go directly to #offer.
117
+ def receive(event)
118
+ # Don't allow tags anywhere to reduce potential confusion since they have
119
+ # no effect.
120
+ if event.data.respond_to? :tag and event.data.tag
121
+ raise Err::BadSyntax, <<~RANT_OVER
122
+ explicit tags are not allowed: type info comes from config schema
123
+ RANT_OVER
124
+ end
125
+ # has no effect if no macros are being recorded right now
126
+ @macros.record event
127
+ # Offer the event back to this handler, resolving any aliases and
128
+ # offering their complete macros as applicable.
129
+ @macros.offer event
130
+ end
131
+
132
+ public
133
+
134
+ # Delegate control of the parser for this event to the State at the top of
135
+ # the stack.
136
+ def offer(event)
137
+ loop do
138
+ memos = []
139
+ # Take the state at the top of the stack, give it the event, and let it
140
+ # decide what states should replace it on the stack.
141
+ @the_stack.push(
142
+ *@the_stack.pop.receive_event(event, memos))
143
+
144
+ # If the current state rejected the event, we'll give it to the new top
145
+ # of the stack.
146
+ break unless memos .include? :reject_event!
147
+ raise "stuck!" if @the_stack == @stack_checkpoint
148
+ @stack_checkpoint = @the_stack.dup
149
+ end
150
+ end
151
+
152
+ def inspect; "#<#{self.class.name}:#{object_id}>"; end
153
+
154
+ # Hook up all the Psych::Handler methods so Parser can send us events.
155
+ RELEVANT_EVENTS.each do |name|
156
+ define_method name do |*data|
157
+ event = Event.new(
158
+ name,
159
+ MAKE_EVENT_DATA[name].call(*data),
160
+ )
161
+ receive event
162
+ end
163
+ end
164
+ # Pass Psych's location updates directly to our location handler.
165
+ def event_location(...); @location.update(...); end
166
+
167
+ end
168
+
169
+ # Keep track of where the parser is in the file, including whether the
170
+ # current event is being replayed from a previous point in the file via a
171
+ # macro.
172
+ Location = Struct.new(
173
+ :path,
174
+ :start_line, :start_column, :end_line, :end_column,
175
+ :alias_of
176
+ ) do
177
+ def update(*args)
178
+ (self.start_line, self.start_column, self.end_line, self.end_column
179
+ ) = args
180
+ end
181
+ # All we ever need to actually do with the location is show it to the user.
182
+ def to_s(omit_path: nil)
183
+ path = self.path || "<YAML>" unless omit_path
184
+ str = [ path, start_line+1, start_column+1 ] .join ':'
185
+ str << " (copied from #{alias_of.to_s omit_path: true})" if alias_of
186
+ str
187
+ end
188
+ end
189
+
190
+ # Parser events are handled by a state machine, each different behavior being
191
+ # handled by a distinct State subclass.
192
+ class State
193
+ # States generally need to know where parsing is, what bit of schema
194
+ # they're parsing for, whom to tell about new macros needing recording, and
195
+ # where to hand retrieved values off to.
196
+ def initialize(location: nil, schema: nil, macros: nil, minder: nil)
197
+ @location = location if location
198
+ @schema = schema if schema
199
+ @macros = macros if macros
200
+ @value_minder = minder if minder
201
+ end
202
+
203
+ # Where it all begins. . . at last!
204
+ class Root < State
205
+ def initialize(config_schema, output_map, location, macros)
206
+ @steps = [ :preamble, :build, :output ]
207
+ @config_schema = config_schema
208
+ @output_map = output_map
209
+ @location = location
210
+ @macros = macros
211
+ end
212
+ def receive_event(event, memos)
213
+ case event.name
214
+ # The root object of a yamlfile must be a map, which is to say a config!
215
+ in :start_mapping
216
+ assert_step :build; next_step!
217
+ # We are just making sure that the root of the yaml file is a config,
218
+ # now we can hand off to the general config handler.
219
+ memos << :reject_event!
220
+ @value_minder = Internal::MultiValueMinder.new
221
+ [ self,
222
+ substate(GetConfig, schema: @config_schema, minder: @value_minder)
223
+ ]
224
+ in :start_document unless this_step == :preamble
225
+ bad_syntax! "YamlFile may not contain multiple documents!"
226
+ in :start_document
227
+ next_step!
228
+ unless event.data.tag_directives.empty?
229
+ # *we* are the schema; allowing tag directives would be confusing
230
+ # since they'd have no effect.
231
+ bad_syntax! "tag directives not permitted!"
232
+ end
233
+ [ self ]
234
+ in :end_document
235
+ assert_step :output; next_step!
236
+ # If the document is over, then GetConfig must be done populating the
237
+ # value minder for us.
238
+ @output_map.update @value_minder.value_map
239
+ end
240
+ end
241
+ end
242
+
243
+ # Turn a yaml map into a config!
244
+ class GetConfig < State
245
+ def initialize(...); super; @steps = [ :open, :collect_pairs ]; end
246
+ def receive_event(event, memos)
247
+ case event.name
248
+ in :start_mapping if this_step == :open
249
+ next_step!
250
+ @anchor = @macros.open_macro? event
251
+ [ self ]
252
+ in :scalar | :start_mapping | :start_sequence
253
+ assert_step :collect_pairs
254
+ # Other than handling the opening and closing of the map, the rest is
255
+ # up to the field getter.
256
+ memos << :reject_event!
257
+ [ self,
258
+ substate(GetKey),
259
+ ]
260
+ in :end_mapping
261
+ assert_step :collect_pairs; next_step!
262
+ # Now that the config is done, double-check that no values were
263
+ # partially specified.
264
+ unless (bads = @value_minder.incompletes).empty?
265
+ raise Err::IncompleteValue, <<~RANT_OVER
266
+ some fields were specified without enough values:
267
+ #{bads.join(', ')}
268
+ RANT_OVER
269
+ end
270
+ @macros.close_macro @anchor if @anchor
271
+ []
272
+ end
273
+ end
274
+ end
275
+
276
+ # Locate the schema for the field whose value is coming up next.
277
+ class GetKey < State
278
+ def initialize(...); super; @steps = [ :get ]; end
279
+ def receive_event(event, memos)
280
+ case event.name
281
+ in :scalar
282
+ assert_step :get; next_step!
283
+ @macros.macro? event
284
+ # Yaml gives us a string, we get the field schema it names.
285
+ key = event.data.value.to_sym
286
+ field_schema = @schema.field_get key
287
+ # Yaml allows repeat keys, but we don't want the ambiguity. If a
288
+ # field needs multiple values, they will be provided via a yaml
289
+ # sequence.
290
+ if @value_minder.value_map.key? key
291
+ raise Err::DanglingValue, <<~RANT_OVER
292
+ #{@location}: #{field_schema} already specified!
293
+ RANT_OVER
294
+ end
295
+ # Now we have a proper field schema to give to the value getter.
296
+ [ substate(GetFieldValue,
297
+ schema: field_schema),
298
+ ]
299
+ in :start_mapping | :start_sequence
300
+ bad_syntax! "YamlFile map keys must be strings!"
301
+ end
302
+ end
303
+ end
304
+
305
+ # Load the value for a field's @schema.
306
+ class GetFieldValue < State
307
+ def initialize(...); super; @steps = [ :get ]; end
308
+ def receive_event(event, memos)
309
+ assert_step :get; next_step!
310
+ case event.name
311
+ in :scalar
312
+ @macros.macro? event
313
+ field_value event.data.value
314
+ []
315
+ in :start_sequence
316
+ memos << :reject_event!
317
+ [ substate(GetSequenceField) ]
318
+ in :start_mapping
319
+ # A map as a value must be a subconfig, so we just need to make way
320
+ # for GetConfig.
321
+ unless @schema.const_defined?(:Config, NO_INHERIT)
322
+ bad_syntax! "#{@schema.attribute_name} is not a subconfig!"
323
+ end
324
+ memos << :reject_event!
325
+ @value_minder.append({}, @schema, human_context: @location.to_s)
326
+ [ substate(GetConfig,
327
+ schema: @schema::Config,
328
+ minder: Internal::MultiValueMinder.new(
329
+ @value_minder.value_map[@schema.attribute_name])),
330
+ ]
331
+ end
332
+ end
333
+ end
334
+
335
+ # Populate a plural field with values from a yaml sequence.
336
+ class GetSequenceField < State
337
+ def initialize(...); super; @steps = [ :open, :collect ]; end
338
+ def receive_event(event, memos)
339
+ case event.name
340
+ in :start_sequence unless this_step == :open
341
+ bad_syntax! "no nested lists!"
342
+ in :start_sequence
343
+ assert_step :open; next_step!
344
+ # Can this field even accept a sequence?
345
+ unless @schema.arity.demands_plurality?
346
+ raise Err::FieldNotApplicable, <<~RANT_OVER
347
+ #{@location}: #{@schema} does not take multiple values!
348
+ RANT_OVER
349
+ end
350
+ @anchor = @macros.open_macro? event
351
+ [ self ]
352
+ in :end_sequence
353
+ # Got all the values, was it enough?
354
+ assert_step :collect; next_step!
355
+ unless @value_minder.satisfied?(@schema)
356
+ raise Err::IncompleteValue, <<~RANT_OVER
357
+ #{@schema} requires #{@schema.arity.begin} values
358
+ RANT_OVER
359
+ end
360
+ []
361
+ in :scalar
362
+ @macros.macro? event
363
+ # repeatedly calling field_value allows the multi-value minder to
364
+ # handle combining the values.
365
+ field_value event.data.value
366
+ [ self ]
367
+ end
368
+ end
369
+ end
370
+
371
+ # General State functions available to all states.
372
+
373
+ # Unmarshal an individual field value and give it to the minder to store it
374
+ # appropriately. Also, gracefully handle unmarshal failures.
375
+ def field_value(value_string)
376
+ unless @schema.value_type.respond_to? :unmarshal
377
+ bad_syntax! <<~RANT_OVER
378
+ #{@schema} cannot be take its value from a string!
379
+ RANT_OVER
380
+ end
381
+ @value_minder.append(
382
+ @schema.value_type.unmarshal(value_string),
383
+ @schema,
384
+ human_context: @location.to_s,
385
+ )
386
+ rescue Err::MarshalUnacceptable
387
+ raise subexception $!, <<~RANT_OVER
388
+ #{@location}:
389
+ #{@schema} cannot accept '#{value_string}' as a value: #{$!}
390
+ RANT_OVER
391
+ end
392
+ # Create a new state of the given class, passing down our instance
393
+ # variables for any values not explicitly given to the initializer.
394
+ def substate(state_class, *args, **kwargs)
395
+ location, schema, macros, minder = [
396
+ @location, @schema, @macros, @value_minder
397
+ ]
398
+ sub = state_class.new(*args, **kwargs)
399
+ sub.instance_exec do
400
+ @location ||= location; @schema ||= schema; @macros ||= macros
401
+ @value_minder ||= minder
402
+ end
403
+ sub
404
+ end
405
+ # Guard against bugs by asserting the order in which events should arrive
406
+ # at a state.
407
+ def this_step; @steps.first; end
408
+ def next_step!; @steps.shift; end
409
+ def assert_step(step); fail if this_step != step; end
410
+
411
+ def subexception(superexception, message)
412
+ message = message.lines(chomp: true).join(' ').strip
413
+ superexception.exception message
414
+ end
415
+
416
+ def bad_syntax!(chide="bad syntax")
417
+ raise Err::BadSyntax, <<~END
418
+ #{chide} @ #{@location}
419
+ END
420
+ end
421
+ end
422
+
423
+ # Recording, storing, and replaying macros.
424
+ class MacroMeister
425
+ def initialize(event_handler, location)
426
+ @event_handler = event_handler
427
+ @handler_location = location
428
+ @pending_macros = []
429
+ @macros = {}
430
+ end
431
+
432
+ # Let States pass any ol' event down to us and not worry about whether it's
433
+ # actually relevant to macro recording.
434
+ def open_macro?(event)
435
+ open_macro event if event.data.anchor
436
+ event.data.anchor
437
+ end
438
+
439
+ def open_macro(event)
440
+ @pending_macros.push [ Step.from_raw(event, @handler_location) ]
441
+ end
442
+
443
+ def close_macro(name); @macros[name] = @pending_macros.pop; end
444
+
445
+ # Scalars are just one event, so don't make States call open and close
446
+ # right after one another for a scalar. See #open_macro?
447
+ def macro?(event)
448
+ return unless event.data.anchor
449
+ open_macro event
450
+ close_macro event.data.anchor
451
+ end
452
+
453
+ # Add the current parser event to any macros currently being recorded.
454
+ def record(event)
455
+ return if @pending_macros.empty?
456
+ step = Step.from_raw(event, @handler_location)
457
+ @pending_macros.each { _1 << step }
458
+ end
459
+
460
+ # Satisfy a yaml alias by replaying the events associated with the named
461
+ # anchor.
462
+ def replay(anchor)
463
+ unless @macros.key? anchor
464
+ raise Err::BadReference, <<~RANT_OVER
465
+ cannot alias nonexistant anchor #{anchor} @ #{@handler_location}
466
+ RANT_OVER
467
+ end
468
+ original_alias = @handler_location.alias_of
469
+
470
+ @macros .fetch(anchor) .each do |step|
471
+ @handler_location.alias_of = step.location
472
+ offer step.event
473
+ end
474
+
475
+ @handler_location.alias_of = original_alias
476
+ end
477
+
478
+ # Wrapper around EventHandler#offer, transparently replacing yaml alias
479
+ # events with the appropriate macro contents.
480
+ def offer(event)
481
+ if event.name == :alias
482
+ replay event.data.reference
483
+ else
484
+ @event_handler.offer event
485
+ end
486
+ end
487
+
488
+ # At every step in a macro, we need to know what we're replaying, and where
489
+ # that event originally came from.
490
+ Step = Struct.new(:event, :location) do
491
+ # Prevent spurious macro re-recording on playback by stripping anchors
492
+ # before recording.
493
+ def self.from_raw(event, location)
494
+ o = new
495
+ o.event = event.dup
496
+ o.event.data.anchor = nil if o.event.data.respond_to? :anchor
497
+ o.location = location.dup
498
+ o
499
+ end
500
+ end
501
+ end
502
+
503
+ end
504
+
505
+ end
506
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'schema'
2
+ require_relative 'sources/command_line'
3
+ require_relative 'sources/environment'
4
+ begin
5
+ require_relative 'sources/yaml_file'
6
+ rescue LoadError; end
7
+
8
+ module Configuratrix
9
+
10
+ module Sources
11
+ # Source that's always ready but never has values. Used for signaling that
12
+ # it's actually desired to have no viable sources over just misconfiguring no
13
+ # sources.
14
+ class Dummy < Source
15
+ ready? { true }
16
+ parse {}
17
+ key? { false }
18
+ get { undefined! _1 }
19
+ end
20
+
21
+ # Source that relies entirely on Souce::BaseBehaviors. By default it's
22
+ # essentially Dummy with extra steps.
23
+ # Having #parse set @value_map to something hashy makes for a nice testing
24
+ # source.
25
+ class Blank < Source
26
+ end
27
+ end
28
+
29
+ # Modules in the Configuratrix::Mutable namespace may be modified by client
30
+ # code wishing to personalize global defaults.
31
+ #
32
+ # This must never be done in library code because it disturbs preconditions.
33
+ # Mutable changes will affect all subsequent schema definitions and
34
+ # modifications. Additionally when modifying DefaultSources, the subsequent
35
+ # loading of all configs is affected.
36
+ #
37
+ # Customization can be cleanly accomplished by `require`ing a short file with a
38
+ # script author's preferences alongside the `require` for Configuratrix.
39
+ module Mutable
40
+
41
+ # Every constant defined in this module is added by default to lists of Sources.
42
+ module DefaultSources
43
+ CommandLine = Sources::CommandLine
44
+ YamlFile = Sources::YamlFile if Sources.const_defined? :YamlFile
45
+ Environment = Sources::Environment
46
+
47
+ def self.add(source_class)
48
+ const_set source_class.const_name, source_class
49
+ end
50
+ end
51
+
52
+ # Every constant defined in this module is available for inclusion and
53
+ # customization in a config. `source :dummy` will resolve to the Dummy below.
54
+ module BuiltinSources
55
+ Dummy = Sources::Dummy
56
+ Blank = Sources::Blank
57
+
58
+ def self.add(source_class)
59
+ const_set source_class.const_name, source_class
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,121 @@
1
+ require_relative 'schema'
2
+ require 'set'
3
+
4
+ module Configuratrix
5
+
6
+ module Types
7
+ class String < Type
8
+ recognize? { _1 .is_a? ::String or _1.nil? }
9
+ # Many types may want to recognize nil, but only String should claim it.
10
+ claim_all_recognized
11
+
12
+ parse { _1.to_s }
13
+ end
14
+
15
+ class Symbol < Type
16
+ recognize? { _1 .is_a? ::Symbol }
17
+ claim_all_recognized
18
+
19
+ parse { _1.to_sym }
20
+ end
21
+
22
+ class Int < Type
23
+ recognize? { _1 .is_a? ::Integer }
24
+ claim_all_recognized
25
+
26
+ parse {
27
+ # Ruby will detect radix quite nicely automatically, but it also includes
28
+ # '0' as a prefix for octal which is too much of a gotcha for just
29
+ # happening to pass 0100 instead of 100.
30
+ radix = 10
31
+ radix = 16 if /^-?0x/i =~ _1
32
+ radix = 2 if /^-?0b/i =~ _1
33
+ Integer(_1, radix, exception: false) or nope! "not integer!"
34
+ }
35
+ end
36
+
37
+ class Float < Type
38
+ recognize? { _1 .is_a? ::Float }
39
+ claim_all_recognized
40
+
41
+ parse { Float(_1, exception: false) or nope! "not float!" }
42
+ end
43
+
44
+ class Bool < Type
45
+ # enables `--boolfield`
46
+ affirm { true }
47
+ # enables `--no-boolfield`
48
+ negate { false }
49
+
50
+ recognize? { _1 == true or _1 == false }
51
+ claim_all_recognized
52
+
53
+ # covers `--boolfield=value`
54
+ parse {
55
+ case _1
56
+ when /^(true|t|yes|y|1)$/i
57
+ true
58
+ when /^(false|f|no|n|0)$/i
59
+ false
60
+ else
61
+ nope! "true or false!"
62
+ end
63
+ }
64
+ end
65
+
66
+ class Enum < Type
67
+ def self.enum_symbols; @values ||= Set.new; end
68
+
69
+ recognize? { enum_symbols.include? _1 }
70
+ parse {
71
+ value = _1.downcase.to_sym
72
+ if enum_symbols.include? value
73
+ value
74
+ else
75
+ nope! "not in #{enum_symbols}!"
76
+ end
77
+ }
78
+ end
79
+
80
+ class Subconfig < Type
81
+ recognize? { _1 .is_a? Configuratrix::Config }
82
+
83
+ # A subconfig is no ordinary field; we can't simply parse it all in one go.
84
+ # We have to construct a subconfig object to recur on all its fields.
85
+ def self.take_value_for(field, from:)
86
+ sources = from
87
+ key = [ *field.schema.subconfig_path, field.schema.attribute_name ]
88
+ if sources.none? { _1.key? key }
89
+ []
90
+ else
91
+ [ field.schema::Config.new(field.containing_config) ]
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # Modules in the Configuratrix::Mutable namespace may be modified by client
98
+ # code wishing to personalize global defaults.
99
+ #
100
+ # This must never be done in library code because it disturbs preconditions.
101
+ # Mutable changes will affect all subsequent schema definitions and
102
+ # modifications.
103
+ #
104
+ # Customization can be cleanly accomplished by `require`ing a short file with a
105
+ # script author's preferences alongside the `require` for Configuratrix.
106
+ module Mutable
107
+
108
+ # Every constant defined in this module is added by default to lists of Types.
109
+ module DefaultTypes
110
+ include Types
111
+
112
+ def self.add(source_class)
113
+ const_set source_class.const_name, source_class
114
+ end
115
+ end
116
+
117
+ # The type that an untyped field implicitly uses.
118
+ DefaultType = DefaultTypes::String
119
+
120
+ end
121
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'configuratrix/initialize'
2
+ # Look here for the statements you can make when defining a configuration for
3
+ # your script.
4
+ require_relative 'configuratrix/language'
5
+ # Config and the other base classes that define a blank generic config.
6
+ require_relative 'configuratrix/schema'
7
+ # Built-in means of loading and organizing values to put into fields.
8
+ require_relative 'configuratrix/sources'
9
+ # Built-in means of translating strings into values for fields.
10
+ require_relative 'configuratrix/types'
11
+ # All the Error classes particular to this library.
12
+ require_relative 'configuratrix/errors'