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,303 @@
|
|
|
1
|
+
module Configuratrix
|
|
2
|
+
module Internal
|
|
3
|
+
|
|
4
|
+
# Wrapper for an enumeration that exchanges raising StopIteration when no
|
|
5
|
+
# values remain to returning a list that is empty if the stream was exhausted,
|
|
6
|
+
# or a list containing only the resulting value from the stream.
|
|
7
|
+
class Stream
|
|
8
|
+
def initialize(enumeration)
|
|
9
|
+
@enumeration = enumeration
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def take; get_via(:next); end
|
|
13
|
+
|
|
14
|
+
def peek; get_via(:peek); end
|
|
15
|
+
|
|
16
|
+
def raw; @enumeration; end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def get_via(message)
|
|
21
|
+
[@enumeration.public_send(message)]
|
|
22
|
+
rescue StopIteration
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Keep track of the pending partial values for a collection of fields, and
|
|
28
|
+
# handle the addition of subsequent values according to the relevant field's
|
|
29
|
+
# schema.
|
|
30
|
+
class MultiValueMinder
|
|
31
|
+
attr_accessor :value_map
|
|
32
|
+
|
|
33
|
+
def initialize(value_map = {})
|
|
34
|
+
# A read-only view into the current values of fields. Used when computing
|
|
35
|
+
# a new combination with a new given value.
|
|
36
|
+
@value_map = value_map
|
|
37
|
+
# The number of values a named field has been observed receiving.
|
|
38
|
+
@value_counts = Hash.new 0
|
|
39
|
+
# The observed arities of the fields that pass through. Powers
|
|
40
|
+
# #incompletes.
|
|
41
|
+
@arities = Hash.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def satisfied?(field)
|
|
45
|
+
field.arity.satisfied_by? @value_counts[field.attribute_name]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def saturated_after_next?(field)
|
|
49
|
+
field.arity.saturated_by? (@value_counts[field.attribute_name] + 1)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def incompletes
|
|
53
|
+
@value_counts.filter_map do |name,count|
|
|
54
|
+
name unless @arities[name].satisfied_by? count
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Add value to the total value of field, returning the new combined value.
|
|
59
|
+
# If the arity is not plural, the value will be returned without
|
|
60
|
+
# modification.
|
|
61
|
+
def append(value, field, human_context: nil)
|
|
62
|
+
name = field.attribute_name
|
|
63
|
+
arity = field.arity
|
|
64
|
+
@arities[name] = arity
|
|
65
|
+
|
|
66
|
+
unless arity.unspecified?
|
|
67
|
+
# An explicit arity must have room left for values.
|
|
68
|
+
if arity.saturated_by? @value_counts[name]
|
|
69
|
+
raise Err::DanglingValue, <<~END
|
|
70
|
+
'#{human_context}' : #{field} cannot accept more than
|
|
71
|
+
#{@value_counts[name]} value(s)
|
|
72
|
+
END
|
|
73
|
+
end
|
|
74
|
+
@value_counts[name] += 1
|
|
75
|
+
# Explicit arities other than 1 require plurality handling.
|
|
76
|
+
if arity.demands_plurality?
|
|
77
|
+
plurality = field.value_type.plurality
|
|
78
|
+
value = plurality.wrap_atom.(value)
|
|
79
|
+
if @value_map.include? name
|
|
80
|
+
value = plurality.combine.(@value_map[name], value)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@value_map[field.attribute_name] = value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Functionality common to all sources for discovering individual schema class'
|
|
90
|
+
# preferences about their representation within the source. Do not include
|
|
91
|
+
# this module in classes directly; use only subclasses of ToggleStruct[].
|
|
92
|
+
# See also Sources::CommandLine::FieldToggles.
|
|
93
|
+
module ToggleStruct
|
|
94
|
+
# Create a customized struct with the named entries and functionality
|
|
95
|
+
# relevant to all toggle register classes.
|
|
96
|
+
def self.[](*entry_names)
|
|
97
|
+
Struct.new(*entry_names) do
|
|
98
|
+
def self.inherited(subclass)
|
|
99
|
+
subclass.const_set :Struct, self
|
|
100
|
+
end
|
|
101
|
+
include ToggleStruct
|
|
102
|
+
extend ToggleStruct::ClassMethods
|
|
103
|
+
# This is the back door to writing values without validation, since
|
|
104
|
+
# validation is done by overriding specific member= methods.
|
|
105
|
+
private :[]=
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
module ClassMethods
|
|
109
|
+
# Get a proc to extract the value of the named toggle from any schema
|
|
110
|
+
# objects passed to the proc. The schema object must respond to
|
|
111
|
+
# :toggles(class) with a the approriate ToggleStruct for the given class.
|
|
112
|
+
def getter_of(name)
|
|
113
|
+
unless members.include? name
|
|
114
|
+
raise "BUG: #{name} is not a member of #{self}"
|
|
115
|
+
end
|
|
116
|
+
# Now we return the getter that will ask any schema object for its
|
|
117
|
+
# toggles relevant to our domain, so we can get the `name`d object out of
|
|
118
|
+
# those toggles. a.k.a. the getter of name
|
|
119
|
+
-> schema {
|
|
120
|
+
unless schema.respond_to? :toggles
|
|
121
|
+
raise "BUG: #{schema} does not support config toggles"
|
|
122
|
+
end
|
|
123
|
+
register = schema.toggles(domain) || self.new
|
|
124
|
+
unless self === register
|
|
125
|
+
raise "BUG: #{self} asked to operate on #{register.class}"
|
|
126
|
+
end
|
|
127
|
+
register.public_send name
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# self is named SomeSuchClass::SomeSuchToggles; get me SomeSuchClass!
|
|
132
|
+
def domain
|
|
133
|
+
(self
|
|
134
|
+
.name
|
|
135
|
+
.split('::')[0..-2]
|
|
136
|
+
.reduce(Object) { |mod,class_name| mod.const_get(class_name) }
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# It only makes sense to use mutation wrappers under an entry= method.
|
|
142
|
+
ENTRY_MUTATOR = /^([a-zA-Z0-9_-]+)=$/
|
|
143
|
+
|
|
144
|
+
# Unless `matches === value`, raise NoCanToggle. Otherwise, return value.
|
|
145
|
+
def reject_unless(value, matches:)
|
|
146
|
+
asker = caller.first[/`(.*)'/,1]
|
|
147
|
+
unless ENTRY_MUTATOR =~ asker
|
|
148
|
+
raise "BUG: reject_unless must be called from a struct member= method"
|
|
149
|
+
end
|
|
150
|
+
key = asker[ENTRY_MUTATOR,1].to_sym
|
|
151
|
+
|
|
152
|
+
if matches === value
|
|
153
|
+
value
|
|
154
|
+
else
|
|
155
|
+
self[key] = nil
|
|
156
|
+
raise Err::NoCanToggle, <<~END
|
|
157
|
+
#{self.class}: can't set :#{key} to #{value.inspect}
|
|
158
|
+
END
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Common toggle validators: "matches A something"
|
|
163
|
+
module A
|
|
164
|
+
BOOL = -> { false == _1 or true == _1 }
|
|
165
|
+
SYMBOL = -> { _1 .is_a? Symbol }
|
|
166
|
+
STRING = -> { _1 .is_a? String }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Bring together the knowledge required to find out what, if anything, all the
|
|
171
|
+
# fields in a config schema have a toggle set to.
|
|
172
|
+
class ToggleSurvey
|
|
173
|
+
def initialize(config_schema, register_class)
|
|
174
|
+
@config_schema = config_schema
|
|
175
|
+
@register_class = register_class
|
|
176
|
+
@toggles_prefix =
|
|
177
|
+
@register_class .name .split("::") .last .delete_suffix "Toggles"
|
|
178
|
+
configure_for @toggles_prefix
|
|
179
|
+
@canvas_orders = []
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def canvas_for(name)
|
|
183
|
+
pending_result = PendingResult.new
|
|
184
|
+
@canvas_orders << CanvasOrder.new(
|
|
185
|
+
name, @register_class.getter_of(name), pending_result)
|
|
186
|
+
pending_result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def fetch_results
|
|
190
|
+
# fold each object's toggle values into the prehash (list of k,v pairs) for
|
|
191
|
+
# the relevant order.
|
|
192
|
+
@population_schema.reduce(@canvas_orders) { |orders,schema_object|
|
|
193
|
+
orders.each { |order|
|
|
194
|
+
value = order.getter.call(schema_object)
|
|
195
|
+
order.prehash << [ schema_object.attribute_name, value ] if value
|
|
196
|
+
order
|
|
197
|
+
}
|
|
198
|
+
} .each { |filled_order|
|
|
199
|
+
# Take the newly constructed prehashes and return results to clients.
|
|
200
|
+
results = Results.new(filled_order.prehash, filled_order.topic,
|
|
201
|
+
@population_label, @config_schema)
|
|
202
|
+
filled_order .pending_result .resolve_pending_result results
|
|
203
|
+
}
|
|
204
|
+
@canvas_orders = []
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def configure_for(population_type)
|
|
210
|
+
case @toggles_prefix
|
|
211
|
+
in "Field"
|
|
212
|
+
@population_label = "fields"
|
|
213
|
+
@population_schema = @config_schema.fields_schema
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
CanvasOrder = Struct.new(:topic, :getter, :pending_result, :prehash) do
|
|
218
|
+
def initialize(topic, getter, pending_result, prehash=[]); super; end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
class Results
|
|
222
|
+
def initialize(prehash, toggle_surveyed, population, config_schema)
|
|
223
|
+
@prehash = prehash
|
|
224
|
+
@toggle_surveyed = toggle_surveyed
|
|
225
|
+
@population = population
|
|
226
|
+
@config_schema = config_schema
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def names; @prehash.filter_map { |k,v| k if v }; end
|
|
230
|
+
def name
|
|
231
|
+
names = self.names
|
|
232
|
+
return names if names.size <= 1
|
|
233
|
+
raise Err::NameCollision, <<~END
|
|
234
|
+
#{@toggle_surveyed} must select a single member of #{@population}
|
|
235
|
+
of #{@config_schema}, but it selects #{names}
|
|
236
|
+
END
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def names_to_values; @prehash.to_h; end
|
|
240
|
+
|
|
241
|
+
def names_holding(t); @prehash.filter_map { |k,v| k if t == v }; end
|
|
242
|
+
|
|
243
|
+
# For every value a toggle takes in a population, a list of the members
|
|
244
|
+
# that hold that value.
|
|
245
|
+
def value_clusters; invert(false); end
|
|
246
|
+
# See #value_clusters, plus the assertion that every value is associated
|
|
247
|
+
# with exactly one key.
|
|
248
|
+
def values_to_owners; invert(true); end
|
|
249
|
+
|
|
250
|
+
def invert(strict)
|
|
251
|
+
(@prehash
|
|
252
|
+
# Hash#group_by groups all the [k,v] pairs with a common result.
|
|
253
|
+
.group_by { |name,value| value }
|
|
254
|
+
# We have `v => [ [k,v], [k,v], ...]`, but we need `v => [k, k, ...]`.
|
|
255
|
+
.transform_values { _1.map { |name,value| name } }
|
|
256
|
+
# Now we have a hash from values to all the names that have that value
|
|
257
|
+
# for @toggle_surveyed
|
|
258
|
+
.map { |value,common_names|
|
|
259
|
+
case common_names
|
|
260
|
+
in [ name ]
|
|
261
|
+
# If we're not being strict, we need list values in all cases.
|
|
262
|
+
name = [ name ] if not strict
|
|
263
|
+
# If only one key points to a certain value, then that value can
|
|
264
|
+
# safely be a key for just that former key.
|
|
265
|
+
[value, name]
|
|
266
|
+
in [ *names ]
|
|
267
|
+
if strict
|
|
268
|
+
raise Err::NameCollision, <<~END
|
|
269
|
+
#{@toggle_surveyed} must be unique among #{@population}
|
|
270
|
+
of #{@config_schema}, but #{names} share #{value}
|
|
271
|
+
END
|
|
272
|
+
end
|
|
273
|
+
[value, names]
|
|
274
|
+
end
|
|
275
|
+
}
|
|
276
|
+
.to_h
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
class PendingResult
|
|
283
|
+
def initialize; @queue = []; end
|
|
284
|
+
|
|
285
|
+
def method_missing(name, *args, &block)
|
|
286
|
+
child = self.class.new
|
|
287
|
+
@queue << [ name, args, block, child ]
|
|
288
|
+
child
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def resolve_pending_result(resolution)
|
|
292
|
+
@resolved = resolution
|
|
293
|
+
@queue.each do |name,args,block,child|
|
|
294
|
+
child.resolve_pending_result resolution.send(name,*args,&block)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# unary tilde unwraps the resolved value
|
|
299
|
+
def ~; @resolved; end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
end
|
|
303
|
+
end
|