versionomy 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,951 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Versionomy delimiter format
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2008-2009 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ module Versionomy
38
+
39
+ module Format
40
+
41
+
42
+ # The Delimiter format class provides a DSL for building formats that
43
+ # can handle most cases where the fields of a version number appear
44
+ # consecutively in order in the string formatting. We expect most
45
+ # version number schemes should fall into this category.
46
+ #
47
+ # In general, the strategy is to provide, for each field, a set of
48
+ # regular expressions that recognize different formats for that field.
49
+ # Every field must be of the form "(pre)(value)(post)"
50
+ # where (pre) and (post) are delimiters preceding and
51
+ # following the value. Either or both delimiters may be the empty string.
52
+ #
53
+ # To parse a string, the string is scanned from left to right and
54
+ # matched against the format for the fields in order. If the string
55
+ # matches, that part of the string is consumed and the field value is
56
+ # interpreted from it. If the string does not match, and the field is
57
+ # not marked as "required", then the field is set to its default value
58
+ # and the next field is tried.
59
+ #
60
+ # During parsing, the actual delimiters, along with other information
61
+ # such as whether or not fields are required, are saved into a default
62
+ # set of parameters for unparsing. These are saved in the unparse_params
63
+ # of the version value, so that the version number can be unparsed in
64
+ # generally the same form. If the version number value is modified, this
65
+ # allows the unparsing of the new value to generally follow the format
66
+ # of the original string.
67
+ #
68
+ # Formats that use the Delimiter mechanism also provide support for
69
+ # certain parsing and unparsing parameters. See the documentation for
70
+ # the parse and unparse methods for details.
71
+ #
72
+ # For a usage example, see the definition of the standard format in
73
+ # Versionomy::Formats#_create_standard.
74
+
75
+ class Delimiter
76
+
77
+
78
+ # Create a format using delimiter tools.
79
+ # You should provide the version number schema, a set of default
80
+ # options, and a block.
81
+ #
82
+ # Within the block, you can call methods of
83
+ # Versionomy::Format::Delimiter::Builder
84
+ # to provide parsers for the fields of the schema. Any fields you do
85
+ # not explicitly configure will get parsed in a default manner.
86
+
87
+ def initialize(schema_, default_opts_={}, &block_)
88
+ # Special case used by modified_copy
89
+ if schema_.kind_of?(Delimiter)
90
+ orig_ = schema_
91
+ @schema = orig_.schema
92
+ @default_parse_params = orig_.default_parse_params
93
+ @default_unparse_params = orig_.default_unparse_params
94
+ @field_handlers = orig_.instance_variable_get(:@field_handlers).dup
95
+ builder_ = Delimiter::Builder.new(@schema, @field_handlers,
96
+ @default_parse_params, @default_unparse_params)
97
+ ::Blockenspiel.invoke(block_, builder_)
98
+ return
99
+ end
100
+
101
+ @schema = schema_
102
+ @field_handlers = {}
103
+ @default_parse_params = {}
104
+ @default_unparse_params = {}
105
+ builder_ = Delimiter::Builder.new(@schema, @field_handlers,
106
+ @default_parse_params, @default_unparse_params)
107
+ ::Blockenspiel.invoke(block_, builder_)
108
+ _interpret_field_lists(@default_unparse_params)
109
+ @schema.names.each do |name_|
110
+ @field_handlers[name_] ||= Delimiter::FieldHandler.new(@schema.field_named(name_), default_opts_)
111
+ end
112
+ end
113
+
114
+
115
+ # Returns the schema understood by this format.
116
+ # This method is required by the Format contract.
117
+
118
+ def schema
119
+ @schema
120
+ end
121
+
122
+
123
+ # Parse the given string and return a value.
124
+ # This method is required by the Format contract.
125
+ #
126
+ # This method provides, out of the box, support for the following
127
+ # parse parameters:
128
+ #
129
+ # <tt>:extra_characters</tt>::
130
+ # Determines what to do if the entire string cannot be consumed by
131
+ # the parsing process. If set to <tt>:ignore</tt> (the default),
132
+ # any extra characters are ignored. If set to <tt>:suffix</tt>,
133
+ # the extra characters are set as the <tt>:suffix</tt> unparse
134
+ # parameter and are thus appended to the end of the string when
135
+ # unparsing takes place. If set to <tt>:error</tt>, causes a
136
+ # Versionomy::Errors::ParseError to be raised if there are
137
+ # uninterpreted characters.
138
+
139
+ def parse(string_, params_=nil)
140
+ values_ = {}
141
+ parse_params_ = default_parse_params
142
+ parse_params_.merge!(params_) if params_
143
+ parse_params_[:string] = string_
144
+ parse_params_[:previous_field_missing] = false
145
+ unparse_params_ = {}
146
+ field_ = @schema.root_field
147
+ while field_
148
+ handler_ = @field_handlers[field_.name]
149
+ v_ = handler_.parse(parse_params_, unparse_params_)
150
+ values_[field_.name] = v_
151
+ field_ = field_.child(v_)
152
+ end
153
+ if parse_params_[:string].length > 0
154
+ case parse_params_[:extra_characters]
155
+ when :error
156
+ raise Errors::ParseError, "Extra characters: #{parse_params_[:string].inspect}"
157
+ when :suffix
158
+ unparse_params_[:suffix] = parse_params_[:string]
159
+ end
160
+ end
161
+ Value.new(values_, self, unparse_params_)
162
+ end
163
+
164
+
165
+ # Unparse the given value and return a string.
166
+ # This method is required by the Format contract.
167
+ #
168
+ # This method provides, out of the box, support for the following
169
+ # unparse parameters:
170
+ #
171
+ # <tt>:suffix</tt>::
172
+ # A string to append to the unparsed string. Default is nothing.
173
+ # <tt>:required_fields</tt>::
174
+ # An array of field names that must be present in the unparsed
175
+ # string. These are generally fields with default_value_optional
176
+ # set, but that we want present in the string anyway. For example,
177
+ # in the version number "2.0.0", often the third field will be
178
+ # default_value_optional, but we can include it in the required
179
+ # fields passed to unparse to force it to appear in the string.
180
+ # <tt>:optional_fields</tt>::
181
+ # An array of field names that should have their presence in
182
+ # required_fields undone.
183
+ # <tt>:<i>fieldname</i>_required</tt>::
184
+ # This is an alternate way of specifying whether a potentially
185
+ # optional field should be required. Accepted values are true
186
+ # and false.
187
+ # <tt>:<i>fieldname</i>_style</tt>::
188
+ # Specify the style for unparsing the given field. See
189
+ # Versionomy::Format::Delimiter::Builder#field for more
190
+ # discussion of styles.
191
+ # <tt>:<i>fieldname</i>_delim</tt>::
192
+ # Set the pre-delimiter for the given field, if supported.
193
+ # Note that the string specified must be legal-- it must match the
194
+ # regexp for the field. If not, it will revert to the default.
195
+ # <tt>:<i>fieldname</i>_postdelim</tt>::
196
+ # Set the post-delimiter for the given field, if supported.
197
+ # Note that the string specified must be legal-- it must match the
198
+ # regexp for the field. If not, it will revert to the default.
199
+ # <tt>:<i>fieldname</i>_case</tt>::
200
+ # This is used by letter-formatted integer fields only, and
201
+ # sets the case to use while unparsing. Recognized values are
202
+ # <tt>:lower</tt> (the default), and <tt>:upper</tt>.
203
+
204
+ def unparse(value_, params_=nil)
205
+ unparse_params_ = value_.unparse_params || default_unparse_params
206
+ _interpret_field_lists(unparse_params_)
207
+ if params_
208
+ unparse_params_.merge!(params_)
209
+ _interpret_field_lists(unparse_params_)
210
+ end
211
+ unparse_params_.delete(:skipped_handler_list)
212
+ unparse_params_.delete(:required_for_later)
213
+ string_ = ''
214
+ value_.each_field_object do |field_, val_|
215
+ handler_ = @field_handlers[field_.name]
216
+ fragment_ = handler_.unparse(val_, unparse_params_)
217
+ if fragment_
218
+ list_ = unparse_params_.delete(:skipped_handler_list)
219
+ if list_ && handler_.requires_previous_field && !unparse_params_[:required_for_later]
220
+ unparse_params_[:required_for_later] = true
221
+ list_.each do |pair_|
222
+ frag_ = pair_[0].unparse(pair_[1], unparse_params_)
223
+ unless frag_
224
+ raise Errors::UnparseError, "Field #{field_.name} empty although a prerequisite for a later field"
225
+ end
226
+ string_ << frag_
227
+ end
228
+ unparse_params_[:required_for_later] = false
229
+ end
230
+ string_ << fragment_
231
+ else
232
+ if handler_.requires_previous_field
233
+ (unparse_params_[:skipped_handler_list] ||= []) << [handler_, val_]
234
+ else
235
+ unparse_params_[:skipped_handler_list] = [[handler_, val_]]
236
+ end
237
+ end
238
+ end
239
+ string_ << (unparse_params_[:suffix] || '')
240
+ string_
241
+ end
242
+
243
+
244
+ # Return a copy of the default parsing parameters used by this format.
245
+ # This hash cannot be edited in place. To modify the default parsing
246
+ # parameters, use modified_copy and call
247
+ # Versionomy::Format::Delimiter::Builder#default_parse_params in the block.
248
+
249
+ def default_parse_params
250
+ @default_parse_params.dup
251
+ end
252
+
253
+
254
+ # Return a copy of the default unparsing parameters used by this format.
255
+ # This hash cannot be edited in place. To modify the default unparsing
256
+ # parameters, use modified_copy and call
257
+ # Versionomy::Format::Delimiter::Builder#default_unparse_params in the block.
258
+
259
+ def default_unparse_params
260
+ @default_unparse_params.dup
261
+ end
262
+
263
+
264
+ # Create a copy of this format, with the modifications given in the
265
+ # provided block. You can call methods of Versionomy::Format::Delimiter::Builder
266
+ # in the block. Field handlers that you specify in that block will
267
+ # override and change the field handlers from the original. Any fields
268
+ # not specified in this block will use the handlers from the original.
269
+
270
+ def modified_copy(&block_)
271
+ Delimiter.new(self, &block_)
272
+ end
273
+
274
+
275
+ # A utility method that interprets required_fields and
276
+ # optional_fields parameters.
277
+
278
+ def _interpret_field_lists(unparse_params_) # :nodoc:
279
+ fields_ = unparse_params_.delete(:required_fields)
280
+ if fields_
281
+ fields_ = [fields_] unless fields_.kind_of?(Array)
282
+ fields_.each do |f_|
283
+ unparse_params_["#{f_}_required".to_sym] = true
284
+ end
285
+ end
286
+ fields_ = unparse_params_.delete(:optional_fields)
287
+ if fields_
288
+ fields_ = [fields_] unless fields_.kind_of?(Array)
289
+ fields_.each do |f_|
290
+ unparse_params_["#{f_}_required".to_sym] = false
291
+ end
292
+ end
293
+ end
294
+ private :_interpret_field_lists
295
+
296
+
297
+ # This class defines methods that you can call within the DSL block
298
+ # passed to Versionomy::Format::Delimiter#new.
299
+ #
300
+ # Generally, you call the field method of this class a number of times
301
+ # to define the formatting for each field.
302
+
303
+ class Builder
304
+
305
+ include ::Blockenspiel::DSL
306
+
307
+ def initialize(schema_, field_handlers_, default_parse_params_, default_unparse_params_) # :nodoc:
308
+ @schema = schema_
309
+ @field_handlers = field_handlers_
310
+ @default_parse_params = default_parse_params_
311
+ @default_unparse_params = default_unparse_params_
312
+ end
313
+
314
+
315
+ # Specify how to handle a given field.
316
+ # You must pass the name of the field, a hash of options, and a
317
+ # block defining the handling of the field.
318
+ #
319
+ # Within the block, you set up "recognizers" for various regular
320
+ # expression patterns. These recognizers are tested in order when
321
+ # parsing a version number.
322
+ #
323
+ # The methods that can be called from the block are determined by
324
+ # the type of field. If the field is an integer field, the methods
325
+ # of Versionomy::Format::Delimiter::IntegerFieldBuilder can be
326
+ # called from the block. If the field is a string field, the methods
327
+ # of Versionomy::Format::Delimiter::StringFieldBuilder can be
328
+ # called. If the field is a symbolic field, the methods of
329
+ # Versionomy::Format::Delimiter::SymbolFieldBuilder can be called.
330
+ #
331
+ # === Options
332
+ #
333
+ # The opts hash includes a number of options that control how the
334
+ # field is parsed.
335
+ #
336
+ # Some of these are regular expressions that indicate what patterns
337
+ # are recognized by the parser. Regular expressions should be passed
338
+ # in as the string representation of the regular expression, not a
339
+ # Regexp object itself. For example, use the string '-' rather than
340
+ # the Regexp /-/ to recognize a hyphen delimiter.
341
+ #
342
+ # The following options are recognized:
343
+ #
344
+ # <tt>:default_value_optional</tt>::
345
+ # If set to true, this the field may be omitted in the unparsed
346
+ # (formatted) version number, if the value is the default value
347
+ # for this field. However, if the following field is present and
348
+ # set as <tt>:requires_previous_field</tt>, then this field is
349
+ # still unparsed even if it is its default value.
350
+ # For example, for a version number like "2.0.0", often the third
351
+ # field is optional, but the first and second are required, so it
352
+ # will often be unparsed as "2.0".
353
+ # Default is false.
354
+ # <tt>:case_sensitive</tt>::
355
+ # If set to true, the regexps are case-sensitive. Default is false.
356
+ # <tt>:delimiter_regexp</tt>::
357
+ # The regular expression string for the pre-delimiter. This pattern
358
+ # must appear before the current value in the string, and is
359
+ # consumed when the field is parsed, but is not part of the value
360
+ # itself. Default is '\.' to recognize a period.
361
+ # <tt>:post_delimiter_regexp</tt>::
362
+ # The regular expression string for the post-delimiter. This pattern
363
+ # must appear before the current value in the string, and is
364
+ # consumed when the field is parsed, but is not part of the value
365
+ # itself. Default is '' to indicate no post-delimiter.
366
+ # <tt>:expected_follower_regexp</tt>::
367
+ # The regular expression string for what characters are expected to
368
+ # follow this field in the string. These characters are not part
369
+ # of the field itself, and are *not* consumed when the field is
370
+ # parsed; however, they must be present immediately after this
371
+ # field in order for the field to be recognized. Default is '' to
372
+ # indicate that we aren't testing for any particular characters.
373
+ # <tt>:default_delimiter</tt>::
374
+ # The default delimiter string. This is the string that is used
375
+ # to unparse a field value if the field was not present when the
376
+ # value was originally parsed. For example, if you parse the string
377
+ # "2.0", bump the tiny version so that the value is "2.0.1", and
378
+ # unparse, the unparsing won't receive the second period from
379
+ # parsing the original string, so its delimiter will use the default.
380
+ # Default value is '.'
381
+ # <tt>:default_post_delimiter</tt>::
382
+ # The default post-delimiter string. Default value is '' indicating
383
+ # no post-delimiter.
384
+ # <tt>:requires_previous_field</tt>::
385
+ # If set to true, this field's presence in a formatted version string
386
+ # requires the presence of the previous field. For example, in a
387
+ # typical version number "major.minor.tiny", tiny should appear in
388
+ # the string only if minor also appears, so tiny should have this
389
+ # parameter set to true. The default is true, so you must specify
390
+ # <tt>:requires_previous_field => false</tt> explicitly if you want
391
+ # a field not to require the previous field.
392
+ # <tt>:default_style</tt>::
393
+ # The default style for this field. This is the style used for
394
+ # unparsing if the value was not constructed by a parser or is
395
+ # otherwise missing the style for this field.
396
+ #
397
+ # === Styles
398
+ #
399
+ # A field may have different representation "styles". For example,
400
+ # you could represent a patchlevel of 1 as "1.0-1" or "1.0a".
401
+ # When a version number string is parsed, the parser and unparser
402
+ # work together to remember which style was parsed, and that style
403
+ # is used when the version number is unparsed.
404
+ #
405
+ # Specify styles as options to the calls made within the block that
406
+ # is passed to this method. In the above case, you could define the
407
+ # patchlevel field with a block that has two calls, one that uses
408
+ # Delimiter::IntegerFieldBuilder#recognize_number and passes the
409
+ # option <tt>:style => :number</tt>, and another that uses
410
+ # Delimiter::IntegerFieldBuilder#recognize_letter and passes the
411
+ # option <tt>:style => :letter</tt>.
412
+ #
413
+ # The standard format uses styles to preserve the different
414
+ # syntaxes for the release_type field. See the source code in
415
+ # Versionomy::Formats#_create_standard for this example.
416
+
417
+ def field(name_, opts_={}, &block_)
418
+ name_ = name_.to_sym
419
+ field_ = @schema.field_named(name_)
420
+ if !field_
421
+ raise Errors::FormatCreationError, "Unknown field name #{name_.inspect}"
422
+ end
423
+ @field_handlers[name_] = Delimiter::FieldHandler.new(field_, opts_, &block_)
424
+ end
425
+
426
+
427
+ # Set or modify the default parameters used when parsing a value.
428
+
429
+ def default_parse_params(params_)
430
+ @default_parse_params.merge!(params_)
431
+ end
432
+
433
+
434
+ # Set or modify the default parameters used when unparsing a value.
435
+
436
+ def default_unparse_params(params_)
437
+ @default_unparse_params.merge!(params_)
438
+ end
439
+
440
+ end
441
+
442
+
443
+ # This class defines methods that can be called from the block passed
444
+ # to Versionomy::Format::Delimiter::Builder#field if the field is
445
+ # of integer type.
446
+
447
+ class IntegerFieldBuilder
448
+
449
+ include ::Blockenspiel::DSL
450
+
451
+ def initialize(recognizers_, field_, default_opts_) # :nodoc:
452
+ @recognizers = recognizers_
453
+ @field = field_
454
+ @default_opts = default_opts_
455
+ end
456
+
457
+
458
+ # Recognize a numeric-formatted integer field.
459
+ # Using the opts parameter, you can override any of the field's
460
+ # overall parsing options.
461
+
462
+ def recognize_number(opts_={})
463
+ @recognizers << Delimiter::BasicIntegerRecognizer.new(@field, @default_opts.merge(opts_))
464
+ end
465
+
466
+
467
+ # Recognize a letter-formatted integer field. That is, the value is
468
+ # formatted as an alphabetic letter where "a" represents 1, up to
469
+ # "z" representing 26.
470
+ #
471
+ # Using the opts parameter, you can override any of the field's
472
+ # overall parsing options. You may also set the following additional
473
+ # options:
474
+ #
475
+ # <tt>:case</tt>::
476
+ # Case-sensitivity of the letter. Possible values are
477
+ # <tt>:upper</tt>, <tt>:lower</tt>, and <tt>:either</tt>.
478
+ # Default is <tt>:either</tt>.
479
+
480
+ def recognize_letter(opts_={})
481
+ @recognizers << Delimiter::AlphabeticIntegerRecognizer.new(@field, @default_opts.merge(opts_))
482
+ end
483
+
484
+ end
485
+
486
+
487
+ # This class defines methods that can be called from the block passed
488
+ # to Versionomy::Format::Delimiter::Builder#field if the field is
489
+ # of string type.
490
+
491
+ class StringFieldBuilder
492
+
493
+ include ::Blockenspiel::DSL
494
+
495
+ def initialize(recognizers_, field_, default_opts_) # :nodoc:
496
+ @recognizers = recognizers_
497
+ @field = field_
498
+ @default_opts = default_opts_
499
+ end
500
+
501
+
502
+ # Recognize a string field whose value matches a regular expression.
503
+ # The regular expression must be passed as a string. E.g. use
504
+ # "[a-z]+" instead of /[a-z]+/.
505
+ # Using the opts parameter, you can override any of the field's
506
+ # overall parsing options.
507
+
508
+ def recognize_regexp(regexp_, opts_={})
509
+ @recognizers << Delimiter::RegexpStringRecognizer.new(@field, regexp_, @default_opts.merge(opts_))
510
+ end
511
+
512
+ end
513
+
514
+
515
+ # This class defines methods that can be called from the block passed
516
+ # to Versionomy::Format::Delimiter::Builder#field if the field is
517
+ # of symbolic type.
518
+
519
+ class SymbolFieldBuilder
520
+
521
+ include ::Blockenspiel::DSL
522
+
523
+ def initialize(recognizers_, field_, default_opts_) # :nodoc:
524
+ @recognizers = recognizers_
525
+ @field = field_
526
+ @default_opts = default_opts_
527
+ end
528
+
529
+
530
+ # Recognize a symbolic value represented by a particular regular
531
+ # expression. The regular expression must be passed as a string.
532
+ # E.g. use "[a-z]+" instead of /[a-z]+/.
533
+ # The "canonical" parameter indicates the canonical syntax for the
534
+ # value, for use in unparsing.
535
+ #
536
+ # Using the opts parameter, you can override any of the field's
537
+ # overall parsing options.
538
+
539
+ def recognize_regexp(value_, regexp_, canonical_, opts_={}, &block_)
540
+ @recognizers << Delimiter::RegexpSymbolRecognizer.new(@field, value_, regexp_, canonical_, @default_opts.merge(opts_))
541
+ end
542
+
543
+
544
+ # Recognize a set of symbolic values, each represented by a
545
+ # particular regular expression, but all sharing the same delimiters
546
+ # and options. Use this instead of repeated calls to recognize_regexp
547
+ # for better performance.
548
+ #
549
+ # Using the opts parameter, you can override any of the field's
550
+ # overall parsing options.
551
+ #
552
+ # In the block, you should call methods of
553
+ # Versionomy::Format::Delimiter::MappingSymbolBuilder to map values
554
+ # to regular expression representations.
555
+
556
+ def recognize_regexp_map(opts_={}, &block_)
557
+ @recognizers << Delimiter::MappingSymbolRecognizer.new(@field, @default_opts.merge(opts_), &block_)
558
+ end
559
+
560
+ end
561
+
562
+
563
+ # Methods in this class can be called from the block passed to
564
+ # Versionomy::Format::Delimiter::SymbolFieldBuilder#recognize_regexp_map
565
+ # to define the mapping between the values of a symbolic field and
566
+ # the string representations of those values.
567
+
568
+ class MappingSymbolBuilder
569
+
570
+ include ::Blockenspiel::DSL
571
+
572
+ def initialize(mappings_in_order_, mappings_by_value_) # :nodoc:
573
+ @mappings_in_order = mappings_in_order_
574
+ @mappings_by_value = mappings_by_value_
575
+ end
576
+
577
+
578
+ # Map a value to a string representation.
579
+ # The optional regexp field, if specified, provides a regular
580
+ # expression pattern for matching the value representation. If it
581
+ # is omitted, the representation is used as the regexp.
582
+
583
+ def map(value_, representation_, regexp_=nil)
584
+ regexp_ ||= representation_
585
+ array_ = [regexp_, representation_, value_]
586
+ @mappings_by_value[value_] ||= array_
587
+ @mappings_in_order << array_
588
+ end
589
+
590
+ end
591
+
592
+
593
+ # This class handles the parsing and unparsing of a single field.
594
+ # It manages an ordered list of recognizers, each understanding a
595
+ # particular syntax. These recognizers are checked in order when
596
+ # parsing and unparsing.
597
+
598
+ class FieldHandler # :nodoc:
599
+
600
+
601
+ # Creates a FieldHandler, using a DSL block appropriate to the
602
+ # field type to configure the recognizers.
603
+
604
+ def initialize(field_, default_opts_={}, &block_)
605
+ @field = field_
606
+ @recognizers = []
607
+ @requires_previous_field = default_opts_.fetch(:requires_previous_field, true)
608
+ @default_style = default_opts_.fetch(:default_style, nil)
609
+ @style_unparse_param_key = "#{field_.name}_style".to_sym
610
+ if block_
611
+ builder_ = case field_.type
612
+ when :integer
613
+ Delimiter::IntegerFieldBuilder.new(@recognizers, field_, default_opts_)
614
+ when :string
615
+ Delimiter::StringFieldBuilder.new(@recognizers, field_, default_opts_)
616
+ when :symbol
617
+ Delimiter::SymbolFieldBuilder.new(@recognizers, field_, default_opts_)
618
+ end
619
+ ::Blockenspiel.invoke(block_, builder_)
620
+ end
621
+ end
622
+
623
+
624
+ # Returns true if this field can appear in an unparsed string only
625
+ # if the previous field also appears.
626
+
627
+ def requires_previous_field
628
+ @requires_previous_field
629
+ end
630
+
631
+
632
+ # Parse this field from the string.
633
+ # This must either return a parsed value, or raise an error.
634
+ # It should also set the style in the unparse_params, if the style
635
+ # is determined not to be the default.
636
+
637
+ def parse(parse_params_, unparse_params_)
638
+ pair_ = nil
639
+ @recognizers.each do |recog_|
640
+ pair_ = recog_.parse(parse_params_, unparse_params_)
641
+ break if pair_
642
+ end
643
+ parse_params_[:previous_field_missing] = pair_.nil?
644
+ pair_ ||= [@field.default_value, @default_style]
645
+ if pair_[1] && pair_[1] != @default_style
646
+ unparse_params_[@style_unparse_param_key] = pair_[1]
647
+ end
648
+ pair_[0]
649
+ end
650
+
651
+
652
+ # Unparse a string from this field value.
653
+ # This may return nil if this field is not required.
654
+
655
+ def unparse(value_, unparse_params_)
656
+ style_ = unparse_params_[@style_unparse_param_key] || @default_style
657
+ @recognizers.each do |recog_|
658
+ if recog_.should_unparse?(value_, style_)
659
+ return recog_.unparse(value_, style_, unparse_params_)
660
+ end
661
+ end
662
+ unparse_params_[:required_for_later] ? '' : nil
663
+ end
664
+
665
+ end
666
+
667
+
668
+ # A recognizer handles both parsing and unparsing of a particular kind
669
+ # of syntax. During parsing, it recognizes the syntax based on regular
670
+ # expressions for the delimiters and the value. If the string matches
671
+ # the syntax recognized by this object, an appropriate value and style
672
+ # are returned. During unparsing, the should_unparse? method should be
673
+ # called first to determine whether this object is responsible for
674
+ # unparsing the given value and style. If should_unparse? returns
675
+ # true, the unparse method should be called to actually generate a
676
+ # a string fragment, or return nil if the field is determined to be
677
+ # optional in the unparsed string.
678
+ #
679
+ # This is a base class. The actual classes should implement
680
+ # initialize, parsed_value, and unparsed_value, and may optionally
681
+ # override the should_unparse? method.
682
+
683
+ class RecognizerBase # :nodoc:
684
+
685
+ # Derived classes should call this from their initialize method
686
+ # to set up the recognizer's basic parameters.
687
+
688
+ def setup(field_, value_regexp_, opts_)
689
+ @style = opts_[:style]
690
+ @default_value_optional = opts_[:default_value_optional]
691
+ @regexp_options = opts_[:case_sensitive] ? nil : ::Regexp::IGNORECASE
692
+ @value_regexp = ::Regexp.new("^(#{value_regexp_})", @regexp_options)
693
+ regexp_ = opts_.fetch(:delimiter_regexp, '\.')
694
+ @delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
695
+ regexp_ = opts_.fetch(:post_delimiter_regexp, '')
696
+ @post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
697
+ regexp_ = opts_.fetch(:expected_follower_regexp, '')
698
+ @follower_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
699
+ @default_delimiter = opts_.fetch(:default_delimiter, '.')
700
+ @default_post_delimiter = opts_.fetch(:default_post_delimiter, '')
701
+ @requires_previous_field = opts_.fetch(:requires_previous_field, true)
702
+ name_ = field_.name
703
+ @default_field_value = field_.default_value
704
+ @delim_unparse_param_key = "#{name_}_delim".to_sym
705
+ @post_delim_unparse_param_key = "#{name_}_postdelim".to_sym
706
+ @required_unparse_param_key = "#{name_}_required".to_sym
707
+ end
708
+
709
+
710
+ # Attempt to parse the field from the string if the syntax matches
711
+ # this recognizer's configuration.
712
+ # Returns either nil, indicating that this recognizer doesn't match
713
+ # the given syntax, or a two element array of the value and style.
714
+
715
+ def parse(parse_params_, unparse_params_)
716
+ return nil if @requires_previous_field && parse_params_[:previous_field_missing]
717
+ string_ = parse_params_[:string]
718
+ if @delimiter_regexp
719
+ match_ = @delimiter_regexp.match(string_)
720
+ return nil unless match_
721
+ delim_ = match_[0]
722
+ string_ = match_.post_match
723
+ else
724
+ delim_ = ''
725
+ end
726
+ match_ = @value_regexp.match(string_)
727
+ return nil unless match_
728
+ value_ = match_[0]
729
+ string_ = match_.post_match
730
+ if @post_delimiter_regexp
731
+ match_ = @post_delimiter_regexp.match(string_)
732
+ return nil unless match_
733
+ post_delim_ = match_[0]
734
+ string_ = match_.post_match
735
+ else
736
+ post_delim_ = nil
737
+ end
738
+ if @follower_regexp
739
+ match_ = @follower_regexp.match(string_)
740
+ return nil unless match_
741
+ end
742
+ value_ = parsed_value(value_, parse_params_, unparse_params_)
743
+ return nil unless value_
744
+ parse_params_[:string] = string_
745
+ if delim_ != @default_delimiter
746
+ unparse_params_[@delim_unparse_param_key] = delim_
747
+ end
748
+ if post_delim_ && post_delim_ != @default_post_delimiter
749
+ unparse_params_[@post_delim_unparse_param_key] = post_delim_
750
+ end
751
+ unparse_params_[@required_unparse_param_key] = true if @default_value_optional
752
+ [value_, @style]
753
+ end
754
+
755
+
756
+ # Returns true if this recognizer should be used to unparse the
757
+ # given value and style.
758
+
759
+ def should_unparse?(value_, style_)
760
+ style_ == @style
761
+ end
762
+
763
+
764
+ # Unparse the given value in the given style, and return a string
765
+ # fragment, or nil if the field is determined to be "optional" to
766
+ # unparse and isn't otherwise required (because a later field needs
767
+ # it to be present, for example).
768
+ #
769
+ # It is guaranteed that this will be called only if should_unparse?
770
+ # returns true.
771
+
772
+ def unparse(value_, style_, unparse_params_)
773
+ str_ = nil
774
+ if !@default_value_optional || value_ != @default_field_value ||
775
+ unparse_params_[:required_for_later] ||
776
+ unparse_params_[@required_unparse_param_key]
777
+ then
778
+ str_ = unparsed_value(value_, style_, unparse_params_)
779
+ if str_
780
+ delim_ = unparse_params_[@delim_unparse_param_key]
781
+ if !delim_ || @delimiter_regexp && @delimiter_regexp !~ delim_
782
+ delim_ = @default_delimiter
783
+ end
784
+ post_delim_ = unparse_params_[@post_delim_unparse_param_key]
785
+ if !post_delim_ || @post_delimiter_regexp && @post_delimiter_regexp !~ post_delim_
786
+ post_delim_ = @default_post_delimiter
787
+ end
788
+ str_ = delim_ + str_ + post_delim_
789
+ end
790
+ str_
791
+ else
792
+ nil
793
+ end
794
+ end
795
+
796
+ end
797
+
798
+
799
+ # A recognizer for a numeric integer field
800
+
801
+ class BasicIntegerRecognizer < RecognizerBase #:nodoc:
802
+
803
+ def initialize(field_, opts_={})
804
+ setup(field_, '\d+', opts_)
805
+ end
806
+
807
+ def parsed_value(value_, parse_params_, unparse_params_)
808
+ value_.to_i
809
+ end
810
+
811
+ def unparsed_value(value_, style_, unparse_params_)
812
+ value_.to_s
813
+ end
814
+
815
+ end
816
+
817
+
818
+ # A recognizer for an alphabetic integer field. Such a field
819
+ # represents values 1-26 as letters of the English alphabet.
820
+
821
+ class AlphabeticIntegerRecognizer < RecognizerBase # :nodoc:
822
+
823
+ def initialize(field_, opts_={})
824
+ @case_unparse_param_key = "#{field_.name}_case".to_sym
825
+ @case = opts_[:case]
826
+ case @case
827
+ when :upper
828
+ value_regexp_ = '[A-Z]'
829
+ when :lower
830
+ value_regexp_ = '[a-z]'
831
+ else #either
832
+ value_regexp_ = '[a-zA-Z]'
833
+ end
834
+ setup(field_, value_regexp_, opts_)
835
+ end
836
+
837
+ def parsed_value(value_, parse_params_, unparse_params_)
838
+ value_ = value_.unpack('c')[0] # Compatible with both 1.8 and 1.9
839
+ if value_ >= 97 && value_ <= 122
840
+ unparse_params_[@case_unparse_param_key] = :lower
841
+ value_ - 96
842
+ elsif value_ >= 65 && value_ <= 90
843
+ unparse_params_[@case_unparse_param_key] = :upper
844
+ value_ - 64
845
+ else
846
+ 0
847
+ end
848
+ end
849
+
850
+ def unparsed_value(value_, style_, unparse_params_)
851
+ if value_ >= 1 && value_ <= 26
852
+ if unparse_params_[@case_unparse_param_key] == :upper
853
+ (value_+64).chr
854
+ else
855
+ (value_+96).chr
856
+ end
857
+ else
858
+ value_.to_s
859
+ end
860
+ end
861
+
862
+ end
863
+
864
+
865
+ # A recognizer for strings that match a particular given regular
866
+ # expression, for use in string-valued fields.
867
+
868
+ class RegexpStringRecognizer < RecognizerBase # :nodoc:
869
+
870
+ def initialize(field_, regexp_='[a-zA-Z0-9]+', opts_={})
871
+ setup(field_, regexp_, opts_)
872
+ end
873
+
874
+ def parsed_value(value_, parse_params_, unparse_params_)
875
+ value_
876
+ end
877
+
878
+ def unparsed_value(value_, style_, unparse_params_)
879
+ value_
880
+ end
881
+
882
+ end
883
+
884
+
885
+ # A recognizer for symbolic fields that recognizes a single regular
886
+ # expression and maps it to a single particular value.
887
+
888
+ class RegexpSymbolRecognizer < RecognizerBase # :nodoc:
889
+
890
+ def initialize(field_, value_, regexp_, canonical_, opts_={})
891
+ setup(field_, regexp_, opts_)
892
+ @value = value_
893
+ @canonical = canonical_
894
+ end
895
+
896
+ def parsed_value(value_, parse_params, unparse_params_)
897
+ @value
898
+ end
899
+
900
+ def unparsed_value(value_, style_, unparse_params_)
901
+ @canonical
902
+ end
903
+
904
+ def should_unparse?(value_, style_)
905
+ style_ == @style && value_ == @value
906
+ end
907
+
908
+ end
909
+
910
+
911
+ # A recognizer for symbolic fields that recognizes a mapping of values
912
+ # to regular expressions.
913
+
914
+ class MappingSymbolRecognizer < RecognizerBase # :nodoc:
915
+
916
+ def initialize(field_, opts_={}, &block_)
917
+ @mappings_in_order = []
918
+ @mappings_by_value = {}
919
+ builder_ = Delimiter::MappingSymbolBuilder.new(@mappings_in_order, @mappings_by_value)
920
+ ::Blockenspiel.invoke(block_, builder_)
921
+ regexps_ = @mappings_in_order.map{ |map_| "(#{map_[0]})" }
922
+ setup(field_, regexps_.join('|'), opts_)
923
+ @mappings_in_order.each do |map_|
924
+ map_[0] = ::Regexp.new("^(#{map_[0]})", @regexp_options)
925
+ end
926
+ end
927
+
928
+ def parsed_value(value_, parse_params, unparse_params_)
929
+ @mappings_in_order.each do |map_|
930
+ return map_[2] if map_[0].match(value_)
931
+ end
932
+ nil
933
+ end
934
+
935
+ def unparsed_value(value_, style_, unparse_params_)
936
+ @mappings_by_value[value_][1]
937
+ end
938
+
939
+ def should_unparse?(value_, style_)
940
+ style_ == @style && @mappings_by_value.include?(value_)
941
+ end
942
+
943
+ end
944
+
945
+
946
+ end
947
+
948
+
949
+ end
950
+
951
+ end