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,585 @@
|
|
|
1
|
+
require_relative '../schema'
|
|
2
|
+
require_relative 'util'
|
|
3
|
+
|
|
4
|
+
module Configuratrix
|
|
5
|
+
module Sources
|
|
6
|
+
|
|
7
|
+
class CommandLine < Source
|
|
8
|
+
init { @line_tokens = ARGV }
|
|
9
|
+
parse { |config_schema|
|
|
10
|
+
@value_map = Parser.parse(config_schema, @line_tokens.map(&:dup))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# Per-field configurations pertinent to command line parsing.
|
|
14
|
+
TOGGLES = [
|
|
15
|
+
# nil or a one-character symbol in [a-zA-Z]. A symbol maps, e.g., :x to
|
|
16
|
+
# "-x" on the command line.
|
|
17
|
+
:short_flag,
|
|
18
|
+
# For fields where :positional is true, the fields are given all command
|
|
19
|
+
# line values not associated with flags. The values are given to
|
|
20
|
+
# positional fields in declaration order successively as each field is
|
|
21
|
+
# satisfied.
|
|
22
|
+
:positional,
|
|
23
|
+
# At most one positional field per config may be marked as the command
|
|
24
|
+
# selector. When the command selector receives its value, command line
|
|
25
|
+
# parsing changes context from the current config to the named command's
|
|
26
|
+
# subconfig.
|
|
27
|
+
:command_selector,
|
|
28
|
+
# This field isn't the one that selected the command, it is one of the
|
|
29
|
+
# subconfigs associated with an available command.
|
|
30
|
+
:command_subconfig,
|
|
31
|
+
]
|
|
32
|
+
class FieldToggles < Internal::ToggleStruct[*TOGGLES]
|
|
33
|
+
SHORT_FLAG = -> { /^[a-zA-Z]$/ =~ _1 and _1.is_a? Symbol }
|
|
34
|
+
def short_flag=(...); reject_unless super, matches: SHORT_FLAG; end
|
|
35
|
+
|
|
36
|
+
def positional=(...); reject_unless super, matches: A::BOOL; end
|
|
37
|
+
def command_selector=(...); reject_unless super, matches: A::BOOL; end
|
|
38
|
+
def command_subconfig=(...); reject_unless super, matches: A::BOOL; end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Parser
|
|
42
|
+
def self.parse(config_schema,tokens)
|
|
43
|
+
new(config_schema).parse(tokens)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(config_schema)
|
|
47
|
+
@config_view = ConfigView.new(config_schema)
|
|
48
|
+
# source of tokens to parse
|
|
49
|
+
@stream = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse(tokens)
|
|
53
|
+
@stream = Internal::Stream.new(tokens.each)
|
|
54
|
+
# Whether tokens that look like flags should be treated like flags. This
|
|
55
|
+
# becomes false upon encountering the "--" token.
|
|
56
|
+
@flags_are_visible = true
|
|
57
|
+
|
|
58
|
+
parse_command_line
|
|
59
|
+
|
|
60
|
+
@config_view.finalize
|
|
61
|
+
@config_view.root[:value_map]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def parse_command_line
|
|
67
|
+
loop do
|
|
68
|
+
case peek
|
|
69
|
+
in []
|
|
70
|
+
return
|
|
71
|
+
in [LONG_FLAG] if @flags_are_visible
|
|
72
|
+
parse_long_flag
|
|
73
|
+
in [SHORT_FLAG] if @flags_are_visible
|
|
74
|
+
parse_short_flag
|
|
75
|
+
in [END_OF_FLAGS] if @flags_are_visible
|
|
76
|
+
take!
|
|
77
|
+
@flags_are_visible = false
|
|
78
|
+
in [String]
|
|
79
|
+
parse_positional
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_long_flag
|
|
85
|
+
raw_token = take!(:LONG_FLAG)
|
|
86
|
+
# remove the now functionless leading "--"
|
|
87
|
+
token = raw_token.sub(LONG_FLAG, '')
|
|
88
|
+
|
|
89
|
+
# handle "--key=value"
|
|
90
|
+
if token.include? SINGLE_TOKEN_KEY_VALUE_BOUNDARY
|
|
91
|
+
key, value_str = split_single_token_kv(token)
|
|
92
|
+
|
|
93
|
+
return unmarshal_and_store_for_field(
|
|
94
|
+
raw_token,
|
|
95
|
+
value_str,
|
|
96
|
+
(field_for key),
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# If `token` wasn't a single-token key-value pair, then it's the key.
|
|
101
|
+
|
|
102
|
+
# Accommodate the convention of using the prefix "no-" to signal the
|
|
103
|
+
# negation of a long flag.
|
|
104
|
+
# Danger! If field keys aren't prevented from starting with "no-"
|
|
105
|
+
# themselves, then deleting NEGATORY_PREFIX creates ambiguity between
|
|
106
|
+
# "--no-chance" standing for negated "chance" or just "no-chance".
|
|
107
|
+
# Validation via #approve_field safely covers this sharp edge.
|
|
108
|
+
negated = (token.delete_prefix! NEGATORY_PREFIX) != nil
|
|
109
|
+
# Having to press shift for underscore is a big drag on operating a cli
|
|
110
|
+
# quickly, but dashes are illegal in field names.
|
|
111
|
+
token.gsub! '-', '_'
|
|
112
|
+
# Dashes in the key string couldn't have served any other purpose to this
|
|
113
|
+
# parser anyway, so it's safe to obliterate them unconditionally like
|
|
114
|
+
# this.
|
|
115
|
+
|
|
116
|
+
key = token.to_sym
|
|
117
|
+
field = field_for key
|
|
118
|
+
|
|
119
|
+
# Key's all set; on to value
|
|
120
|
+
|
|
121
|
+
if negated
|
|
122
|
+
# A negated flag must take on its type's negative value. Nothing else
|
|
123
|
+
# to do for this token.
|
|
124
|
+
if !field.value_type.respond_to? :negatory
|
|
125
|
+
raise Err::FieldNotApplicable, <<~END
|
|
126
|
+
'#{raw_token}' : #{field} can not be negated
|
|
127
|
+
END
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
value = field.value_type.negatory(field.list_of_default)
|
|
131
|
+
return store_for_field(raw_token, value, field)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# If the field's type has an implicit positive value, then we must apply
|
|
135
|
+
# that value now and be done with the token. If the type also allows
|
|
136
|
+
# explicit values, they must be provided via the --key=value format which
|
|
137
|
+
# has already been handled. These restrictions prevent ambiguity in the
|
|
138
|
+
# command line grammar.
|
|
139
|
+
# For example, in "--bool 0":
|
|
140
|
+
# - Is :bool being implicitly affirmed true followed by positional
|
|
141
|
+
# argument "0"?
|
|
142
|
+
# - Is :bool being explicitly declared false by the following "0" token?
|
|
143
|
+
# Better to resolve this now by always preferring the implicit option and
|
|
144
|
+
# leaving explicit values to --key=value.
|
|
145
|
+
if field.value_type.respond_to? :affirmative
|
|
146
|
+
value = field.value_type.affirmative(field.list_of_default)
|
|
147
|
+
return store_for_field(raw_token, value, field)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# By this point the value must be the next command line token.
|
|
151
|
+
|
|
152
|
+
unless field.value_type.respond_to? :unmarshal
|
|
153
|
+
# Die before unmarshal_and_store_for_field: here we have the additional
|
|
154
|
+
# context that :affirm was also tried.
|
|
155
|
+
raise Err::FieldNotApplicable, <<~END
|
|
156
|
+
'#{raw_token}' : #{field} can be neither set explicitly nor affirmed
|
|
157
|
+
implicitly
|
|
158
|
+
END
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
raw_token << " " << (value_str = take!(:VALUE))
|
|
162
|
+
unmarshal_and_store_for_field( raw_token, value_str, field )
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_short_flag
|
|
166
|
+
raw_token = take!(:SHORT_FLAG)
|
|
167
|
+
token = raw_token.sub(SHORT_FLAG, '')
|
|
168
|
+
|
|
169
|
+
# handle -k=value
|
|
170
|
+
if token.include? SINGLE_TOKEN_KEY_VALUE_BOUNDARY
|
|
171
|
+
key, value_str = split_single_token_kv(token)
|
|
172
|
+
# single-token short flags may not specify multiple flags at once.
|
|
173
|
+
if key.length > 1
|
|
174
|
+
raise Err::BadSyntax, <<~END
|
|
175
|
+
'#{raw_token}' : cannot combine multiple short flags and '='
|
|
176
|
+
assignment
|
|
177
|
+
END
|
|
178
|
+
end
|
|
179
|
+
field = field_for_short_flag key
|
|
180
|
+
return unmarshal_and_store_for_field( raw_token, value_str, field )
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# If it wasn't a single-token key-value pair, then this token must just
|
|
184
|
+
# be the key(s).
|
|
185
|
+
case token.chars.map(&:to_sym)
|
|
186
|
+
in []
|
|
187
|
+
# no keys should never happen
|
|
188
|
+
raise Err::BadSyntax, <<~END
|
|
189
|
+
'#{raw_token}' : no short flags given
|
|
190
|
+
END
|
|
191
|
+
in [sole_key]
|
|
192
|
+
# One key by itself (e.g. `[:c]` from "-c") can represent fields with
|
|
193
|
+
# two potential value representations:
|
|
194
|
+
# - Implicit, where the flag receives a certain value that is
|
|
195
|
+
# particular to the field's Type.
|
|
196
|
+
# e.g. `[:c]` from "-c" affirming the boolean `:c` field to `true`.
|
|
197
|
+
# - Explicit, where the value is taken from the following line token.
|
|
198
|
+
# For fields that support implicit affirmation we will never take the
|
|
199
|
+
# following line token to be the field's explicit value.
|
|
200
|
+
#
|
|
201
|
+
# This prohibition eliminates ambiguity in the command line grammar.
|
|
202
|
+
# Fields can still be implicitly affirmable and work with explicit
|
|
203
|
+
# values via the -k=v syntax.
|
|
204
|
+
field = field_for_short_flag sole_key
|
|
205
|
+
if field.value_type.respond_to? :affirmative
|
|
206
|
+
value = field.value_type.affirmative(field.list_of_default)
|
|
207
|
+
store_for_field(raw_token, value, field)
|
|
208
|
+
else
|
|
209
|
+
# Only for non-affirmable types will we take the next token as value.
|
|
210
|
+
raw_token << " " << (value_str = take!(:VALUE))
|
|
211
|
+
unmarshal_and_store_for_field( raw_token, value_str, field )
|
|
212
|
+
end
|
|
213
|
+
in [*keys]
|
|
214
|
+
# with multiple keys, they all must be settable implicitly
|
|
215
|
+
fields = keys.map { field_for_short_flag _1 }
|
|
216
|
+
misplaced = fields.reject { _1.value_type.respond_to? :affirmative }
|
|
217
|
+
unless misplaced.empty?
|
|
218
|
+
bad_flags = misplaced.map {
|
|
219
|
+
"-#{_1.toggles(CommandLine).short_flag}"
|
|
220
|
+
} .join(" and ")
|
|
221
|
+
raise Err::FieldNotApplicable, <<~END
|
|
222
|
+
'#{raw_token}' : #{bad_flags} can't be specified without a value
|
|
223
|
+
END
|
|
224
|
+
end
|
|
225
|
+
# Set all fields named by `keys` to their implicit affirmative values.
|
|
226
|
+
fields.map do |field|
|
|
227
|
+
value = field.value_type.affirmative(field.list_of_default)
|
|
228
|
+
store_for_field(raw_token, value, field)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def parse_positional
|
|
234
|
+
raw_token = take!
|
|
235
|
+
positional_stack = @config_view[:positionals].stack
|
|
236
|
+
if positional_stack.empty?
|
|
237
|
+
raise Err::DanglingValue, <<~END
|
|
238
|
+
'#{raw_token}' : no further values expected
|
|
239
|
+
END
|
|
240
|
+
end
|
|
241
|
+
field = positional_stack.pop
|
|
242
|
+
unless @config_view[:multi_minder].saturated_after_next? field
|
|
243
|
+
positional_stack.push field
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
unmarshal_and_store_for_field( raw_token, raw_token, field )
|
|
247
|
+
|
|
248
|
+
if toggles_of(field).command_selector
|
|
249
|
+
selected_subconfig = @config_view[:value_map][field.attribute_name]
|
|
250
|
+
unselected_subconfigs = (
|
|
251
|
+
@config_view[:subconfig_fields].map(&:attribute_name)
|
|
252
|
+
.select { selected_subconfig != _1 })
|
|
253
|
+
|
|
254
|
+
# Removing the submaps for unselected subconfigs signals to
|
|
255
|
+
# Types::Subconfig not to load them.
|
|
256
|
+
@config_view[:value_map].reject! { |k,v|
|
|
257
|
+
unselected_subconfigs.include? k
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@config_view.enter selected_subconfig
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
#
|
|
265
|
+
# Parsing Helpers
|
|
266
|
+
#
|
|
267
|
+
|
|
268
|
+
def peek; @stream.peek; end
|
|
269
|
+
def take; @stream.take; end
|
|
270
|
+
|
|
271
|
+
# Take a required value. If args are not empty, they are taken to be
|
|
272
|
+
# symbols naming consts in Recognizers, one of which must match the value.
|
|
273
|
+
def take!(*names)
|
|
274
|
+
case take
|
|
275
|
+
in []
|
|
276
|
+
suffix = names.join(' or ')
|
|
277
|
+
suffix = ", needed " + suffix unless suffix.empty?
|
|
278
|
+
raise Err::BadSyntax, <<~END
|
|
279
|
+
unexpected end of input#{suffix}
|
|
280
|
+
END
|
|
281
|
+
in [value]
|
|
282
|
+
if (names.empty? or
|
|
283
|
+
names.map { Recognizers.const_get _1 } .any? { _1 === value })
|
|
284
|
+
value
|
|
285
|
+
else
|
|
286
|
+
raise Err::BadSyntax, <<~END
|
|
287
|
+
unexpected #{value.inspect}, must be #{names.join(' or ')}
|
|
288
|
+
END
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# properly split a token of the form "key=value"
|
|
294
|
+
def split_single_token_kv(token)
|
|
295
|
+
# If the delimiter appears more than once, the subsequent occurrences
|
|
296
|
+
# are just part of the value. So, split the token into at most 2
|
|
297
|
+
# pieces: the key; and the value, equalses and all.
|
|
298
|
+
key_str, value_str = token.split SINGLE_TOKEN_KEY_VALUE_BOUNDARY, 2
|
|
299
|
+
[key_str.to_sym, value_str]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Fetch the schema for the named field, or raise since we've encountered a
|
|
303
|
+
# key not in the schema.
|
|
304
|
+
def field_for(key)
|
|
305
|
+
true_schema = @config_view[:schema]
|
|
306
|
+
field_name_in_context(key) do |field_name|
|
|
307
|
+
config_schema = @config_view[:schema]
|
|
308
|
+
if !config_schema.field_defined? field_name
|
|
309
|
+
raise Err::NoSuchField, <<~END
|
|
310
|
+
#{true_schema} has no field :#{key}
|
|
311
|
+
END
|
|
312
|
+
end
|
|
313
|
+
return config_schema.field_get field_name
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Call the supplied block with the field name from raw_key, after
|
|
318
|
+
# temporarily advancing the ConfigView context down any path encoded in
|
|
319
|
+
# raw_key, e.g. :'a.b.field' or :field
|
|
320
|
+
def field_name_in_context(raw_key, &block)
|
|
321
|
+
keys = raw_key .to_s .split('.') .map(&:to_sym)
|
|
322
|
+
path = keys[0..-2]
|
|
323
|
+
field = keys.last
|
|
324
|
+
|
|
325
|
+
@config_view.subcontext(path) do
|
|
326
|
+
block.call field.to_sym
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def field_for_short_flag(short_key)
|
|
331
|
+
key = @config_view[:shorts_to_longs][short_key]
|
|
332
|
+
if key.nil?
|
|
333
|
+
raise Err::AmbiguousName, <<~END
|
|
334
|
+
short flag -#{short_key} doesn't stand for anything
|
|
335
|
+
END
|
|
336
|
+
end
|
|
337
|
+
field_for key
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def unmarshal_and_store_for_field(raw_token, value_str, field)
|
|
341
|
+
unless field.value_type.respond_to? :unmarshal
|
|
342
|
+
raise Err::FieldNotApplicable, <<~END
|
|
343
|
+
'#{raw_token}' : #{field} cannot be given an explicit value
|
|
344
|
+
END
|
|
345
|
+
end
|
|
346
|
+
# Parse and set the value, giving a nicer error message on parse
|
|
347
|
+
# failure than the type can give by itself.
|
|
348
|
+
begin
|
|
349
|
+
store_for_field(raw_token, field.value_type.unmarshal(value_str), field)
|
|
350
|
+
rescue Err::MarshalUnacceptable
|
|
351
|
+
raise $!.exception("'#{raw_token}' : " +
|
|
352
|
+
"#{field} cannot accept '#{value_str}' as a value: #{$!}")
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def store_for_field(raw_token, value, field)
|
|
357
|
+
@config_view[:multi_minder].append(value, field, human_context: raw_token)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def toggles_of(field); field.toggles(CommandLine); end
|
|
361
|
+
|
|
362
|
+
# Mediates the interaction between the parser and the config, allowing for
|
|
363
|
+
# subconfigs to be handled transparently to the parsing logic.
|
|
364
|
+
class ConfigView
|
|
365
|
+
attr_accessor :root
|
|
366
|
+
|
|
367
|
+
def initialize(config_schema)
|
|
368
|
+
@root = ConfigContext.new
|
|
369
|
+
@root[:schema] = config_schema
|
|
370
|
+
@root[:value_map] = {}
|
|
371
|
+
@root[:multi_minder] = Internal::MultiValueMinder.new(@root[:value_map])
|
|
372
|
+
build_tree @root
|
|
373
|
+
|
|
374
|
+
@context = @root
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def enter(name)
|
|
378
|
+
@context = @context.children.fetch name
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def leave; @context = @context.parent; end
|
|
382
|
+
|
|
383
|
+
def subcontext(path, &block)
|
|
384
|
+
path.each { enter _1 }
|
|
385
|
+
block.call
|
|
386
|
+
path.each { leave }
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def finalize
|
|
390
|
+
all_contexts.each do |context|
|
|
391
|
+
CommandLine.validate_config_postconditions context
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def build_tree(context)
|
|
396
|
+
context.survey
|
|
397
|
+
CommandLine.validate_config_preconditions context
|
|
398
|
+
# Recur on all fields with internal config schemata.
|
|
399
|
+
context[:schema].fields_schema.select {
|
|
400
|
+
_1.const_defined?(:Config, NO_INHERIT)
|
|
401
|
+
}.each do |field_schema|
|
|
402
|
+
subname = field_schema.attribute_name
|
|
403
|
+
|
|
404
|
+
child = ConfigContext.new
|
|
405
|
+
child.parent = context
|
|
406
|
+
child[:schema] = field_schema::Config
|
|
407
|
+
context[:value_map][subname] = {}
|
|
408
|
+
child[:value_map] = context[:value_map][subname]
|
|
409
|
+
child[:multi_minder] = Internal::MultiValueMinder.new(
|
|
410
|
+
child[:value_map])
|
|
411
|
+
context.children[subname] = child
|
|
412
|
+
|
|
413
|
+
build_tree child
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def all_contexts
|
|
418
|
+
Enumerator.new do |output|
|
|
419
|
+
queue = [@root]
|
|
420
|
+
while !queue.empty? do
|
|
421
|
+
context = queue.pop
|
|
422
|
+
output.yield context
|
|
423
|
+
queue.prepend(*context.children.values)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def [](...); @context.[](...); end
|
|
429
|
+
def []=(...); @context.[]=(...); end
|
|
430
|
+
|
|
431
|
+
context_sensitive_values = [
|
|
432
|
+
:schema, # the schema of the config we're currently parsing into
|
|
433
|
+
:value_map, # the map into which newly parsed values should be put
|
|
434
|
+
:multi_minder, # manager of multi-value args, see MultiValueMinder
|
|
435
|
+
:positionals, # see Positionals
|
|
436
|
+
:shorts_to_longs, # map from 1-char short flags to full flag names
|
|
437
|
+
:command_field, # the field that selects the command
|
|
438
|
+
:subconfig_fields, # subconfigs for commands
|
|
439
|
+
]
|
|
440
|
+
class ContextSensitives < Struct.new(*context_sensitive_values)
|
|
441
|
+
def initialize
|
|
442
|
+
self.positionals = Positionals.new
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
class ConfigContext < Struct.new(:parent, :payload, :children)
|
|
447
|
+
def initialize
|
|
448
|
+
self.payload = ContextSensitives.new
|
|
449
|
+
self.children = {}
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def survey
|
|
453
|
+
survey = Internal::ToggleSurvey.new(self[:schema], FieldToggles)
|
|
454
|
+
fetch = -> { self[:schema].field_get _1 }
|
|
455
|
+
# declare survey plan
|
|
456
|
+
shorts_to_longs = survey.canvas_for(:short_flag).values_to_owners
|
|
457
|
+
command_field = (
|
|
458
|
+
survey.canvas_for(:command_selector).name.map(&fetch).last)
|
|
459
|
+
positionals_schema = survey.canvas_for(:positional).names.map(&fetch)
|
|
460
|
+
subconfig_fields = (
|
|
461
|
+
survey.canvas_for(:command_subconfig) .names .map(&fetch))
|
|
462
|
+
|
|
463
|
+
# execute and unpack results
|
|
464
|
+
survey.fetch_results
|
|
465
|
+
self[:shorts_to_longs] = ~shorts_to_longs
|
|
466
|
+
self[:command_field] = ~command_field
|
|
467
|
+
self[:positionals].schema = ~positionals_schema
|
|
468
|
+
self[:subconfig_fields] = ~subconfig_fields
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Brackets give quick access to payload values without having them
|
|
472
|
+
# intermixed with metadata like #parent.
|
|
473
|
+
def [](...); self.payload.[](...); end
|
|
474
|
+
def []=(...); self.payload.[]=(...); end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Schema of all the positional arguments in a context along with the
|
|
478
|
+
# stack of the next positionals to receive parsed values.
|
|
479
|
+
class Positionals < Struct.new(:schema, :stack)
|
|
480
|
+
def initialize
|
|
481
|
+
self.schema = []
|
|
482
|
+
self.stack = []
|
|
483
|
+
end
|
|
484
|
+
def schema=(schema_list)
|
|
485
|
+
super
|
|
486
|
+
self.stack = schema_list.reverse
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# 'normally' a flag key and value are separated by being in separate tokens
|
|
493
|
+
# (--key value), but also offering a non-tokenizing delimiter (--key=value)
|
|
494
|
+
# allows for greater flexibility (e.g. --testarg=--filter=foo).
|
|
495
|
+
SINGLE_TOKEN_KEY_VALUE_BOUNDARY = '='
|
|
496
|
+
# Specifying a field like `--no-field` asks the field's value type to
|
|
497
|
+
# produce its generic negative value.
|
|
498
|
+
NEGATORY_PREFIX = 'no-'
|
|
499
|
+
|
|
500
|
+
module Recognizers
|
|
501
|
+
# The types of command line tokens.
|
|
502
|
+
LONG_FLAG = /^--(?=.)/
|
|
503
|
+
SHORT_FLAG = /^-(?=[^-])/
|
|
504
|
+
END_OF_FLAGS = /^--$/
|
|
505
|
+
# Categorical
|
|
506
|
+
NON_VALUE = Regexp.union(LONG_FLAG, SHORT_FLAG, END_OF_FLAGS)
|
|
507
|
+
VALUE = -> { !(NON_VALUE === _1) }
|
|
508
|
+
end
|
|
509
|
+
include Recognizers
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Verify that the given field schema is not fundamentamentally incompatible
|
|
513
|
+
# with CommandLine parsing.
|
|
514
|
+
def self.approve_field(field_schema)
|
|
515
|
+
super
|
|
516
|
+
# If a field's name starts with :no_, then the convenience equivalence of
|
|
517
|
+
# dash and underscore makes this field ambiguous with the negation of a
|
|
518
|
+
# valid potential field name, e.g. :no_chance vs. negated :chance.
|
|
519
|
+
# Prohibiting fields that start with :no_ seems a small price to pay for a
|
|
520
|
+
# quick and non-bug-prone guarantee of unambiguity.
|
|
521
|
+
if field_schema.attribute_name.start_with? 'no_'
|
|
522
|
+
raise Err::PathologicalSchema, <<~END
|
|
523
|
+
field #{field_schema.name} conflicts with the `no-` negatory flag
|
|
524
|
+
prefix on the command line.
|
|
525
|
+
END
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def self.validate_config_preconditions(context)
|
|
530
|
+
positionals_schema = context[:positionals].schema
|
|
531
|
+
# Positionals are inherently values without explicit keys; it doesn't
|
|
532
|
+
# make sense for positionals to only produce implicit values for explicit
|
|
533
|
+
# keys.
|
|
534
|
+
bads = positionals_schema.reject { _1.value_type.respond_to? :unmarshal }
|
|
535
|
+
unless bads.empty?
|
|
536
|
+
raise Err::FieldNotApplicable, <<~END
|
|
537
|
+
positional fields must take explicit values, unlike #{bads.join(', ')}
|
|
538
|
+
END
|
|
539
|
+
end
|
|
540
|
+
# It would be very confusing to get yelled at for a missing value in one
|
|
541
|
+
# positional, only to add a value and have it get consumed by a lurking
|
|
542
|
+
# optional before the required positional.
|
|
543
|
+
positionals_schema.each_cons(2) do |prior,later|
|
|
544
|
+
if !prior.required? and later.required?
|
|
545
|
+
raise Err::PathologicalSchema, <<~END
|
|
546
|
+
required positional #{later} cannot follow optional #{prior}
|
|
547
|
+
END
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
# If a positional field would never stop consuming values, then it must
|
|
551
|
+
# be the last positional.
|
|
552
|
+
first_endless = positionals_schema.find { _1.arity.endless? }
|
|
553
|
+
unless first_endless.nil? or first_endless == positionals_schema.last
|
|
554
|
+
inaccessibles = (positionals_schema.slice_after { first_endless == _1 }
|
|
555
|
+
.to_a.last.join(', '))
|
|
556
|
+
raise Err::PathologicalSchema, <<~END
|
|
557
|
+
#{first_endless} accepts any number of values, so #{inaccessibles}
|
|
558
|
+
will never receive values
|
|
559
|
+
END
|
|
560
|
+
end
|
|
561
|
+
# A command permanently changes out of the current config context, so any
|
|
562
|
+
# following positionals in this config are inaccessible.
|
|
563
|
+
command_field = context[:command_field]
|
|
564
|
+
unless command_field.nil? or command_field == positionals_schema.last
|
|
565
|
+
inaccessibles = (positionals_schema.slice_after(command_field)
|
|
566
|
+
.to_a.last.join(', '))
|
|
567
|
+
raise Err::PathologicalSchema, <<~END
|
|
568
|
+
positionals beyond the command selector will never receive values:
|
|
569
|
+
#{inaccessibles}
|
|
570
|
+
END
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def self.validate_config_postconditions(context)
|
|
575
|
+
unless (bads = context[:multi_minder].incompletes).empty?
|
|
576
|
+
raise Err::IncompleteValue, <<~END
|
|
577
|
+
some fields were specified without enough values:
|
|
578
|
+
#{bads.join(', ')}
|
|
579
|
+
END
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
end
|
|
585
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require_relative '../schema'
|
|
2
|
+
|
|
3
|
+
module Configuratrix
|
|
4
|
+
module Sources
|
|
5
|
+
|
|
6
|
+
class Environment < Source
|
|
7
|
+
ready? { schema .const_defined? :PREFIX }
|
|
8
|
+
|
|
9
|
+
init { @env = ENV.to_hash }
|
|
10
|
+
parse { |config_schema|
|
|
11
|
+
@value_map = config_schema.subconfig_tree
|
|
12
|
+
parse_config(config_schema)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def parse_config(config_schema)
|
|
16
|
+
target_map = [
|
|
17
|
+
@value_map, *config_schema.subconfig_path ] .reduce { _1 .fetch _2 }
|
|
18
|
+
|
|
19
|
+
# Parse all the leaf fields in this config.
|
|
20
|
+
config_schema .fields_schema .map {
|
|
21
|
+
[ env_key(config_schema, _1.attribute_name), _1 ]
|
|
22
|
+
} .select { |key,field|
|
|
23
|
+
@env.key? key and field.value_type.respond_to? :unmarshal
|
|
24
|
+
} .each do |key,field|
|
|
25
|
+
value_str = @env .fetch key
|
|
26
|
+
begin
|
|
27
|
+
target_map[field.attribute_name] = field.value_type.unmarshal(value_str)
|
|
28
|
+
rescue Err::MarshalUnacceptable
|
|
29
|
+
raise $!.exception "ENV[ #{key} ] : #{field} " +
|
|
30
|
+
"cannot accept '#{value_str}' as a value"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Recur on subconfig fields.
|
|
35
|
+
config_schema .fields_schema .select {
|
|
36
|
+
_1.const_defined?(:Config, NO_INHERIT)
|
|
37
|
+
} .each do |field|
|
|
38
|
+
parse_config field::Config
|
|
39
|
+
end
|
|
40
|
+
@value_map
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def env_key(config_schema, field_name)
|
|
44
|
+
(PREFIX.to_s +
|
|
45
|
+
[ *config_schema.subconfig_path, field_name ] .join('__')
|
|
46
|
+
) .upcase .to_sym
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Set the prefix that will be prepended to field names when looking them up
|
|
50
|
+
# from the environment. Setting this constant enables loading from the
|
|
51
|
+
# environment, use PREFIX = '' if you want the variable names to be the same
|
|
52
|
+
# as the (UPCASED) field names.
|
|
53
|
+
def self.prefix(str)
|
|
54
|
+
remove_const :PREFIX if const_defined? :PREFIX
|
|
55
|
+
const_set :PREFIX, str
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|