versionomy 0.0.4 → 0.1.0

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