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,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 &block; 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