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,639 @@
|
|
|
1
|
+
require_relative 'language-util'
|
|
2
|
+
|
|
3
|
+
module Configuratrix
|
|
4
|
+
module Internal
|
|
5
|
+
|
|
6
|
+
# These methods are the statements used to define a configuration.
|
|
7
|
+
#
|
|
8
|
+
# The methods become class methods of the Config class itself. Defining a
|
|
9
|
+
# configuration for a script is defining a subclass of Config, so these methods
|
|
10
|
+
# can be called directly (i.e. with implicit receiver) within the class block.
|
|
11
|
+
# If you're using a shortcut method where you never see the schema class, the
|
|
12
|
+
# methods are available in whatever block defines the configuration.
|
|
13
|
+
module ConfigSchemaLanguage
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Add a config field named `name` and defined by `block`. Config objects
|
|
17
|
+
# will have a method of the same name to get the field's value for the
|
|
18
|
+
# configuration.
|
|
19
|
+
#
|
|
20
|
+
# If name is not given, and in its place one keyword argument is given, then
|
|
21
|
+
# the key will be the field's name, and the value will be the field's
|
|
22
|
+
# default value. A block can be given to define the field further just as
|
|
23
|
+
# with the no-default form.
|
|
24
|
+
# for example:
|
|
25
|
+
# field :no_default
|
|
26
|
+
# field defaults_to: 42
|
|
27
|
+
#
|
|
28
|
+
# If the field already exists, block is class_exec'd against that (after the
|
|
29
|
+
# default is set if specified). See FieldSchemaLanguage for the statements
|
|
30
|
+
# available in field blocks. The resulting field class will be named
|
|
31
|
+
# <self.name>::Field::Name.
|
|
32
|
+
def field(name=nil, **kwargs, &block)
|
|
33
|
+
name, has_default, default_value = (
|
|
34
|
+
Internal::ConfigSchemaLanguageUtil.unpack_field_args(name, kwargs))
|
|
35
|
+
|
|
36
|
+
target = Internal::NestingSchemaUtil.absent_local_or_remote(
|
|
37
|
+
name,
|
|
38
|
+
method(:fields),
|
|
39
|
+
absent: -> { field_add Field.nested_class!(name, under: self) },
|
|
40
|
+
local: -> { field_get name },
|
|
41
|
+
remote: -> { field_lift name },
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if has_default
|
|
45
|
+
target.class_exec { default default_value }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
target.class_exec(&block) if block
|
|
49
|
+
target
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Define a positional command line argument. See also #field.
|
|
53
|
+
def arg(name=nil, **kwargs, &user_block)
|
|
54
|
+
schema = field(name, **kwargs) do
|
|
55
|
+
loads_from :command_line
|
|
56
|
+
positional
|
|
57
|
+
count 1
|
|
58
|
+
end
|
|
59
|
+
schema.class_exec(&user_block) if user_block
|
|
60
|
+
schema
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Declare a command the script can follow. A command has two essential parts:
|
|
64
|
+
# - a name that becomes an acceptable value for the implicit :command
|
|
65
|
+
# positional argument
|
|
66
|
+
# - a subconfig specific to the command (defined by █ see also
|
|
67
|
+
# ConfigSchemaLanguage)
|
|
68
|
+
# On a command line, after the :command is specified, all further values are
|
|
69
|
+
# interpreted within the named command's subconfig.
|
|
70
|
+
# If a command is declared with a default (see #field), the default must be
|
|
71
|
+
# :default. Such a declaration marks the default command if none is
|
|
72
|
+
# specified by the script inputs.
|
|
73
|
+
def command(name=nil, **kwargs, &block)
|
|
74
|
+
name, has_default, default_value = (
|
|
75
|
+
Internal::ConfigSchemaLanguageUtil.unpack_field_args(
|
|
76
|
+
name, kwargs, declaring: :command))
|
|
77
|
+
if has_default
|
|
78
|
+
command_is_default = true
|
|
79
|
+
if default_value != :default
|
|
80
|
+
raise Err::BadArgument, <<~END
|
|
81
|
+
a command can take no default value, only `:default` to mark which of
|
|
82
|
+
the commands is the default
|
|
83
|
+
END
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Initialize/extend the positional field that selects the command to
|
|
88
|
+
# include this new command.
|
|
89
|
+
arg :command do
|
|
90
|
+
type_must_be :enum
|
|
91
|
+
type { enum_symbols << name }
|
|
92
|
+
count 1
|
|
93
|
+
default name if command_is_default
|
|
94
|
+
|
|
95
|
+
toggles(Sources::CommandLine).command_selector = true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
subconfig_field = subconfig name
|
|
99
|
+
subconfig_field .toggles(Sources::CommandLine) .command_subconfig = true
|
|
100
|
+
subconfig_field::Config.class_exec(&block) if block
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Define a field that does not contain a value, but a subconfiguration with a
|
|
104
|
+
# distinct field namespace.
|
|
105
|
+
# &block defines that subconfig using the same language as the root config.
|
|
106
|
+
# (ConfigSchemaLanguage). However, behavioral differences vs root Config
|
|
107
|
+
# can be found in Subconfig.
|
|
108
|
+
# Only source definitions in the root config are used: do not modify sources
|
|
109
|
+
# in subconfigs. (Field#loads_from is unrelated and still works.)
|
|
110
|
+
def subconfig(name=nil, **kwargs, &block)
|
|
111
|
+
name, default_was_given, name_of_default = (
|
|
112
|
+
Internal::ConfigSchemaLanguageUtil.unpack_field_args(
|
|
113
|
+
name, kwargs, declaring: :subconfig))
|
|
114
|
+
|
|
115
|
+
# Get the appropriate snippet of FieldSchemaLanguage to set up the
|
|
116
|
+
# subconfig field's default value. This will be exec'd against the field's
|
|
117
|
+
# schema.
|
|
118
|
+
set_default = if not default_was_given
|
|
119
|
+
# never overwrite an explicit configuration implicitly
|
|
120
|
+
-> {
|
|
121
|
+
if not default_explicit?
|
|
122
|
+
default self::Config.new_defaulty
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
case name_of_default
|
|
127
|
+
when :required
|
|
128
|
+
-> { required }
|
|
129
|
+
when :unloaded
|
|
130
|
+
-> { default self::Config.new_unloaded }
|
|
131
|
+
when :defaulty
|
|
132
|
+
-> { default self::Config.new_defaulty }
|
|
133
|
+
else
|
|
134
|
+
raise Err::BadArgument, <<~END
|
|
135
|
+
Subconfig default must be :required, :unloaded or
|
|
136
|
+
:defaulty. If no default value is specified, and the
|
|
137
|
+
subconfig default has not been previously configured,
|
|
138
|
+
then it will be set like :defaulty.
|
|
139
|
+
END
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
superconfig = self
|
|
144
|
+
|
|
145
|
+
# At last, we can define the subconfig field itself.
|
|
146
|
+
|
|
147
|
+
field name do
|
|
148
|
+
type_must_be :subconfig
|
|
149
|
+
# Set up the schema class for the subconfig as FieldName::Config, if
|
|
150
|
+
# that hasn't been done already for this field name.
|
|
151
|
+
if not const_defined?(:Config, NO_INHERIT)
|
|
152
|
+
schema = Class.new(superconfig)
|
|
153
|
+
schema.const_set(:SUBCONFIG_NAME, name)
|
|
154
|
+
# It's not useful to have subconfigs behave exactly like the root
|
|
155
|
+
# config, so inject the appropriate behavioral differences. If we are
|
|
156
|
+
# making a sub-subconfig, then we don't need to add a duplicate copy of
|
|
157
|
+
# Subconfig to the ancestry.
|
|
158
|
+
#
|
|
159
|
+
# Testing `is_a? Subconfig` should not be done anywhere else: all the
|
|
160
|
+
# behavioral differences must be contained within the Subconfig module
|
|
161
|
+
# itself for clarity.
|
|
162
|
+
if !schema.is_a? Subconfig
|
|
163
|
+
schema.singleton_class.prepend Subconfig
|
|
164
|
+
schema.prepend Subconfig::InstanceMethods
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
const_set :Config, schema
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
self::Config.class_exec(&block) if block
|
|
171
|
+
|
|
172
|
+
self.instance_exec(&set_default)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Modify the named source to affect how values will be loaded into this
|
|
177
|
+
# config. If it's desired to define a source from scratch, use #new_source.
|
|
178
|
+
#
|
|
179
|
+
# See SourceSchemaLanguage for the statements available in source blocks.
|
|
180
|
+
def source(name, &block)
|
|
181
|
+
# While #sources does look in Mutable::DefaultSources, it does not look at
|
|
182
|
+
# builtins that are not default. In order to have non-default sources to
|
|
183
|
+
# use as templates without always also using them, we have to do some extra
|
|
184
|
+
# searching here ourselves.
|
|
185
|
+
handle_absent = -> {
|
|
186
|
+
const_name = NestingSchemaUtil.consty_name(name)
|
|
187
|
+
mod = Mutable::BuiltinSources
|
|
188
|
+
if mod.constants.include? const_name
|
|
189
|
+
# we can't even use normal lift since we're reaching outside of the
|
|
190
|
+
# schema's ancestry.
|
|
191
|
+
lifted = mod.const_get(const_name).clone
|
|
192
|
+
lifted.alias! name, under: self
|
|
193
|
+
source_add lifted
|
|
194
|
+
else
|
|
195
|
+
raise Err::NoSuchSource, <<~END
|
|
196
|
+
#{name} is not a source in #{self.name}, use new_source to define one
|
|
197
|
+
from scratch
|
|
198
|
+
END
|
|
199
|
+
end
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
target = NestingSchemaUtil.absent_local_or_remote(
|
|
203
|
+
name,
|
|
204
|
+
method(:sources),
|
|
205
|
+
absent: handle_absent,
|
|
206
|
+
local: -> { source_get name },
|
|
207
|
+
remote: -> { source_lift name },
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
target.class_exec(&block) if block
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Define a new source from scratch.
|
|
214
|
+
# See SourceSchemaLanguage for the callbacks a Source needs, and see
|
|
215
|
+
# Source::BaseBehaviors to see which ones your use will have to override.
|
|
216
|
+
#
|
|
217
|
+
# The new Source will be named <self.name>::Source::Name.
|
|
218
|
+
def new_source(name, &block)
|
|
219
|
+
# non-default builtin sources have to be handled specially. See also
|
|
220
|
+
# #source.
|
|
221
|
+
const_name = NestingSchemaUtil.consty_name(name)
|
|
222
|
+
builtins = Mutable::BuiltinSources
|
|
223
|
+
if builtins.constants.include? const_name
|
|
224
|
+
raise Err::NameCollision, <<~END
|
|
225
|
+
can't shadow built-in source #{const_name}, use `source :#{name}` to use
|
|
226
|
+
or reconfigure it
|
|
227
|
+
END
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# we can use the same lambda for local and remote
|
|
231
|
+
prevent_overwrite = -> {
|
|
232
|
+
raise Err::NameCollision, <<~END
|
|
233
|
+
#{name} already names the source #{source_get name}
|
|
234
|
+
END
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
target = NestingSchemaUtil.absent_local_or_remote(
|
|
238
|
+
name,
|
|
239
|
+
method(:sources),
|
|
240
|
+
absent: -> { source_add Source.nested_class!(name, under: self) },
|
|
241
|
+
local: prevent_overwrite,
|
|
242
|
+
remote: prevent_overwrite,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
target.class_exec(&block) if block
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Also load values from a config file. The config file path is taken either
|
|
249
|
+
# from the default given here, or from a value explicitly given on the
|
|
250
|
+
# command line via `--config-file`. Format is either the Source schema
|
|
251
|
+
# object that can load the file, or the name of the file type (:yaml for
|
|
252
|
+
# YamlFile). If format is nil, the file extension is assumed to be the file
|
|
253
|
+
# type name.
|
|
254
|
+
def file(default_path, format=nil)
|
|
255
|
+
config_schema = self
|
|
256
|
+
field_name = :config_file
|
|
257
|
+
source_schema =
|
|
258
|
+
Internal::ConfigSchemaLanguageUtil.resolve_file_source_schema(
|
|
259
|
+
default_path, format)
|
|
260
|
+
|
|
261
|
+
source :command_line do
|
|
262
|
+
path = [ *config_schema.subconfig_path, field_name ]
|
|
263
|
+
# XXX what would it mean if #file were called more than once?
|
|
264
|
+
toggles(source_schema).file_path_field = path.join('.')
|
|
265
|
+
end
|
|
266
|
+
field field_name do
|
|
267
|
+
type_must_be :string
|
|
268
|
+
default default_path
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Modify the named value type to affect how values will be loaded into this
|
|
273
|
+
# config. If it's desired to define a type from scratch, use #new_type.
|
|
274
|
+
#
|
|
275
|
+
# See TypeSchemaLanguage for the statements available in type blocks.
|
|
276
|
+
def type(name, &block)
|
|
277
|
+
target = NestingSchemaUtil.absent_local_or_remote(
|
|
278
|
+
name,
|
|
279
|
+
method(:types),
|
|
280
|
+
absent: -> {
|
|
281
|
+
raise Err::NoSuchType, <<~END
|
|
282
|
+
#{name} is not a type in #{self.name}, use new_type to define one from
|
|
283
|
+
scratch
|
|
284
|
+
END
|
|
285
|
+
},
|
|
286
|
+
local: -> { type_get name },
|
|
287
|
+
remote: -> { type_lift name },
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
target.class_exec(&block) if block
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Define a new value type from scratch.
|
|
294
|
+
# See TypeSchemaLanguage for the callbacks a Type can define and their
|
|
295
|
+
# meaning.
|
|
296
|
+
#
|
|
297
|
+
# The new Type will be named <self.name>::Type::Name.
|
|
298
|
+
def new_type(name, &block)
|
|
299
|
+
# we can use the same lambda for local and remote
|
|
300
|
+
prevent_overwrite = -> {
|
|
301
|
+
raise Err::NameCollision, <<~END
|
|
302
|
+
#{name} already names the type #{type_get name}
|
|
303
|
+
END
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
target = NestingSchemaUtil.absent_local_or_remote(
|
|
307
|
+
name,
|
|
308
|
+
method(:types),
|
|
309
|
+
absent: -> { type_add Type.nested_class!(name, under: self) },
|
|
310
|
+
local: prevent_overwrite,
|
|
311
|
+
remote: prevent_overwrite,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
target.class_exec(&block) if block
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# These methods are the statements used to define an individual field.
|
|
321
|
+
#
|
|
322
|
+
# The methods are added to Field as class methods.
|
|
323
|
+
# See Also: ConfigSchemaLanguage
|
|
324
|
+
module FieldSchemaLanguage
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
# Declare that the field must explicitly be given a value for the
|
|
328
|
+
# configuration to be valid.
|
|
329
|
+
def required; raw_set_default([]); end
|
|
330
|
+
|
|
331
|
+
# Set the value this field holds when no value is given explicitly.
|
|
332
|
+
def default(value)
|
|
333
|
+
# Infer the type if there is none
|
|
334
|
+
unless typed?
|
|
335
|
+
inferred = infer_value_type value
|
|
336
|
+
if inferred.nil?
|
|
337
|
+
raise Err::TypeUndefined, <<~END
|
|
338
|
+
The type of #{value.inspect} could not be inferred; to use this value
|
|
339
|
+
as a default, specify `type :<typename>` before `default <value>` in
|
|
340
|
+
the field block.
|
|
341
|
+
END
|
|
342
|
+
end
|
|
343
|
+
set_value_type inferred
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Infer the arity if there is none and the value is explicitly pluraloid.
|
|
347
|
+
plurality = value_type.plurality
|
|
348
|
+
if !arity_explicit? and plurality != nil and plurality.can_be?.(value)
|
|
349
|
+
set_arity plurality.atoms_of(value).count
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
raw_set_default [value]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Specify the attribute_names of the only kinds of source you want the field
|
|
356
|
+
# to potentially draw from. `loads_from :any` restores the default condition
|
|
357
|
+
# of allowing any source.
|
|
358
|
+
def loads_from(first, *rest)
|
|
359
|
+
case [first, *rest]
|
|
360
|
+
in [:any]
|
|
361
|
+
set_acceptable_sources nil
|
|
362
|
+
in [*source_names]
|
|
363
|
+
set_acceptable_sources source_names
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Specify the value type of this field.
|
|
368
|
+
# `name` selects a type from the config schema by its attribute_name. If a
|
|
369
|
+
# block is also passed, a copy of the named type is lifted and modified by
|
|
370
|
+
# block.
|
|
371
|
+
# If name is nil, then the field's type is modified. If the type is not
|
|
372
|
+
# internal to this field, a copy is lifted before modification. If the field
|
|
373
|
+
# is untyped, a blank internal type is defined before modification. This
|
|
374
|
+
# immediate type will be named <field>::Type::InternalType.
|
|
375
|
+
def type(name=nil, &block)
|
|
376
|
+
if name.nil? and block.nil?
|
|
377
|
+
raise Err::BadArgument, <<~END
|
|
378
|
+
use `type {}` if you really want to set a totally blank type
|
|
379
|
+
END
|
|
380
|
+
end
|
|
381
|
+
type = if name.nil? and not typed?
|
|
382
|
+
Type.nested_class!(:internal_type, under: self, &block)
|
|
383
|
+
elsif name.nil?
|
|
384
|
+
value_type
|
|
385
|
+
else
|
|
386
|
+
self::CONTAINING_CONFIG.type_get name
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
if block != nil
|
|
390
|
+
# If the Type is external to this Field, then it is not safe to mutate it
|
|
391
|
+
# and we must lift a copy for modification.
|
|
392
|
+
unless type.ruby_address.start_with? "#{ruby_address}::Type::"
|
|
393
|
+
sire = type
|
|
394
|
+
type = sire.clone
|
|
395
|
+
type.alias! sire.attribute_name, under: self
|
|
396
|
+
end
|
|
397
|
+
type.class_exec(&block)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
set_value_type type
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def type_must_be(name)
|
|
404
|
+
if typed?
|
|
405
|
+
return value_type if value_type.attribute_name == name
|
|
406
|
+
raise Err::UnsafeReopen, <<~END
|
|
407
|
+
will not treat #{value_type.attribute_name} #{self} as #{name};
|
|
408
|
+
is there a name collision?
|
|
409
|
+
END
|
|
410
|
+
else
|
|
411
|
+
type name
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Specify the single character symbol that this field may be referred to as
|
|
416
|
+
# in a command line, like `-f` for :f. The character must be in [a-zA-Z].
|
|
417
|
+
def short_flag(name=nil)
|
|
418
|
+
toggles(Sources::CommandLine).short_flag = (
|
|
419
|
+
name || self.attribute_name
|
|
420
|
+
).to_sym
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Sets the field to be treated as a positional argument in command lines.
|
|
424
|
+
def positional
|
|
425
|
+
toggles(Sources::CommandLine).positional = true
|
|
426
|
+
end
|
|
427
|
+
# Removes the positional argument distinction in command lines.
|
|
428
|
+
def not_positional
|
|
429
|
+
toggles(Sources::CommandLine).positional = false
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Indicate how many values of its type the field must get in order to be
|
|
433
|
+
# valid. The default nil means any number, with the most recent overwriting
|
|
434
|
+
# any previous values. An integer specifies an exact requirement, and a
|
|
435
|
+
# range specifies a range of acceptable numbers (including endless ranges for
|
|
436
|
+
# arbitrary varargs).
|
|
437
|
+
def count(spec); set_arity(spec); end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# These methods are the statements used to define an individual source.
|
|
442
|
+
# See Also: ConfigSchemaLanguage
|
|
443
|
+
#
|
|
444
|
+
# The blocks passed to these statements will override particular instance
|
|
445
|
+
# methods used at key points in the process of using a Source, allowing for
|
|
446
|
+
# arbitrary sources in a common interface. See the builtins defined in
|
|
447
|
+
# sources/ for examples.
|
|
448
|
+
#
|
|
449
|
+
# Notably, the various blocks of a Source may count on being able to read each
|
|
450
|
+
# other's instance variables since they'll be invoked against a common source
|
|
451
|
+
# object.
|
|
452
|
+
# See Also: Source::BaseBehaviors
|
|
453
|
+
module SourceSchemaLanguage
|
|
454
|
+
private
|
|
455
|
+
|
|
456
|
+
def initialize(&block)
|
|
457
|
+
SourceSchemaLanguage.enact self, :initialize, &block
|
|
458
|
+
end
|
|
459
|
+
alias_method :init, :initialize
|
|
460
|
+
|
|
461
|
+
# This block must return a boolean indicating whether the source is in a
|
|
462
|
+
# state where it can function correctly. A config file source has to have
|
|
463
|
+
# its path set, for example. ready? may be called multiple times in the
|
|
464
|
+
# course of loading a set of sources.
|
|
465
|
+
#
|
|
466
|
+
# Block may take one parameter: the list of already-completed source objects
|
|
467
|
+
# which may be used for interdependencies like a config file source looking
|
|
468
|
+
# for an optional path override from :command_line.
|
|
469
|
+
def ready?(&block)
|
|
470
|
+
SourceSchemaLanguage.enact self, :ready?, &block
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# This block is the opportunity to resolve dependencies on other sources,
|
|
474
|
+
# e.g. getting a config file path out of command line arguments. Called
|
|
475
|
+
# after ready? but before parse. The block may take one argument, the list
|
|
476
|
+
# of source objects that have already been parsed.
|
|
477
|
+
def cross_configure(&block)
|
|
478
|
+
SourceSchemaLanguage.enact self, :cross_configure, &block
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# This block must load all the data relevant to the source and prepare any
|
|
482
|
+
# data structures for answering fields' queries about the keys and values
|
|
483
|
+
# available. Block may take one argument, the config schema.
|
|
484
|
+
def parse(&block)
|
|
485
|
+
SourceSchemaLanguage.enact self, :parse, &block
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# This block must return a boolean indicating whether a given key has a value
|
|
489
|
+
# loaded by the source. Block may take one argument, the key.
|
|
490
|
+
# The key is a subconfig path: e.g. [ :foo, :bar, :some_field ]
|
|
491
|
+
def key?(&block)
|
|
492
|
+
SourceSchemaLanguage.enact self, :key?, &block
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# This block must return the appropriate value loaded by this source for the
|
|
496
|
+
# given key. If the source did not load a value for the key, it must raise
|
|
497
|
+
# KeyError. Block may take one argument, the key.
|
|
498
|
+
# The key is a subconfig path: e.g. [ :foo, :bar, :some_field ]
|
|
499
|
+
def get(&block)
|
|
500
|
+
SourceSchemaLanguage.enact self, :get, &block
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Copy the block to the appropriate method without raising warning for
|
|
504
|
+
# redefining a method since that's what we actually want.
|
|
505
|
+
def self.enact(target, name, &block)
|
|
506
|
+
target.remove_method(name) if target.instance_methods(false).include? name
|
|
507
|
+
target.define_method(name) do |*args|
|
|
508
|
+
self.instance_exec(*args, &block)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# These methods are the statements used to define an individual value type.
|
|
515
|
+
# See Also: ConfigSchemaLanguage
|
|
516
|
+
#
|
|
517
|
+
# The blocks passed to these statements will form the bodies of class methods
|
|
518
|
+
# on the schema to be called as needed in the processing of its containing
|
|
519
|
+
# fields.
|
|
520
|
+
#
|
|
521
|
+
# Unlike a source, there is no sharing of variables between the various blocks.
|
|
522
|
+
# Each function must be able to act based only on its arguments. Instance
|
|
523
|
+
# variables will be those of the Type schema and will be therby shared by all
|
|
524
|
+
# fields of the same type.
|
|
525
|
+
module TypeSchemaLanguage
|
|
526
|
+
private
|
|
527
|
+
|
|
528
|
+
# If specified, this block must produce a well-defined value that corresponds
|
|
529
|
+
# with the marshalled representation passed as the argument. If it cannot,
|
|
530
|
+
# it must raise Err::MarshalUnacceptable.
|
|
531
|
+
# In a command line, #parse enables specification of a field of this type in
|
|
532
|
+
# the form of `--field value` or `--field=value`, possibly among others.
|
|
533
|
+
def parse(&block)
|
|
534
|
+
TypeSchemaLanguage.enact self, :unmarshal, &block
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# If specified, this block must produce the implicit value for a field of
|
|
538
|
+
# this type. The block may take one argument: a unitary list containing the
|
|
539
|
+
# field's default value, or the empty list if there is none.
|
|
540
|
+
# In a command line, #affirm enables specification of a field of this type in
|
|
541
|
+
# the form of `--field`, possibly among others.
|
|
542
|
+
# Note that specifying an #affirm block on a type with a #parse block may
|
|
543
|
+
# restrict the ways in which an explicit value may be specified. In a
|
|
544
|
+
# command line, `--field value` becomes ambiguous and so only `--field=value`
|
|
545
|
+
# remains if both #parse and #affirm are specified.
|
|
546
|
+
def affirm(&block)
|
|
547
|
+
TypeSchemaLanguage.enact self, :affirmative, args: :lax, &block
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# If specified, this block must produce the implicit value for the negation
|
|
551
|
+
# of a field of this type. The block may take one argument: a unitary list
|
|
552
|
+
# containing the field's default value, or the empty list if there is none.
|
|
553
|
+
# In a command line, #negate enables specification of a field of this type in
|
|
554
|
+
# the form of `--no-field`.
|
|
555
|
+
def negate(&block)
|
|
556
|
+
TypeSchemaLanguage.enact self, :negatory, args: :lax, &block
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# If specified, this block must produce a boolean indicating whether the
|
|
560
|
+
# argument is an appropriate (unmarshalled) value for the type.
|
|
561
|
+
def recognize?(&block)
|
|
562
|
+
TypeSchemaLanguage.enact self, :recognize_atom?, &block
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Analogous to `recognize?` but stronger. For a type to claim a value is to
|
|
566
|
+
# say that value implies the type. Consider that many types may want to
|
|
567
|
+
# recognize nil as an acceptable value, but only String claims it.
|
|
568
|
+
#
|
|
569
|
+
# Any types in a config that specify this method will get an opportunity to
|
|
570
|
+
# claim untyped fields when the default value is set. See also #priority.
|
|
571
|
+
def claim?(&block)
|
|
572
|
+
TypeSchemaLanguage.enact self, :assert_claim, &block
|
|
573
|
+
end
|
|
574
|
+
# Shortcut for types that don't need to distinguish between recognition and
|
|
575
|
+
# claiming.
|
|
576
|
+
def claim_all_recognized; claim? { self === _1 }; end
|
|
577
|
+
|
|
578
|
+
# Influence the order in which Types are given the chance to claim a default
|
|
579
|
+
# field value as implying the Type. Builtins have priority 0. Any negative
|
|
580
|
+
# number, :early, or :high will cause a type to get a chance before the
|
|
581
|
+
# builtins. Any positive number, :late, or :low will guarantee that the
|
|
582
|
+
# builtins have a go first. :reset will return the type to priority 0.
|
|
583
|
+
def priority(value)
|
|
584
|
+
@priority = case value
|
|
585
|
+
in :reset
|
|
586
|
+
0
|
|
587
|
+
in :early | :high
|
|
588
|
+
-1000
|
|
589
|
+
in :late | :low
|
|
590
|
+
1000
|
|
591
|
+
in Numeric
|
|
592
|
+
value
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Specify how a type may accept multiple values. If spec is a symbol, its
|
|
597
|
+
# upcase must name a constant in Type::Plurality. Otherwise, spec is treated
|
|
598
|
+
# as a Plurality object itself. By default, types take multiple values
|
|
599
|
+
# :as_array.
|
|
600
|
+
def multivalue(spec)
|
|
601
|
+
block = if spec.is_a? Symbol
|
|
602
|
+
name = spec.upcase
|
|
603
|
+
unless Type::Plurality.constants(false).include? name
|
|
604
|
+
raise Err::ValuelessKey, <<~END
|
|
605
|
+
`#{name}` does not name a plurality
|
|
606
|
+
END
|
|
607
|
+
end
|
|
608
|
+
plurality = Type::Plurality.const_get name
|
|
609
|
+
-> { plurality }
|
|
610
|
+
else
|
|
611
|
+
-> { spec }
|
|
612
|
+
end
|
|
613
|
+
TypeSchemaLanguage.enact self, :plurality, &block
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Copy the block to the appropriate method without raising warning for
|
|
617
|
+
# redefining a method since that's what we actually want.
|
|
618
|
+
# If `args` is :strict, then the block must take exactly the appropriate
|
|
619
|
+
# arguments for the function. If it is :lax, then the block can take fewer
|
|
620
|
+
# arguments if it has no need for the input.
|
|
621
|
+
def self.enact(target, name, args: :strict, &block)
|
|
622
|
+
if target.singleton_methods(false).include? name
|
|
623
|
+
target.singleton_class.remove_method(name)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
case args
|
|
627
|
+
in :strict
|
|
628
|
+
target.define_singleton_method(name, &block)
|
|
629
|
+
in :lax
|
|
630
|
+
target.define_singleton_method(name) do |*args|
|
|
631
|
+
block.(*args)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
end
|
|
639
|
+
end
|