# -----------------------------------------------------------------------------
#
# Versionomy delimiter format
#
# -----------------------------------------------------------------------------
# Copyright 2008-2009 Daniel Azuma
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the copyright holder, nor the names of any other
# contributors to this software, may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
;
module Versionomy
module Format
# The Delimiter format class provides a DSL for building formats that
# can handle most cases where the fields of a version number appear
# consecutively in order in the string formatting. We expect most
# version number schemes should fall into this category.
#
# In general, the strategy is to provide, for each field, a set of
# regular expressions that recognize different formats for that field.
# Every field must be of the form "(pre)(value)(post)"
# where (pre) and (post) are delimiters preceding and
# following the value. Either or both delimiters may be the empty string.
#
# To parse a string, the string is scanned from left to right and
# matched against the format for the fields in order. If the string
# matches, that part of the string is consumed and the field value is
# interpreted from it. If the string does not match, and the field is
# not marked as "required", then the field is set to its default value
# and the next field is tried.
#
# During parsing, the actual delimiters, along with other information
# such as whether or not fields are required, are saved into a default
# set of parameters for unparsing. These are saved in the unparse_params
# of the version value, so that the version number can be unparsed in
# generally the same form. If the version number value is modified, this
# allows the unparsing of the new value to generally follow the format
# of the original string.
#
# Formats that use the Delimiter mechanism also provide support for
# certain parsing and unparsing parameters. See the documentation for
# the parse and unparse methods for details.
#
# For a usage example, see the definition of the standard format in
# Versionomy::Format::Standard#create.
class Delimiter < Base
# Create a format using delimiter tools.
# You should provide the version number schema, a set of default
# options, and a block.
#
# Within the block, you can call methods of
# Versionomy::Format::Delimiter::Builder
# to provide parsers for the fields of the schema. Any fields you do
# not explicitly configure will get parsed in a default manner.
def initialize(schema_, default_opts_={}, &block_)
# Special case used by modified_copy
if schema_.kind_of?(Delimiter)
orig_ = schema_
@schema = orig_.schema
@default_parse_params = orig_.default_parse_params
@default_unparse_params = orig_.default_unparse_params
@field_handlers = orig_.instance_variable_get(:@field_handlers).dup
builder_ = Delimiter::Builder.new(@schema, @field_handlers,
@default_parse_params, @default_unparse_params)
::Blockenspiel.invoke(block_, builder_)
return
end
@schema = schema_
@field_handlers = {}
@default_parse_params = {}
@default_unparse_params = {}
builder_ = Delimiter::Builder.new(@schema, @field_handlers,
@default_parse_params, @default_unparse_params)
::Blockenspiel.invoke(block_, builder_)
_interpret_field_lists(@default_unparse_params)
@schema.names.each do |name_|
@field_handlers[name_] ||= Delimiter::FieldHandler.new(@schema.field_named(name_), default_opts_)
end
end
# Returns the schema understood by this format.
# This method is required by the Format contract.
def schema
@schema
end
# Parse the given string and return a value.
# This method is required by the Format contract.
#
# This method provides, out of the box, support for the following
# parse parameters:
#
# :extra_characters::
# Determines what to do if the entire string cannot be consumed by
# the parsing process. If set to :ignore, any extra
# characters are ignored. If set to :suffix, the extra
# characters are set as the :suffix unparse parameter and
# are thus appended to the end of the string when unparsing takes
# place. If set to :error (the default), causes a
# Versionomy::Errors::ParseError to be raised if there are
# uninterpreted characters.
def parse(string_, params_=nil)
parse_params_ = default_parse_params
parse_params_.merge!(params_) if params_
parse_state_ = {
:backtrack => nil,
:string => string_,
:values => {},
:unparse_params => {},
:field => @schema.root_field,
:recognizer_index => 0,
:previous_field_missing => false
}
while (field_ = parse_state_[:field])
handler_ = @field_handlers[field_.name]
recognizer_ = handler_.get_recognizer(parse_state_[:recognizer_index])
parse_data_ = nil
if recognizer_
parse_state_[:recognizer_index] += 1
parse_data_ = recognizer_.parse(parse_state_, parse_params_)
if parse_data_
parse_state_[:previous_field_missing] = false
if recognizer_.requires_next_field
parse_state_ = {
:backtrack => parse_state_,
:string => parse_state_[:string],
:values => parse_state_[:values].dup,
:unparse_params => parse_state_[:unparse_params].dup,
:field => parse_state_[:field],
:recognizer_index => 0,
:previous_field_missing => false,
:next_field_required => true,
}
else
parse_state_[:next_field_required] = false
end
end
elsif parse_state_[:next_field_required]
parse_state_ = parse_state_[:backtrack]
else
parse_data_ = [handler_.default_value, nil, nil, nil]
parse_state_[:previous_field_missing] = true
parse_state_[:next_field_required] = false
end
if parse_data_
parse_state_[:values][field_.name] = parse_data_[0]
parse_state_[:string] = parse_data_[2] if parse_data_[2]
parse_state_[:unparse_params].merge!(parse_data_[3]) if parse_data_[3]
parse_state_[:field] = field_.child(parse_data_[0])
parse_state_[:recognizer_index] = 0
handler_.set_style_unparse_param(parse_data_[1], parse_state_[:unparse_params])
end
end
unparse_params_ = parse_state_[:unparse_params]
if parse_state_[:string].length > 0
case parse_params_[:extra_characters]
when :ignore
# do nothing
when :suffix
unparse_params_[:suffix] = parse_state_[:string]
else
raise Errors::ParseError, "Extra characters: #{parse_state_[:string].inspect}"
end
end
Value.new(parse_state_[:values], self, unparse_params_)
end
# Unparse the given value and return a string.
# This method is required by the Format contract.
#
# This method provides, out of the box, support for the following
# unparse parameters:
#
# :suffix::
# A string to append to the unparsed string. Default is nothing.
# :required_fields::
# An array of field names that must be present in the unparsed
# string. These are generally fields with default_value_optional
# set, but that we want present in the string anyway. For example,
# in the version number "2.0.0", often the third field will be
# default_value_optional, but we can include it in the required
# fields passed to unparse to force it to appear in the string.
# :optional_fields::
# An array of field names that should have their presence in
# required_fields undone.
# :fieldname_required::
# This is an alternate way of specifying whether a potentially
# optional field should be required. Accepted values are true
# and false.
# :fieldname_style::
# Specify the style for unparsing the given field. See
# Versionomy::Format::Delimiter::Builder#field for more
# discussion of styles.
# :fieldname_delim::
# Set the pre-delimiter for the given field, if supported.
# Note that the string specified must be legal-- it must match the
# regexp for the field. If not, it will revert to the default.
# :fieldname_postdelim::
# Set the post-delimiter for the given field, if supported.
# Note that the string specified must be legal-- it must match the
# regexp for the field. If not, it will revert to the default.
# :fieldname_case::
# This is used by letter-formatted integer fields only, and
# sets the case to use while unparsing. Recognized values are
# :lower (the default), and :upper.
def unparse(value_, params_=nil)
unparse_params_ = value_.unparse_params || default_unparse_params
_interpret_field_lists(unparse_params_)
if params_
unparse_params_.merge!(params_)
_interpret_field_lists(unparse_params_)
end
skipped_handler_list_ = nil
requires_next_field_ = false
string_ = ''
value_.each_field_object do |field_, val_|
handler_ = @field_handlers[field_.name]
unparse_data_ = handler_.unparse(val_, unparse_params_, requires_next_field_)
if unparse_data_
if skipped_handler_list_ && handler_.requires_previous_field
skipped_handler_list_.each do |pair_|
frag_ = pair_[0].unparse(pair_[1], unparse_params_, true)
unless frag_
raise Errors::UnparseError, "Field #{field_.name} empty although a prerequisite for a later field"
end
string_ << frag_[0]
end
end
skipped_handler_list_ = nil
string_ << unparse_data_[0]
requires_next_field_ = unparse_data_[1]
else
if handler_.requires_previous_field
(skipped_handler_list_ ||= []) << [handler_, val_]
else
skipped_handler_list_ = [[handler_, val_]]
end
requires_next_field_ = false
end
end
string_ << (unparse_params_[:suffix] || '')
string_
end
# Return a copy of the default parsing parameters used by this format.
# This hash cannot be edited in place. To modify the default parsing
# parameters, use modified_copy and call
# Versionomy::Format::Delimiter::Builder#default_parse_params in the block.
def default_parse_params
@default_parse_params.dup
end
# Return a copy of the default unparsing parameters used by this format.
# This hash cannot be edited in place. To modify the default unparsing
# parameters, use modified_copy and call
# Versionomy::Format::Delimiter::Builder#default_unparse_params in the block.
def default_unparse_params
@default_unparse_params.dup
end
# Create a copy of this format, with the modifications given in the
# provided block. You can call methods of Versionomy::Format::Delimiter::Builder
# in the block. Field handlers that you specify in that block will
# override and change the field handlers from the original. Any fields
# not specified in this block will use the handlers from the original.
def modified_copy(&block_)
Delimiter.new(self, &block_)
end
# A utility method that interprets required_fields and
# optional_fields parameters.
def _interpret_field_lists(unparse_params_) # :nodoc:
fields_ = unparse_params_.delete(:required_fields)
if fields_
fields_ = [fields_] unless fields_.kind_of?(::Array)
fields_.each do |f_|
unparse_params_["#{f_}_required".to_sym] = true
end
end
fields_ = unparse_params_.delete(:optional_fields)
if fields_
fields_ = [fields_] unless fields_.kind_of?(::Array)
fields_.each do |f_|
unparse_params_["#{f_}_required".to_sym] = false
end
end
end
private :_interpret_field_lists
# This class defines methods that you can call within the DSL block
# passed to Versionomy::Format::Delimiter#new.
#
# Generally, you call the field method of this class a number of times
# to define the formatting for each field.
class Builder
include ::Blockenspiel::DSL
def initialize(schema_, field_handlers_, default_parse_params_, default_unparse_params_) # :nodoc:
@schema = schema_
@field_handlers = field_handlers_
@default_parse_params = default_parse_params_
@default_unparse_params = default_unparse_params_
end
# Specify how to handle a given field.
# You must pass the name of the field, a hash of options, and a
# block defining the handling of the field.
#
# Within the block, you set up "recognizers" for various regular
# expression patterns. These recognizers are tested in order when
# parsing a version number.
#
# The methods that can be called from the block are determined by
# the type of field. If the field is an integer field, the methods
# of Versionomy::Format::Delimiter::IntegerFieldBuilder can be
# called from the block. If the field is a string field, the methods
# of Versionomy::Format::Delimiter::StringFieldBuilder can be
# called. If the field is a symbolic field, the methods of
# Versionomy::Format::Delimiter::SymbolFieldBuilder can be called.
#
# === Options
#
# The opts hash includes a number of options that control how the
# field is parsed.
#
# Some of these are regular expressions that indicate what patterns
# are recognized by the parser. Regular expressions should be passed
# in as the string representation of the regular expression, not a
# Regexp object itself. For example, use the string '-' rather than
# the Regexp /-/ to recognize a hyphen delimiter.
#
# The following options are recognized:
#
# :default_value_optional::
# If set to true, this the field may be omitted in the unparsed
# (formatted) version number, if the value is the default value
# for this field. However, if the following field is present and
# set as :requires_previous_field, then this field is
# still unparsed even if it is its default value.
# For example, for a version number like "2.0.0", often the third
# field is optional, but the first and second are required, so it
# will often be unparsed as "2.0".
# Default is false.
# :default_value::
# The actual value set for this field if it is omitted from the
# version string. Defaults to the field's schema default value,
# but that can be overridden here.
# :case_sensitive::
# If set to true, the regexps are case-sensitive. Default is false.
# :delimiter_regexp::
# The regular expression string for the pre-delimiter. This pattern
# must appear before the current value in the string, and is
# consumed when the field is parsed, but is not part of the value
# itself. Default is '\.' to recognize a period.
# :post_delimiter_regexp::
# The regular expression string for the post-delimiter. This pattern
# must appear before the current value in the string, and is
# consumed when the field is parsed, but is not part of the value
# itself. Default is '' to indicate no post-delimiter.
# :expected_follower_regexp::
# The regular expression string for what characters are expected to
# follow this field in the string. These characters are not part
# of the field itself, and are *not* consumed when the field is
# parsed; however, they must be present immediately after this
# field in order for the field to be recognized. Default is '' to
# indicate that we aren't testing for any particular characters.
# :default_delimiter::
# The default delimiter string. This is the string that is used
# to unparse a field value if the field was not present when the
# value was originally parsed. For example, if you parse the string
# "2.0", bump the tiny version so that the value is "2.0.1", and
# unparse, the unparsing won't receive the second period from
# parsing the original string, so its delimiter will use the default.
# Default value is '.'
# :default_post_delimiter::
# The default post-delimiter string. Default value is '' indicating
# no post-delimiter.
# :requires_previous_field::
# If set to true, this field's presence in a formatted version string
# requires the presence of the previous field. For example, in a
# typical version number "major.minor.tiny", tiny should appear in
# the string only if minor also appears, so tiny should have this
# parameter set to true. The default is true, so you must specify
# :requires_previous_field => false explicitly if you want
# a field not to require the previous field.
# :requires_next_field::
# If set to true, this field's presence in a formatted version
# string requires the presence of the next field. For example, in
# the version "1.0a5", the release_type field requires the presence
# of the alpha_version field, because if the "5" was missing, the
# string "1.0a" looks like a patchlevel indicator. Often it is
# easier to set default_value_optional in the next field, but this
# option is also available if the behavior is dependent on the
# value of this previous field.
# :default_style::
# The default style for this field. This is the style used for
# unparsing if the value was not constructed by a parser or is
# otherwise missing the style for this field.
#
# === Styles
#
# A field may have different representation "styles". For example,
# you could represent a patchlevel of 1 as "1.0-1" or "1.0a".
# When a version number string is parsed, the parser and unparser
# work together to remember which style was parsed, and that style
# is used when the version number is unparsed.
#
# Specify styles as options to the calls made within the block that
# is passed to this method. In the above case, you could define the
# patchlevel field with a block that has two calls, one that uses
# Delimiter::IntegerFieldBuilder#recognize_number and passes the
# option :style => :number, and another that uses
# Delimiter::IntegerFieldBuilder#recognize_letter and passes the
# option :style => :letter.
#
# The standard format uses styles to preserve the different
# syntaxes for the release_type field. See the source code in
# Versionomy::Format::Standard#create for this example.
def field(name_, opts_={}, &block_)
name_ = name_.to_sym
field_ = @schema.field_named(name_)
if !field_
raise Errors::FormatCreationError, "Unknown field name #{name_.inspect}"
end
@field_handlers[name_] = Delimiter::FieldHandler.new(field_, opts_, &block_)
end
# Set or modify the default parameters used when parsing a value.
def default_parse_params(params_)
@default_parse_params.merge!(params_)
end
# Set or modify the default parameters used when unparsing a value.
def default_unparse_params(params_)
@default_unparse_params.merge!(params_)
end
end
# This class defines methods that can be called from the block passed
# to Versionomy::Format::Delimiter::Builder#field if the field is
# of integer type.
class IntegerFieldBuilder
include ::Blockenspiel::DSL
def initialize(recognizers_, field_, default_opts_) # :nodoc:
@recognizers = recognizers_
@field = field_
@default_opts = default_opts_
end
# Recognize a numeric-formatted integer field.
# Using the opts parameter, you can override any of the field's
# overall parsing options.
def recognize_number(opts_={})
@recognizers << Delimiter::BasicIntegerRecognizer.new(@field, @default_opts.merge(opts_))
end
# Recognize a letter-formatted integer field. That is, the value is
# formatted as an alphabetic letter where "a" represents 1, up to
# "z" representing 26.
#
# Using the opts parameter, you can override any of the field's
# overall parsing options. You may also set the following additional
# options:
#
# :case::
# Case-sensitivity of the letter. Possible values are
# :upper, :lower, and :either.
# Default is :either.
def recognize_letter(opts_={})
@recognizers << Delimiter::AlphabeticIntegerRecognizer.new(@field, @default_opts.merge(opts_))
end
end
# This class defines methods that can be called from the block passed
# to Versionomy::Format::Delimiter::Builder#field if the field is
# of string type.
class StringFieldBuilder
include ::Blockenspiel::DSL
def initialize(recognizers_, field_, default_opts_) # :nodoc:
@recognizers = recognizers_
@field = field_
@default_opts = default_opts_
end
# Recognize a string field whose value matches a regular expression.
# The regular expression must be passed as a string. E.g. use
# "[a-z]+" instead of /[a-z]+/.
# Using the opts parameter, you can override any of the field's
# overall parsing options.
def recognize_regexp(regexp_, opts_={})
@recognizers << Delimiter::RegexpStringRecognizer.new(@field, regexp_, @default_opts.merge(opts_))
end
end
# This class defines methods that can be called from the block passed
# to Versionomy::Format::Delimiter::Builder#field if the field is
# of symbolic type.
class SymbolFieldBuilder
include ::Blockenspiel::DSL
def initialize(recognizers_, field_, default_opts_) # :nodoc:
@recognizers = recognizers_
@field = field_
@default_opts = default_opts_
end
# Recognize a symbolic value represented by a particular regular
# expression. The regular expression must be passed as a string.
# E.g. use "[a-z]+" instead of /[a-z]+/.
# The "canonical" parameter indicates the canonical syntax for the
# value, for use in unparsing.
#
# Using the opts parameter, you can override any of the field's
# overall parsing options.
def recognize_regexp(value_, regexp_, canonical_, opts_={}, &block_)
@recognizers << Delimiter::RegexpSymbolRecognizer.new(@field, value_, regexp_, canonical_, @default_opts.merge(opts_))
end
# Recognize a set of symbolic values, each represented by a
# particular regular expression, but all sharing the same delimiters
# and options. Use this instead of repeated calls to recognize_regexp
# for better performance.
#
# Using the opts parameter, you can override any of the field's
# overall parsing options.
#
# In the block, you should call methods of
# Versionomy::Format::Delimiter::MappingSymbolBuilder to map values
# to regular expression representations.
def recognize_regexp_map(opts_={}, &block_)
@recognizers << Delimiter::MappingSymbolRecognizer.new(@field, @default_opts.merge(opts_), &block_)
end
end
# Methods in this class can be called from the block passed to
# Versionomy::Format::Delimiter::SymbolFieldBuilder#recognize_regexp_map
# to define the mapping between the values of a symbolic field and
# the string representations of those values.
class MappingSymbolBuilder
include ::Blockenspiel::DSL
def initialize(mappings_in_order_, mappings_by_value_) # :nodoc:
@mappings_in_order = mappings_in_order_
@mappings_by_value = mappings_by_value_
end
# Map a value to a string representation.
# The optional regexp field, if specified, provides a regular
# expression pattern for matching the value representation. If it
# is omitted, the representation is used as the regexp.
def map(value_, representation_, regexp_=nil)
regexp_ ||= representation_
array_ = [regexp_, representation_, value_]
@mappings_by_value[value_] ||= array_
@mappings_in_order << array_
end
end
# This class handles the parsing and unparsing of a single field.
# It manages an ordered list of recognizers, each understanding a
# particular syntax. These recognizers are checked in order when
# parsing and unparsing.
class FieldHandler # :nodoc:
# Creates a FieldHandler, using a DSL block appropriate to the
# field type to configure the recognizers.
def initialize(field_, default_opts_={}, &block_)
@field = field_
@recognizers = []
@requires_previous_field = default_opts_.fetch(:requires_previous_field, true)
@default_value = default_opts_[:default_value] || field_.default_value
@default_style = default_opts_.fetch(:default_style, nil)
@style_unparse_param_key = "#{field_.name}_style".to_sym
if block_
builder_ = case field_.type
when :integer
Delimiter::IntegerFieldBuilder.new(@recognizers, field_, default_opts_)
when :string
Delimiter::StringFieldBuilder.new(@recognizers, field_, default_opts_)
when :symbol
Delimiter::SymbolFieldBuilder.new(@recognizers, field_, default_opts_)
end
::Blockenspiel.invoke(block_, builder_)
end
end
# Returns true if this field can appear in an unparsed string only
# if the previous field also appears.
def requires_previous_field
@requires_previous_field
end
# Returns the default value set when this field is missing from a
# version string.
def default_value
@default_value
end
# Gets the given indexed recognizer. Returns nil if the index is out
# of range.
def get_recognizer(index_)
@recognizers[index_]
end
# Finishes up parsing by setting the appropriate style field in the
# unparse_params, if needed.
def set_style_unparse_param(style_, unparse_params_)
if style_ && style_ != @default_style
unparse_params_[@style_unparse_param_key] = style_
end
end
# Unparse a string from this field value.
# This may return nil if this field is not required.
def unparse(value_, unparse_params_, required_for_later_)
style_ = unparse_params_[@style_unparse_param_key] || @default_style
@recognizers.each do |recog_|
if recog_.should_unparse?(value_, style_)
fragment_ = recog_.unparse(value_, style_, unparse_params_, required_for_later_)
return fragment_ ? [fragment_, recog_.requires_next_field] : nil
end
end
required_for_later_ ? ['', false] : nil
end
end
# A recognizer handles both parsing and unparsing of a particular kind
# of syntax. During parsing, it recognizes the syntax based on regular
# expressions for the delimiters and the value. If the string matches
# the syntax recognized by this object, an appropriate value and style
# are returned. During unparsing, the should_unparse? method should be
# called first to determine whether this object is responsible for
# unparsing the given value and style. If should_unparse? returns
# true, the unparse method should be called to actually generate a
# a string fragment, or return nil if the field is determined to be
# optional in the unparsed string.
#
# This is a base class. The actual classes should implement
# initialize, parsed_value, and unparsed_value, and may optionally
# override the should_unparse? method.
class RecognizerBase # :nodoc:
# Derived classes should call this from their initialize method
# to set up the recognizer's basic parameters.
def setup(field_, value_regexp_, opts_)
@style = opts_[:style]
@default_value_optional = opts_[:default_value_optional]
@default_value = opts_[:default_value] || field_.default_value
@regexp_options = opts_[:case_sensitive] ? nil : ::Regexp::IGNORECASE
@value_regexp = ::Regexp.new("\\A(#{value_regexp_})", @regexp_options)
regexp_ = opts_[:delimiter_regexp] || '\.'
@delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})", @regexp_options) : nil
@full_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})\\z", @regexp_options) : nil
regexp_ = opts_[:post_delimiter_regexp] || ''
@post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})", @regexp_options) : nil
@full_post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})\\z", @regexp_options) : nil
regexp_ = opts_[:expected_follower_regexp] || ''
@follower_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})", @regexp_options) : nil
@default_delimiter = opts_[:default_delimiter] || '.'
@default_post_delimiter = opts_[:default_post_delimiter] || ''
@requires_previous_field = opts_.fetch(:requires_previous_field, true)
@requires_next_field = opts_.fetch(:requires_next_field, false)
name_ = field_.name
@delim_unparse_param_key = "#{name_}_delim".to_sym
@post_delim_unparse_param_key = "#{name_}_postdelim".to_sym
@required_unparse_param_key = "#{name_}_required".to_sym
end
# Attempt to parse the field from the string if the syntax matches
# this recognizer's configuration.
# Returns either nil, indicating that this recognizer doesn't match
# the given syntax, or a two element array of the value and style.
def parse(parse_state_, parse_params_)
return nil if @requires_previous_field && parse_state_[:previous_field_missing]
string_ = parse_state_[:string]
if @delimiter_regexp
match_ = @delimiter_regexp.match(string_)
return nil unless match_
delim_ = match_[0]
string_ = match_.post_match
else
delim_ = ''
end
match_ = @value_regexp.match(string_)
return nil unless match_
value_ = match_[0]
string_ = match_.post_match
if @post_delimiter_regexp
match_ = @post_delimiter_regexp.match(string_)
return nil unless match_
post_delim_ = match_[0]
string_ = match_.post_match
else
post_delim_ = nil
end
if @follower_regexp
match_ = @follower_regexp.match(string_)
return nil unless match_
end
parse_result_ = parsed_value(value_, parse_params_)
return nil unless parse_result_
unparse_params_ = parse_result_[1] || {}
if delim_ != @default_delimiter
unparse_params_[@delim_unparse_param_key] = delim_
end
if post_delim_ && post_delim_ != @default_post_delimiter
unparse_params_[@post_delim_unparse_param_key] = post_delim_
end
unparse_params_[@required_unparse_param_key] = true if @default_value_optional
[parse_result_[0], @style, string_, unparse_params_]
end
# Returns true if this field can appear in an unparsed string only
# if the next field also appears.
def requires_next_field
@requires_next_field
end
# Returns true if this recognizer should be used to unparse the
# given value and style.
def should_unparse?(value_, style_)
style_ == @style
end
# Unparse the given value in the given style, and return a string
# fragment, or nil if the field is determined to be "optional" to
# unparse and isn't otherwise required (because a later field needs
# it to be present, for example).
#
# It is guaranteed that this will be called only if should_unparse?
# returns true.
def unparse(value_, style_, unparse_params_, required_for_later_)
str_ = nil
if !@default_value_optional || value_ != @default_value ||
required_for_later_ || unparse_params_[@required_unparse_param_key]
then
str_ = unparsed_value(value_, style_, unparse_params_)
if str_
if !@full_delimiter_regexp
delim_ = ''
else
delim_ = unparse_params_[@delim_unparse_param_key] || @default_delimiter
if @full_delimiter_regexp !~ delim_
delim_ = @default_delimiter
end
end
if !@full_post_delimiter_regexp
post_delim_ = ''
else
post_delim_ = unparse_params_[@post_delim_unparse_param_key] || @default_post_delimiter
if @full_post_delimiter_regexp !~ post_delim_
post_delim_ = @default_post_delimiter
end
end
str_ = delim_ + str_ + post_delim_
end
str_
else
nil
end
end
end
# A recognizer for a numeric integer field
class BasicIntegerRecognizer < RecognizerBase #:nodoc:
def initialize(field_, opts_={})
setup(field_, '\d+', opts_)
end
def parsed_value(value_, parse_params_)
[value_.to_i, nil]
end
def unparsed_value(value_, style_, unparse_params_)
value_.to_s
end
end
# A recognizer for an alphabetic integer field. Such a field
# represents values 1-26 as letters of the English alphabet.
class AlphabeticIntegerRecognizer < RecognizerBase # :nodoc:
def initialize(field_, opts_={})
@case_unparse_param_key = "#{field_.name}_case".to_sym
@case = opts_[:case]
case @case
when :upper
value_regexp_ = '[A-Z]'
when :lower
value_regexp_ = '[a-z]'
else #either
value_regexp_ = '[a-zA-Z]'
end
setup(field_, value_regexp_, opts_)
end
def parsed_value(value_, parse_params_)
value_ = value_.unpack('c')[0] # Compatible with both 1.8 and 1.9
if value_ >= 97 && value_ <= 122
[value_ - 96, {@case_unparse_param_key => :lower}]
elsif value_ >= 65 && value_ <= 90
[value_ - 64, {@case_unparse_param_key => :upper}]
else
[0, nil]
end
end
def unparsed_value(value_, style_, unparse_params_)
if value_ >= 1 && value_ <= 26
if unparse_params_[@case_unparse_param_key] == :upper
(value_+64).chr
else
(value_+96).chr
end
else
value_.to_s
end
end
end
# A recognizer for strings that match a particular given regular
# expression, for use in string-valued fields.
class RegexpStringRecognizer < RecognizerBase # :nodoc:
def initialize(field_, regexp_='[a-zA-Z0-9]+', opts_={})
setup(field_, regexp_, opts_)
end
def parsed_value(value_, parse_params_)
[value_, nil]
end
def unparsed_value(value_, style_, unparse_params_)
value_.to_s
end
end
# A recognizer for symbolic fields that recognizes a single regular
# expression and maps it to a single particular value.
class RegexpSymbolRecognizer < RecognizerBase # :nodoc:
def initialize(field_, value_, regexp_, canonical_, opts_={})
setup(field_, regexp_, opts_)
@value = value_
@canonical = canonical_
end
def parsed_value(value_, parse_params_)
[@value, nil]
end
def unparsed_value(value_, style_, unparse_params_)
@canonical
end
def should_unparse?(value_, style_)
style_ == @style && value_ == @value
end
end
# A recognizer for symbolic fields that recognizes a mapping of values
# to regular expressions.
class MappingSymbolRecognizer < RecognizerBase # :nodoc:
def initialize(field_, opts_={}, &block_)
@mappings_in_order = []
@mappings_by_value = {}
builder_ = Delimiter::MappingSymbolBuilder.new(@mappings_in_order, @mappings_by_value)
::Blockenspiel.invoke(block_, builder_)
regexps_ = @mappings_in_order.map{ |map_| "(#{map_[0]})" }
setup(field_, regexps_.join('|'), opts_)
@mappings_in_order.each do |map_|
map_[0] = ::Regexp.new("\\A(#{map_[0]})", @regexp_options)
end
end
def parsed_value(value_, parse_params_)
@mappings_in_order.each do |map_|
return [map_[2], nil] if map_[0].match(value_)
end
nil
end
def unparsed_value(value_, style_, unparse_params_)
@mappings_by_value[value_][1]
end
def should_unparse?(value_, style_)
style_ == @style && @mappings_by_value.include?(value_)
end
end
end
end
end