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,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