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,585 @@
1
+ require_relative '../schema'
2
+ require_relative 'util'
3
+
4
+ module Configuratrix
5
+ module Sources
6
+
7
+ class CommandLine < Source
8
+ init { @line_tokens = ARGV }
9
+ parse { |config_schema|
10
+ @value_map = Parser.parse(config_schema, @line_tokens.map(&:dup))
11
+ }
12
+
13
+ # Per-field configurations pertinent to command line parsing.
14
+ TOGGLES = [
15
+ # nil or a one-character symbol in [a-zA-Z]. A symbol maps, e.g., :x to
16
+ # "-x" on the command line.
17
+ :short_flag,
18
+ # For fields where :positional is true, the fields are given all command
19
+ # line values not associated with flags. The values are given to
20
+ # positional fields in declaration order successively as each field is
21
+ # satisfied.
22
+ :positional,
23
+ # At most one positional field per config may be marked as the command
24
+ # selector. When the command selector receives its value, command line
25
+ # parsing changes context from the current config to the named command's
26
+ # subconfig.
27
+ :command_selector,
28
+ # This field isn't the one that selected the command, it is one of the
29
+ # subconfigs associated with an available command.
30
+ :command_subconfig,
31
+ ]
32
+ class FieldToggles < Internal::ToggleStruct[*TOGGLES]
33
+ SHORT_FLAG = -> { /^[a-zA-Z]$/ =~ _1 and _1.is_a? Symbol }
34
+ def short_flag=(...); reject_unless super, matches: SHORT_FLAG; end
35
+
36
+ def positional=(...); reject_unless super, matches: A::BOOL; end
37
+ def command_selector=(...); reject_unless super, matches: A::BOOL; end
38
+ def command_subconfig=(...); reject_unless super, matches: A::BOOL; end
39
+ end
40
+
41
+ class Parser
42
+ def self.parse(config_schema,tokens)
43
+ new(config_schema).parse(tokens)
44
+ end
45
+
46
+ def initialize(config_schema)
47
+ @config_view = ConfigView.new(config_schema)
48
+ # source of tokens to parse
49
+ @stream = nil
50
+ end
51
+
52
+ def parse(tokens)
53
+ @stream = Internal::Stream.new(tokens.each)
54
+ # Whether tokens that look like flags should be treated like flags. This
55
+ # becomes false upon encountering the "--" token.
56
+ @flags_are_visible = true
57
+
58
+ parse_command_line
59
+
60
+ @config_view.finalize
61
+ @config_view.root[:value_map]
62
+ end
63
+
64
+ private
65
+
66
+ def parse_command_line
67
+ loop do
68
+ case peek
69
+ in []
70
+ return
71
+ in [LONG_FLAG] if @flags_are_visible
72
+ parse_long_flag
73
+ in [SHORT_FLAG] if @flags_are_visible
74
+ parse_short_flag
75
+ in [END_OF_FLAGS] if @flags_are_visible
76
+ take!
77
+ @flags_are_visible = false
78
+ in [String]
79
+ parse_positional
80
+ end
81
+ end
82
+ end
83
+
84
+ def parse_long_flag
85
+ raw_token = take!(:LONG_FLAG)
86
+ # remove the now functionless leading "--"
87
+ token = raw_token.sub(LONG_FLAG, '')
88
+
89
+ # handle "--key=value"
90
+ if token.include? SINGLE_TOKEN_KEY_VALUE_BOUNDARY
91
+ key, value_str = split_single_token_kv(token)
92
+
93
+ return unmarshal_and_store_for_field(
94
+ raw_token,
95
+ value_str,
96
+ (field_for key),
97
+ )
98
+ end
99
+
100
+ # If `token` wasn't a single-token key-value pair, then it's the key.
101
+
102
+ # Accommodate the convention of using the prefix "no-" to signal the
103
+ # negation of a long flag.
104
+ # Danger! If field keys aren't prevented from starting with "no-"
105
+ # themselves, then deleting NEGATORY_PREFIX creates ambiguity between
106
+ # "--no-chance" standing for negated "chance" or just "no-chance".
107
+ # Validation via #approve_field safely covers this sharp edge.
108
+ negated = (token.delete_prefix! NEGATORY_PREFIX) != nil
109
+ # Having to press shift for underscore is a big drag on operating a cli
110
+ # quickly, but dashes are illegal in field names.
111
+ token.gsub! '-', '_'
112
+ # Dashes in the key string couldn't have served any other purpose to this
113
+ # parser anyway, so it's safe to obliterate them unconditionally like
114
+ # this.
115
+
116
+ key = token.to_sym
117
+ field = field_for key
118
+
119
+ # Key's all set; on to value
120
+
121
+ if negated
122
+ # A negated flag must take on its type's negative value. Nothing else
123
+ # to do for this token.
124
+ if !field.value_type.respond_to? :negatory
125
+ raise Err::FieldNotApplicable, <<~END
126
+ '#{raw_token}' : #{field} can not be negated
127
+ END
128
+ end
129
+
130
+ value = field.value_type.negatory(field.list_of_default)
131
+ return store_for_field(raw_token, value, field)
132
+ end
133
+
134
+ # If the field's type has an implicit positive value, then we must apply
135
+ # that value now and be done with the token. If the type also allows
136
+ # explicit values, they must be provided via the --key=value format which
137
+ # has already been handled. These restrictions prevent ambiguity in the
138
+ # command line grammar.
139
+ # For example, in "--bool 0":
140
+ # - Is :bool being implicitly affirmed true followed by positional
141
+ # argument "0"?
142
+ # - Is :bool being explicitly declared false by the following "0" token?
143
+ # Better to resolve this now by always preferring the implicit option and
144
+ # leaving explicit values to --key=value.
145
+ if field.value_type.respond_to? :affirmative
146
+ value = field.value_type.affirmative(field.list_of_default)
147
+ return store_for_field(raw_token, value, field)
148
+ end
149
+
150
+ # By this point the value must be the next command line token.
151
+
152
+ unless field.value_type.respond_to? :unmarshal
153
+ # Die before unmarshal_and_store_for_field: here we have the additional
154
+ # context that :affirm was also tried.
155
+ raise Err::FieldNotApplicable, <<~END
156
+ '#{raw_token}' : #{field} can be neither set explicitly nor affirmed
157
+ implicitly
158
+ END
159
+ end
160
+
161
+ raw_token << " " << (value_str = take!(:VALUE))
162
+ unmarshal_and_store_for_field( raw_token, value_str, field )
163
+ end
164
+
165
+ def parse_short_flag
166
+ raw_token = take!(:SHORT_FLAG)
167
+ token = raw_token.sub(SHORT_FLAG, '')
168
+
169
+ # handle -k=value
170
+ if token.include? SINGLE_TOKEN_KEY_VALUE_BOUNDARY
171
+ key, value_str = split_single_token_kv(token)
172
+ # single-token short flags may not specify multiple flags at once.
173
+ if key.length > 1
174
+ raise Err::BadSyntax, <<~END
175
+ '#{raw_token}' : cannot combine multiple short flags and '='
176
+ assignment
177
+ END
178
+ end
179
+ field = field_for_short_flag key
180
+ return unmarshal_and_store_for_field( raw_token, value_str, field )
181
+ end
182
+
183
+ # If it wasn't a single-token key-value pair, then this token must just
184
+ # be the key(s).
185
+ case token.chars.map(&:to_sym)
186
+ in []
187
+ # no keys should never happen
188
+ raise Err::BadSyntax, <<~END
189
+ '#{raw_token}' : no short flags given
190
+ END
191
+ in [sole_key]
192
+ # One key by itself (e.g. `[:c]` from "-c") can represent fields with
193
+ # two potential value representations:
194
+ # - Implicit, where the flag receives a certain value that is
195
+ # particular to the field's Type.
196
+ # e.g. `[:c]` from "-c" affirming the boolean `:c` field to `true`.
197
+ # - Explicit, where the value is taken from the following line token.
198
+ # For fields that support implicit affirmation we will never take the
199
+ # following line token to be the field's explicit value.
200
+ #
201
+ # This prohibition eliminates ambiguity in the command line grammar.
202
+ # Fields can still be implicitly affirmable and work with explicit
203
+ # values via the -k=v syntax.
204
+ field = field_for_short_flag sole_key
205
+ if field.value_type.respond_to? :affirmative
206
+ value = field.value_type.affirmative(field.list_of_default)
207
+ store_for_field(raw_token, value, field)
208
+ else
209
+ # Only for non-affirmable types will we take the next token as value.
210
+ raw_token << " " << (value_str = take!(:VALUE))
211
+ unmarshal_and_store_for_field( raw_token, value_str, field )
212
+ end
213
+ in [*keys]
214
+ # with multiple keys, they all must be settable implicitly
215
+ fields = keys.map { field_for_short_flag _1 }
216
+ misplaced = fields.reject { _1.value_type.respond_to? :affirmative }
217
+ unless misplaced.empty?
218
+ bad_flags = misplaced.map {
219
+ "-#{_1.toggles(CommandLine).short_flag}"
220
+ } .join(" and ")
221
+ raise Err::FieldNotApplicable, <<~END
222
+ '#{raw_token}' : #{bad_flags} can't be specified without a value
223
+ END
224
+ end
225
+ # Set all fields named by `keys` to their implicit affirmative values.
226
+ fields.map do |field|
227
+ value = field.value_type.affirmative(field.list_of_default)
228
+ store_for_field(raw_token, value, field)
229
+ end
230
+ end
231
+ end
232
+
233
+ def parse_positional
234
+ raw_token = take!
235
+ positional_stack = @config_view[:positionals].stack
236
+ if positional_stack.empty?
237
+ raise Err::DanglingValue, <<~END
238
+ '#{raw_token}' : no further values expected
239
+ END
240
+ end
241
+ field = positional_stack.pop
242
+ unless @config_view[:multi_minder].saturated_after_next? field
243
+ positional_stack.push field
244
+ end
245
+
246
+ unmarshal_and_store_for_field( raw_token, raw_token, field )
247
+
248
+ if toggles_of(field).command_selector
249
+ selected_subconfig = @config_view[:value_map][field.attribute_name]
250
+ unselected_subconfigs = (
251
+ @config_view[:subconfig_fields].map(&:attribute_name)
252
+ .select { selected_subconfig != _1 })
253
+
254
+ # Removing the submaps for unselected subconfigs signals to
255
+ # Types::Subconfig not to load them.
256
+ @config_view[:value_map].reject! { |k,v|
257
+ unselected_subconfigs.include? k
258
+ }
259
+
260
+ @config_view.enter selected_subconfig
261
+ end
262
+ end
263
+
264
+ #
265
+ # Parsing Helpers
266
+ #
267
+
268
+ def peek; @stream.peek; end
269
+ def take; @stream.take; end
270
+
271
+ # Take a required value. If args are not empty, they are taken to be
272
+ # symbols naming consts in Recognizers, one of which must match the value.
273
+ def take!(*names)
274
+ case take
275
+ in []
276
+ suffix = names.join(' or ')
277
+ suffix = ", needed " + suffix unless suffix.empty?
278
+ raise Err::BadSyntax, <<~END
279
+ unexpected end of input#{suffix}
280
+ END
281
+ in [value]
282
+ if (names.empty? or
283
+ names.map { Recognizers.const_get _1 } .any? { _1 === value })
284
+ value
285
+ else
286
+ raise Err::BadSyntax, <<~END
287
+ unexpected #{value.inspect}, must be #{names.join(' or ')}
288
+ END
289
+ end
290
+ end
291
+ end
292
+
293
+ # properly split a token of the form "key=value"
294
+ def split_single_token_kv(token)
295
+ # If the delimiter appears more than once, the subsequent occurrences
296
+ # are just part of the value. So, split the token into at most 2
297
+ # pieces: the key; and the value, equalses and all.
298
+ key_str, value_str = token.split SINGLE_TOKEN_KEY_VALUE_BOUNDARY, 2
299
+ [key_str.to_sym, value_str]
300
+ end
301
+
302
+ # Fetch the schema for the named field, or raise since we've encountered a
303
+ # key not in the schema.
304
+ def field_for(key)
305
+ true_schema = @config_view[:schema]
306
+ field_name_in_context(key) do |field_name|
307
+ config_schema = @config_view[:schema]
308
+ if !config_schema.field_defined? field_name
309
+ raise Err::NoSuchField, <<~END
310
+ #{true_schema} has no field :#{key}
311
+ END
312
+ end
313
+ return config_schema.field_get field_name
314
+ end
315
+ end
316
+
317
+ # Call the supplied block with the field name from raw_key, after
318
+ # temporarily advancing the ConfigView context down any path encoded in
319
+ # raw_key, e.g. :'a.b.field' or :field
320
+ def field_name_in_context(raw_key, &block)
321
+ keys = raw_key .to_s .split('.') .map(&:to_sym)
322
+ path = keys[0..-2]
323
+ field = keys.last
324
+
325
+ @config_view.subcontext(path) do
326
+ block.call field.to_sym
327
+ end
328
+ end
329
+
330
+ def field_for_short_flag(short_key)
331
+ key = @config_view[:shorts_to_longs][short_key]
332
+ if key.nil?
333
+ raise Err::AmbiguousName, <<~END
334
+ short flag -#{short_key} doesn't stand for anything
335
+ END
336
+ end
337
+ field_for key
338
+ end
339
+
340
+ def unmarshal_and_store_for_field(raw_token, value_str, field)
341
+ unless field.value_type.respond_to? :unmarshal
342
+ raise Err::FieldNotApplicable, <<~END
343
+ '#{raw_token}' : #{field} cannot be given an explicit value
344
+ END
345
+ end
346
+ # Parse and set the value, giving a nicer error message on parse
347
+ # failure than the type can give by itself.
348
+ begin
349
+ store_for_field(raw_token, field.value_type.unmarshal(value_str), field)
350
+ rescue Err::MarshalUnacceptable
351
+ raise $!.exception("'#{raw_token}' : " +
352
+ "#{field} cannot accept '#{value_str}' as a value: #{$!}")
353
+ end
354
+ end
355
+
356
+ def store_for_field(raw_token, value, field)
357
+ @config_view[:multi_minder].append(value, field, human_context: raw_token)
358
+ end
359
+
360
+ def toggles_of(field); field.toggles(CommandLine); end
361
+
362
+ # Mediates the interaction between the parser and the config, allowing for
363
+ # subconfigs to be handled transparently to the parsing logic.
364
+ class ConfigView
365
+ attr_accessor :root
366
+
367
+ def initialize(config_schema)
368
+ @root = ConfigContext.new
369
+ @root[:schema] = config_schema
370
+ @root[:value_map] = {}
371
+ @root[:multi_minder] = Internal::MultiValueMinder.new(@root[:value_map])
372
+ build_tree @root
373
+
374
+ @context = @root
375
+ end
376
+
377
+ def enter(name)
378
+ @context = @context.children.fetch name
379
+ end
380
+
381
+ def leave; @context = @context.parent; end
382
+
383
+ def subcontext(path, &block)
384
+ path.each { enter _1 }
385
+ block.call
386
+ path.each { leave }
387
+ end
388
+
389
+ def finalize
390
+ all_contexts.each do |context|
391
+ CommandLine.validate_config_postconditions context
392
+ end
393
+ end
394
+
395
+ def build_tree(context)
396
+ context.survey
397
+ CommandLine.validate_config_preconditions context
398
+ # Recur on all fields with internal config schemata.
399
+ context[:schema].fields_schema.select {
400
+ _1.const_defined?(:Config, NO_INHERIT)
401
+ }.each do |field_schema|
402
+ subname = field_schema.attribute_name
403
+
404
+ child = ConfigContext.new
405
+ child.parent = context
406
+ child[:schema] = field_schema::Config
407
+ context[:value_map][subname] = {}
408
+ child[:value_map] = context[:value_map][subname]
409
+ child[:multi_minder] = Internal::MultiValueMinder.new(
410
+ child[:value_map])
411
+ context.children[subname] = child
412
+
413
+ build_tree child
414
+ end
415
+ end
416
+
417
+ def all_contexts
418
+ Enumerator.new do |output|
419
+ queue = [@root]
420
+ while !queue.empty? do
421
+ context = queue.pop
422
+ output.yield context
423
+ queue.prepend(*context.children.values)
424
+ end
425
+ end
426
+ end
427
+
428
+ def [](...); @context.[](...); end
429
+ def []=(...); @context.[]=(...); end
430
+
431
+ context_sensitive_values = [
432
+ :schema, # the schema of the config we're currently parsing into
433
+ :value_map, # the map into which newly parsed values should be put
434
+ :multi_minder, # manager of multi-value args, see MultiValueMinder
435
+ :positionals, # see Positionals
436
+ :shorts_to_longs, # map from 1-char short flags to full flag names
437
+ :command_field, # the field that selects the command
438
+ :subconfig_fields, # subconfigs for commands
439
+ ]
440
+ class ContextSensitives < Struct.new(*context_sensitive_values)
441
+ def initialize
442
+ self.positionals = Positionals.new
443
+ end
444
+ end
445
+
446
+ class ConfigContext < Struct.new(:parent, :payload, :children)
447
+ def initialize
448
+ self.payload = ContextSensitives.new
449
+ self.children = {}
450
+ end
451
+
452
+ def survey
453
+ survey = Internal::ToggleSurvey.new(self[:schema], FieldToggles)
454
+ fetch = -> { self[:schema].field_get _1 }
455
+ # declare survey plan
456
+ shorts_to_longs = survey.canvas_for(:short_flag).values_to_owners
457
+ command_field = (
458
+ survey.canvas_for(:command_selector).name.map(&fetch).last)
459
+ positionals_schema = survey.canvas_for(:positional).names.map(&fetch)
460
+ subconfig_fields = (
461
+ survey.canvas_for(:command_subconfig) .names .map(&fetch))
462
+
463
+ # execute and unpack results
464
+ survey.fetch_results
465
+ self[:shorts_to_longs] = ~shorts_to_longs
466
+ self[:command_field] = ~command_field
467
+ self[:positionals].schema = ~positionals_schema
468
+ self[:subconfig_fields] = ~subconfig_fields
469
+ end
470
+
471
+ # Brackets give quick access to payload values without having them
472
+ # intermixed with metadata like #parent.
473
+ def [](...); self.payload.[](...); end
474
+ def []=(...); self.payload.[]=(...); end
475
+ end
476
+
477
+ # Schema of all the positional arguments in a context along with the
478
+ # stack of the next positionals to receive parsed values.
479
+ class Positionals < Struct.new(:schema, :stack)
480
+ def initialize
481
+ self.schema = []
482
+ self.stack = []
483
+ end
484
+ def schema=(schema_list)
485
+ super
486
+ self.stack = schema_list.reverse
487
+ end
488
+ end
489
+ end
490
+
491
+
492
+ # 'normally' a flag key and value are separated by being in separate tokens
493
+ # (--key value), but also offering a non-tokenizing delimiter (--key=value)
494
+ # allows for greater flexibility (e.g. --testarg=--filter=foo).
495
+ SINGLE_TOKEN_KEY_VALUE_BOUNDARY = '='
496
+ # Specifying a field like `--no-field` asks the field's value type to
497
+ # produce its generic negative value.
498
+ NEGATORY_PREFIX = 'no-'
499
+
500
+ module Recognizers
501
+ # The types of command line tokens.
502
+ LONG_FLAG = /^--(?=.)/
503
+ SHORT_FLAG = /^-(?=[^-])/
504
+ END_OF_FLAGS = /^--$/
505
+ # Categorical
506
+ NON_VALUE = Regexp.union(LONG_FLAG, SHORT_FLAG, END_OF_FLAGS)
507
+ VALUE = -> { !(NON_VALUE === _1) }
508
+ end
509
+ include Recognizers
510
+ end
511
+
512
+ # Verify that the given field schema is not fundamentamentally incompatible
513
+ # with CommandLine parsing.
514
+ def self.approve_field(field_schema)
515
+ super
516
+ # If a field's name starts with :no_, then the convenience equivalence of
517
+ # dash and underscore makes this field ambiguous with the negation of a
518
+ # valid potential field name, e.g. :no_chance vs. negated :chance.
519
+ # Prohibiting fields that start with :no_ seems a small price to pay for a
520
+ # quick and non-bug-prone guarantee of unambiguity.
521
+ if field_schema.attribute_name.start_with? 'no_'
522
+ raise Err::PathologicalSchema, <<~END
523
+ field #{field_schema.name} conflicts with the `no-` negatory flag
524
+ prefix on the command line.
525
+ END
526
+ end
527
+ end
528
+
529
+ def self.validate_config_preconditions(context)
530
+ positionals_schema = context[:positionals].schema
531
+ # Positionals are inherently values without explicit keys; it doesn't
532
+ # make sense for positionals to only produce implicit values for explicit
533
+ # keys.
534
+ bads = positionals_schema.reject { _1.value_type.respond_to? :unmarshal }
535
+ unless bads.empty?
536
+ raise Err::FieldNotApplicable, <<~END
537
+ positional fields must take explicit values, unlike #{bads.join(', ')}
538
+ END
539
+ end
540
+ # It would be very confusing to get yelled at for a missing value in one
541
+ # positional, only to add a value and have it get consumed by a lurking
542
+ # optional before the required positional.
543
+ positionals_schema.each_cons(2) do |prior,later|
544
+ if !prior.required? and later.required?
545
+ raise Err::PathologicalSchema, <<~END
546
+ required positional #{later} cannot follow optional #{prior}
547
+ END
548
+ end
549
+ end
550
+ # If a positional field would never stop consuming values, then it must
551
+ # be the last positional.
552
+ first_endless = positionals_schema.find { _1.arity.endless? }
553
+ unless first_endless.nil? or first_endless == positionals_schema.last
554
+ inaccessibles = (positionals_schema.slice_after { first_endless == _1 }
555
+ .to_a.last.join(', '))
556
+ raise Err::PathologicalSchema, <<~END
557
+ #{first_endless} accepts any number of values, so #{inaccessibles}
558
+ will never receive values
559
+ END
560
+ end
561
+ # A command permanently changes out of the current config context, so any
562
+ # following positionals in this config are inaccessible.
563
+ command_field = context[:command_field]
564
+ unless command_field.nil? or command_field == positionals_schema.last
565
+ inaccessibles = (positionals_schema.slice_after(command_field)
566
+ .to_a.last.join(', '))
567
+ raise Err::PathologicalSchema, <<~END
568
+ positionals beyond the command selector will never receive values:
569
+ #{inaccessibles}
570
+ END
571
+ end
572
+ end
573
+
574
+ def self.validate_config_postconditions(context)
575
+ unless (bads = context[:multi_minder].incompletes).empty?
576
+ raise Err::IncompleteValue, <<~END
577
+ some fields were specified without enough values:
578
+ #{bads.join(', ')}
579
+ END
580
+ end
581
+ end
582
+ end
583
+
584
+ end
585
+ end
@@ -0,0 +1,60 @@
1
+ require_relative '../schema'
2
+
3
+ module Configuratrix
4
+ module Sources
5
+
6
+ class Environment < Source
7
+ ready? { schema .const_defined? :PREFIX }
8
+
9
+ init { @env = ENV.to_hash }
10
+ parse { |config_schema|
11
+ @value_map = config_schema.subconfig_tree
12
+ parse_config(config_schema)
13
+ }
14
+
15
+ def parse_config(config_schema)
16
+ target_map = [
17
+ @value_map, *config_schema.subconfig_path ] .reduce { _1 .fetch _2 }
18
+
19
+ # Parse all the leaf fields in this config.
20
+ config_schema .fields_schema .map {
21
+ [ env_key(config_schema, _1.attribute_name), _1 ]
22
+ } .select { |key,field|
23
+ @env.key? key and field.value_type.respond_to? :unmarshal
24
+ } .each do |key,field|
25
+ value_str = @env .fetch key
26
+ begin
27
+ target_map[field.attribute_name] = field.value_type.unmarshal(value_str)
28
+ rescue Err::MarshalUnacceptable
29
+ raise $!.exception "ENV[ #{key} ] : #{field} " +
30
+ "cannot accept '#{value_str}' as a value"
31
+ end
32
+ end
33
+
34
+ # Recur on subconfig fields.
35
+ config_schema .fields_schema .select {
36
+ _1.const_defined?(:Config, NO_INHERIT)
37
+ } .each do |field|
38
+ parse_config field::Config
39
+ end
40
+ @value_map
41
+ end
42
+
43
+ def env_key(config_schema, field_name)
44
+ (PREFIX.to_s +
45
+ [ *config_schema.subconfig_path, field_name ] .join('__')
46
+ ) .upcase .to_sym
47
+ end
48
+
49
+ # Set the prefix that will be prepended to field names when looking them up
50
+ # from the environment. Setting this constant enables loading from the
51
+ # environment, use PREFIX = '' if you want the variable names to be the same
52
+ # as the (UPCASED) field names.
53
+ def self.prefix(str)
54
+ remove_const :PREFIX if const_defined? :PREFIX
55
+ const_set :PREFIX, str
56
+ end
57
+ end
58
+
59
+ end
60
+ end