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,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'
|