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,314 @@
|
|
|
1
|
+
require 'delegate'
|
|
2
|
+
|
|
3
|
+
module Configuratrix
|
|
4
|
+
module Internal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Module to prepend to the singleton class of an uninitialized config object to
|
|
8
|
+
# make it fail in a more descriptive way. Useful for the default of a
|
|
9
|
+
# non-required subconfig (via Config#new_unloaded).
|
|
10
|
+
module UnloadedConfig
|
|
11
|
+
def [](field_name)
|
|
12
|
+
if self.class.fields_schema.find { _1.attribute_name == field_name }
|
|
13
|
+
raise Err::NoSuchValue, <<~END
|
|
14
|
+
#{self.class} is unloaded; cannot access #{field_name}
|
|
15
|
+
END
|
|
16
|
+
end
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Module to prepend to the singleton class of an uninitialized config object to
|
|
23
|
+
# make it provide only default values for fields. Useful for the default of a
|
|
24
|
+
# non-required subconfig (via Config#new_defaulty).
|
|
25
|
+
module DefaultyConfig
|
|
26
|
+
def [](field_name)
|
|
27
|
+
schema_init unless instance_variable_defined? :@field_map
|
|
28
|
+
unless @field_map.include? field_name
|
|
29
|
+
field_schema = (
|
|
30
|
+
self.class.fields_schema.find { _1.attribute_name == field_name })
|
|
31
|
+
unless field_schema.nil?
|
|
32
|
+
@field_map[field_name] = field_schema.new
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
module SourceUtil
|
|
41
|
+
# Load each given source within a config schema, handling interdependencies.
|
|
42
|
+
def self.parse_all(sources, within:)
|
|
43
|
+
config_schema = within
|
|
44
|
+
todo = sources.dup
|
|
45
|
+
complete_sources = []
|
|
46
|
+
|
|
47
|
+
loop do
|
|
48
|
+
ready_sources, unready_sources = (
|
|
49
|
+
todo.partition { _1.ready? complete_sources } )
|
|
50
|
+
ready_sources.each do |source|
|
|
51
|
+
source.cross_configure complete_sources
|
|
52
|
+
source.parse config_schema
|
|
53
|
+
complete_sources << source
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if unready_sources == todo
|
|
57
|
+
# Progress has stopped. If we have any complete sources, that'll do.
|
|
58
|
+
break unless complete_sources.empty?
|
|
59
|
+
raise Err::PathologicalSchema, <<~END
|
|
60
|
+
sources did not become ready: #{unready_sources.map &:name}
|
|
61
|
+
END
|
|
62
|
+
end
|
|
63
|
+
todo = unready_sources
|
|
64
|
+
end
|
|
65
|
+
complete_sources .map { [ _1.name, _1 ] } .to_h
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# A range that keeps track of how many values a field may take. Ensures
|
|
71
|
+
# preconditions, normalizes range for easy comparison, and provides arity util
|
|
72
|
+
# methods.
|
|
73
|
+
class Arity < DelegateClass(Range)
|
|
74
|
+
private_class_method :new
|
|
75
|
+
|
|
76
|
+
# Validate, normalize, and internalize the given range as an arity.
|
|
77
|
+
def self.from_range(range)
|
|
78
|
+
lower = range.begin
|
|
79
|
+
unless Integer === lower and lower > 0
|
|
80
|
+
raise Err::BadArgument, <<~END
|
|
81
|
+
minimum arity must be a natural number, not #{lower}
|
|
82
|
+
END
|
|
83
|
+
end
|
|
84
|
+
upper = range.end || Float::INFINITY
|
|
85
|
+
unless (Integer === upper and upper >= lower) or upper == Float::INFINITY
|
|
86
|
+
raise Err::BadArgument, <<~END
|
|
87
|
+
invalid arity range #{lower} to #{upper}
|
|
88
|
+
END
|
|
89
|
+
end
|
|
90
|
+
upper -= 1 if range.exclude_end?
|
|
91
|
+
new Range.new(lower, upper, false)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Quickly make an arity with exactly one value.
|
|
95
|
+
def self.from_int(int)
|
|
96
|
+
from_range(int..int)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Allow arity to be compared to both ranges and to integers.
|
|
100
|
+
def ==(other)
|
|
101
|
+
case other
|
|
102
|
+
in nil
|
|
103
|
+
false
|
|
104
|
+
in Integer
|
|
105
|
+
self.begin == other and self.end == other
|
|
106
|
+
in Range
|
|
107
|
+
other_end = other.end || Float::INFINITY
|
|
108
|
+
other_end -= 1 if other.exclude_end?
|
|
109
|
+
self.begin == other.begin and self.end == other_end
|
|
110
|
+
in Arity
|
|
111
|
+
__getobj__ == other.__getobj__
|
|
112
|
+
else
|
|
113
|
+
raise Err::BadArgument, <<~END
|
|
114
|
+
comparison of Arity with #{other.inspect} failed
|
|
115
|
+
END
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
def !=(other); !(self == other); end
|
|
119
|
+
|
|
120
|
+
def satisfied_by?(count)
|
|
121
|
+
return true if unspecified?
|
|
122
|
+
include? count
|
|
123
|
+
end
|
|
124
|
+
def saturated_by?(count)
|
|
125
|
+
return false if unspecified?
|
|
126
|
+
count >= self.end
|
|
127
|
+
end
|
|
128
|
+
def unspecified?; self == Unspec; end
|
|
129
|
+
def demands_plurality?; self != Unspec and self != 1; end
|
|
130
|
+
def endless?; self.end.nil? or self.end == Float::INFINITY; end
|
|
131
|
+
|
|
132
|
+
def to_s
|
|
133
|
+
return '<unspecified>' if unspecified?
|
|
134
|
+
return self.begin.to_s if self.begin == self.end
|
|
135
|
+
super
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Unspec = new(nil..nil)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Class methods that facilitate defining nested trees of classes as with a
|
|
143
|
+
# configuration schema. Mix-in this module only with `extend`.
|
|
144
|
+
module NestingSchema
|
|
145
|
+
def self.extended(mod)
|
|
146
|
+
# To extend a class with NestingSchema is to say that the class's basename
|
|
147
|
+
# should also be a namespace for subclasses. Explicitly marking such
|
|
148
|
+
# classes with a constant allows possible further customization by
|
|
149
|
+
# subclassing without fragmenting the collection of classes under the
|
|
150
|
+
# nest's basename.
|
|
151
|
+
# See also #nest and its callers.
|
|
152
|
+
mod.const_set :NEST, mod
|
|
153
|
+
# It is useful for some nesting classes like Field to change the
|
|
154
|
+
# information given by #inspect, but there are times where #name will
|
|
155
|
+
# return nil but the address-based inspect gives useful information.
|
|
156
|
+
class << mod
|
|
157
|
+
alias :ruby_address :inspect
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Create a subclass of the current class, specified by block. If a name is
|
|
162
|
+
# given, the class will be set as a constant under base_namespace. The
|
|
163
|
+
# nesting path under base_namespace will be of the form Field::VitalVal for a
|
|
164
|
+
# class stemming from Field.nested_class!(:vital_val).
|
|
165
|
+
# Only the leaf of the parent class name is considered, so
|
|
166
|
+
# SomeModule::Field.nested_class! would place named subclasses in
|
|
167
|
+
# base_namespace::Field as well, not base_namespace::SomeModule::Field.
|
|
168
|
+
#
|
|
169
|
+
# Note: Being assigned to a constant for the first time is how Ruby
|
|
170
|
+
# classes get their internal names.
|
|
171
|
+
# Note: Ruby puts classes defined by class blocks into Object if that block is
|
|
172
|
+
# not already contained within a class or module block.
|
|
173
|
+
# Note: Here 'class block' and 'module block' refer only to blocks opened
|
|
174
|
+
# with `class <Classname>` or `module <Modulename>` since `Class.new`
|
|
175
|
+
# does not affect the constant nesting. See also Module::nesting.
|
|
176
|
+
def nested_class!(name=nil, under: Object, &block)
|
|
177
|
+
base_namespace = under
|
|
178
|
+
subclass = Class.new(self, &block)
|
|
179
|
+
unless name.nil?
|
|
180
|
+
subclass.alias!(name, under: base_namespace)
|
|
181
|
+
end
|
|
182
|
+
subclass
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Place create a recognizeable constant within base_namespace that refers to
|
|
186
|
+
# this class. Constants are of the form <base>::NestingBasename::Name. If
|
|
187
|
+
# no constant has previously been set to this class, then the full resulting
|
|
188
|
+
# name will also become the class's name.
|
|
189
|
+
# See Also: #nested_class!
|
|
190
|
+
def alias!(name=basename, under:Object)
|
|
191
|
+
base_namespace = under
|
|
192
|
+
# Put Fields in ::Field, etc.
|
|
193
|
+
nest_name = nest.const_name
|
|
194
|
+
unless base_namespace.constants(false).include? nest_name
|
|
195
|
+
base_namespace.const_set nest_name, Module.new
|
|
196
|
+
end
|
|
197
|
+
namespace = base_namespace.const_get nest_name
|
|
198
|
+
|
|
199
|
+
namespace.const_set NestingSchemaUtil.consty_name(name), self
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Access the portion of my schema that was specified as an extension of me by
|
|
203
|
+
# `other_class`.
|
|
204
|
+
def toggles(other_class)
|
|
205
|
+
our_kind_of_toggles = nest.const_get(:TOGGLE_TYPE, NO_INHERIT)
|
|
206
|
+
if !other_class.const_defined?(our_kind_of_toggles, NO_INHERIT)
|
|
207
|
+
raise Err::NoSuchValue, <<~END
|
|
208
|
+
#{other_class} is missing a
|
|
209
|
+
#{other_class}::#{our_kind_of_toggles} definition
|
|
210
|
+
END
|
|
211
|
+
end
|
|
212
|
+
their_toggles_for_us = other_class.const_get(our_kind_of_toggles)
|
|
213
|
+
toggle_registers[other_class] ||= their_toggles_for_us.new
|
|
214
|
+
end
|
|
215
|
+
def toggle_registers; @toggle_registers ||= {}; end
|
|
216
|
+
|
|
217
|
+
# Get this class's nest. The nest is the class under whose basename all
|
|
218
|
+
# subclasses are to be organized, e.g. ::Field::SomeField where Field was
|
|
219
|
+
# `extend`ed by NestingSchema.
|
|
220
|
+
def nest; self::NEST; end
|
|
221
|
+
|
|
222
|
+
# Get only the leaf name of the class, e.g. A from the class Foo::Bar::A.
|
|
223
|
+
def basename; name.split('::').last; end
|
|
224
|
+
|
|
225
|
+
def const_name; basename.to_sym; end
|
|
226
|
+
|
|
227
|
+
def attribute_name; NestingSchemaUtil.attry_name(basename); end
|
|
228
|
+
|
|
229
|
+
# These kinds of classes are often defined with do-blocks and so may not know
|
|
230
|
+
# their names at the point an error is raised. But, since the base classes
|
|
231
|
+
# this module extends are defined in class blocks, we know someone up the
|
|
232
|
+
# chain has a useful basename.
|
|
233
|
+
def name; super || "<anonymous #{nest.basename}>"; end
|
|
234
|
+
|
|
235
|
+
# #succ allows schema classes to be used in ranges
|
|
236
|
+
def succ; superclass; end
|
|
237
|
+
|
|
238
|
+
# spaceship allows schema classes to be used in ranges
|
|
239
|
+
def <=>(other)
|
|
240
|
+
return -1 if self < other
|
|
241
|
+
return 0 if self == other
|
|
242
|
+
return 1 if self > other
|
|
243
|
+
raise "can't compare #{self} to unrelated #{other}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Companion methods to include into the extended class to allow objects to
|
|
247
|
+
# more easily interact with their schemas.
|
|
248
|
+
module InstanceMethods
|
|
249
|
+
def schema; self.class; end
|
|
250
|
+
|
|
251
|
+
def name; schema.attribute_name; end
|
|
252
|
+
|
|
253
|
+
def type; schema.name; end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
module NestingSchemaUtil
|
|
259
|
+
# Search for name among the members of lister, then call the appropriate
|
|
260
|
+
# lambda based on the situation.
|
|
261
|
+
# lister must take a boolean specifying whether to include inherited values
|
|
262
|
+
# in the list or not. This distinction allows for the separation of local
|
|
263
|
+
# and remote values.
|
|
264
|
+
def self.absent_local_or_remote(name, lister, absent:, local:, remote:)
|
|
265
|
+
if !lister.(true).include? name
|
|
266
|
+
absent.()
|
|
267
|
+
elsif lister.(false).include? name
|
|
268
|
+
local.()
|
|
269
|
+
else
|
|
270
|
+
remote.()
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# #send :message to each class in class_range and `+` the resulting values
|
|
275
|
+
# all together. If base_case is not nil, the result of calling it without
|
|
276
|
+
# arguments is added at the end.
|
|
277
|
+
# Classes aren't normally usable in ranges, but NestingSchema classes form
|
|
278
|
+
# ranges by superclassing toward an ancestor.
|
|
279
|
+
def self.roll_up(message, class_range, base_case: nil)
|
|
280
|
+
value = class_range .map { _1.send message } .reduce :+
|
|
281
|
+
|
|
282
|
+
unless base_case.nil?
|
|
283
|
+
value += base_case.call
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
value
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Turn an :attribute_like symbol into a :ConstLike symbol.
|
|
290
|
+
def self.consty_name(name)
|
|
291
|
+
name .to_s .downcase .split('_') .map(&:capitalize) .join .to_sym
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Turn a :ConstLike symbol into an :attribute_like symbol.
|
|
295
|
+
def self.attry_name(name)
|
|
296
|
+
name .to_s .gsub(/([a-z])([A-Z])/, '\1_\2') .downcase .to_sym
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
class HookedArray < DelegateClass(Array)
|
|
301
|
+
def initialize(&block)
|
|
302
|
+
super []
|
|
303
|
+
@hook = block
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def <<(new_entry)
|
|
307
|
+
@hook.call new_entry
|
|
308
|
+
super new_entry
|
|
309
|
+
self
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
end
|
|
314
|
+
end
|