Permalink
Browse files

RT#55628: Improve flexibility of date parsing.

This adds the ability to pass any ISO 8601 string to the
RPC::XML::datetime_iso8601 constructor.
  • Loading branch information...
1 parent cc02b29 commit c2e33bcb41fd923f74581b70bca2849dbd91a679 @rjray committed Aug 21, 2011
Showing with 220 additions and 31 deletions.
  1. +1 −0 MANIFEST
  2. +87 −23 lib/RPC/XML.pm
  3. +12 −8 t/10_data.t
  4. +120 −0 t/14_datetime_iso8601.t
View
@@ -53,6 +53,7 @@ t/10_data.t
t/11_base64_fh.t
t/12_nil.t
t/13_no_deep_recursion.t
+t/14_datetime_iso8601.t
t/15_serialize.t
t/20_xml_parser.t
t/21_xml_libxml.t
View
@@ -27,7 +27,7 @@ use strict;
use warnings;
use vars qw(@EXPORT_OK %EXPORT_TAGS $VERSION $ERROR
%XMLMAP $XMLRE $ENCODING $FORCE_STRING_ENCODING $ALLOW_NIL
- $DATETIME_REGEXP);
+ $DATETIME_REGEXP $DATETIME_ISO8601_AVAILABLE);
use subs qw(time2iso8601 smart_encode);
use base 'Exporter';
@@ -47,6 +47,11 @@ BEGIN
# Allow the <nil /> extension?
$ALLOW_NIL = 0;
+
+ # Determine if the DateTime::Format::ISO8601 module is available for
+ # RPC::XML::datetime_iso8601 to use:
+ my $retval = eval 'use DateTime::Format::ISO8601; 1;';
+ $DATETIME_ISO8601_AVAILABLE = $retval ? 1 : 0;
}
@EXPORT_OK = qw(time2iso8601 smart_encode
@@ -58,7 +63,7 @@ BEGIN
RPC_NIL) ],
all => [ @EXPORT_OK ]);
-$VERSION = '1.55';
+$VERSION = '1.56';
$VERSION = eval $VERSION; ## no critic (ProhibitStringyEval)
# Global error string
@@ -89,10 +94,10 @@ my $time_re =
qr{
([012]\d):
([0-5]\d):
- ([0-5]\d)([.]\d+)?
+ ([0-5]\d)([.,]\d+)?
(Z|[-+]\d\d:\d\d)?
}x;
-$DATETIME_REGEXP = qr{^${date_re}T${time_re}$};
+$DATETIME_REGEXP = qr{^${date_re}T?${time_re}$};
# All of the RPC_* functions are convenience-encoders
sub RPC_STRING ($)
@@ -203,7 +208,7 @@ sub time2iso8601
{
# Must be a DateTime object, convert to ISO8601
$type = RPC::XML::datetime_iso8601
- ->new($_->clone->set_time_zone('UTC')->iso8601);
+ ->new($_->clone->set_time_zone('UTC'));
}
}
elsif (reftype($_) eq 'HASH')
@@ -575,33 +580,52 @@ sub type { return 'dateTime.iso8601'; };
sub new
{
my ($class, $value) = @_;
+ my $newvalue;
if (ref($value) && reftype($value) eq 'SCALAR')
{
$value = ${$value};
}
- if ($value && $value =~ /$RPC::XML::DATETIME_REGEXP/)
+ if (defined $value)
{
- # This is the WRONG way to represent this, but it's the way it is
- # given in the spec, so assume that other implementations can only
- # accept this form. Also, this should match the form that time2iso8601
- # produces.
- $value = $7 ? "$1$2$3T$4:$5:$6$7" : "$1$2$3T$4:$5:$6";
- if ($8)
+ if ($value =~ /$RPC::XML::DATETIME_REGEXP/)
+ {
+ # This is *not* a valid ISO 8601 format, but it's the way it is
+ # given in the spec, so assume that other implementations can only
+ # accept this form. Also, this should match the form that
+ # time2iso8601 produces.
+ $newvalue = $7 ? "$1$2$3T$4:$5:$6$7" : "$1$2$3T$4:$5:$6";
+ if ($8) {
+ $newvalue .= $8;
+ }
+ }
+ elsif ($RPC::XML::DATETIME_ISO8601_AVAILABLE)
+ {
+ $newvalue =
+ eval { DateTime::Format::ISO8601->parse_datetime($value) };
+ if ($newvalue)
+ {
+ # This both removes the dashes (*sigh*) and forces it from an
+ # object to an ordinary string:
+ $newvalue =~ s/-//g;
+ }
+ }
+
+ if (! $newvalue)
{
- $value .= $8;
+ $RPC::XML::ERROR = "${class}::new: Malformed data ($value) " .
+ 'passed as dateTime.iso8601';
+ return;
}
}
else
{
- $RPC::XML::ERROR = "${class}::new: Malformed data (" .
- (defined($value) ? $value : '<undef>') .
- ') passed as dateTime.iso8601';
+ $RPC::XML::ERROR = "${class}::new: Value required in constructor";
return;
}
- return bless \$value, $class;
+ return bless \$newvalue, $class;
}
###############################################################################
@@ -1601,7 +1625,7 @@ This module provides a set of classes for creating values to pass to the
constructors for requests and responses. These are lightweight objects, most of
which are implemented as blessed scalar references so as to associate specific
type information with the value. Classes are also provided for requests,
-responses and faults (errors) and a parsers .
+responses and faults (errors).
This module does not actually provide any transport implementation or server
basis. For these, see L<RPC::XML::Client|RPC::XML::Client> and
@@ -1617,10 +1641,12 @@ imported as part of the C<use> statement, or with a direct call to C<import>:
=item time2iso8601([$time])
Convert the integer time value in C<$time> (which defaults to calling the
-built-in C<time> if not present) to a ISO 8601 string in the UTC time
-zone. This is a convenience function for occassions when the return value
-needs to be of the B<dateTime.iso8601> type, but the value on hand is the
-return from the C<time> built-in.
+built-in C<time> if not present) to a (pseudo) ISO 8601 string in the UTC time
+zone. This is a convenience function for occassions when the return value needs
+to be of the B<dateTime.iso8601> type, but the value on hand is the return from
+the C<time> built-in. Note that the format of this string is not strictly
+compliant with ISO 8601 due to the way the B<dateTime.iso8601> data-type was
+defined in the specification. See L</"DATES AND TIMES">, below.
=item smart_encode(@args)
@@ -1764,7 +1790,8 @@ for ISO 8601 may be found elsewhere. No processing is done to the data. Note
that the XML-RPC specification actually got the format of an ISO 8601 date
slightly wrong. Because this is what is in the published spec, this package
produces dates that match the XML-RPC spec, not the the ISO 8601 spec. However,
-it will I<read> date-strings in proper ISO 8601 format.
+it will I<read> date-strings in proper ISO 8601 format. See L</"DATES AND
+TIMES">, below.
=item RPC::XML::nil
@@ -1945,6 +1972,43 @@ provided for clarity and simplicity.
=back
+=head1 DATES AND TIMES
+
+The XML-RPC specification refers to the date/time values as ISO 8601, but
+unfortunately got the syntax slightly wrong in the examples. However, since
+this is the published specification it is necessary to produce time-stamps that
+conform to this format. The specification implies that the only format for
+date/time values is:
+
+ YYYYMMDDThh:mm:ss
+
+(Here, the C<T> is literal, the rest represent elements of the date and time.)
+However, the ISO 8601 specification does not allow this particular format, and
+in generally is I<considerably> more flexible than this. Yet there are
+implementations of the XML-RPC standard in other languages that rely on a
+strict interpretation of this format.
+
+To accomodate this, the B<RPC::XML> package only produces B<dateTime.iso8601>
+values in the format given in the spec, with the possible addition of timezone
+information if the string used to create a B<RPC::XML::datetime_iso8601>
+instance included a timezone offset. The string passed in to the constructor
+for that class must match:
+
+ \d\d\d\d-?\d\d-?\d\dT?\d\d:\d\d:\d\d([.,]\d+)?(Z|[-+]\d\d:\d\d)?
+
+This pattern is also used by B<smart_encode> to distinguish a date/time string
+from a regular string. Note that the C<T> is optional here, as it is in the
+ISO 8601 spec. The timezone is optional, and if it is not given then UTC is
+assumed. The XML-RPC specification says not to assume anything about the
+timezone in the absence of one, but the format of ISO 8601 declares that that
+absence of an explicit timezone dictates UTC.
+
+If you have L<DateTime::Format::ISO8601|DateTime::Format::ISO8601> installed,
+then B<RPC::XML::datetime_iso8601> will fall back on it to try and parse any
+input strings that do not match the above pattern. If the string cannot be
+parsed by the B<DateTime::Format::ISO8601> module, then the constructor returns
+B<undef> and B<$RPC::XML::ERROR> is set.
+
=head1 DIAGNOSTICS
All constructors (in all data classes) return C<undef> upon failure, with the
View
@@ -7,7 +7,7 @@ use vars qw($val $str $fh $obj $class %val_tbl @values $datetime_avail);
use Config;
-use Test::More tests => 250;
+use Test::More tests => 252;
use File::Spec;
use RPC::XML ':all';
@@ -129,9 +129,9 @@ for (qw(0 1 yes no tRuE FaLsE))
}
# This should not
$obj = RPC::XML::boolean->new('of course!');
-ok(! ref $obj, "RPC::XML::boolean, bad value did not yield referent");
+ok(! ref $obj, 'RPC::XML::boolean, bad value did not yield referent');
like($RPC::XML::ERROR, qr/::new: Value must be one of/,
- "RPC::XML::boolean, bad value correctly set \$RPC::XML::ERROR");
+ 'RPC::XML::boolean, bad value correctly set $RPC::XML::ERROR');
# The dateTime.iso8601 type
$val = time2iso8601(time);
@@ -160,12 +160,16 @@ is(length($obj->as_string), $obj->length,
"RPC::XML::datetime_iso8601, length() method test");
is($obj->value, $val, 'RPC::XML::datetime_iso8601, value() method test');
# Test bad date-data
-substr($val, -5, 5) = ''; # Drop the Z and the fractional
-$val .= '-07:00'; # Add a specification of a time zone that isn't UTC
$obj = RPC::XML::datetime_iso8601->new();
-ok(! ref $obj, "RPC::XML::datetime_iso8601, bad value did not yield referent");
-like($RPC::XML::ERROR, qr/::new: Malformed data.*passed/,
- 'RPC::XML::datetime_iso8601, bad value correctly set \$RPC::XML::ERROR');
+ok(! ref $obj,
+ 'RPC::XML::datetime_iso8601, empty value did not yield referent');
+like($RPC::XML::ERROR, qr/::new: Value required/,
+ 'RPC::XML::datetime_iso8601, empty value correctly set $RPC::XML::ERROR');
+$obj = RPC::XML::datetime_iso8601->new('not a date');
+ok(! ref $obj,
+ 'RPC::XML::datetime_iso8601, bad value did not yield referent');
+like($RPC::XML::ERROR, qr/::new: Malformed data/,
+ 'RPC::XML::datetime_iso8601, empty value correctly set $RPC::XML::ERROR');
# Test the slightly different date format
$obj = RPC::XML::datetime_iso8601->new('2008-09-29T12:00:00-07:00');
isa_ok($obj, 'RPC::XML::datetime_iso8601', '$obj');
@@ -0,0 +1,120 @@
+#!/usr/bin/perl
+
+# Test the date-parsing facilities provided by the DateTime::Format::ISO8601
+# module, if available
+
+use strict;
+use vars qw($obj @values $formatter);
+
+use Test::More;
+
+use RPC::XML;
+
+eval "use DateTime::Format::ISO8601";
+# Do not run this suite if the package is not available
+plan skip_all => 'DateTime::Format::ISO8601 not available' if $@;
+
+# Otherwise, we have to calculate our tests from the content after __DATA__:
+while (defined(my $line = <DATA>))
+{
+ next if ($line =~ /^#/);
+ chomp $line;
+ next if ($line =~ /^$/);
+ push @values, [ split /[|]/, $line ];
+}
+
+plan tests => (scalar(@values) * 2);
+
+# Create a formatter from the DateTime::Format::ISO8601 package, we'll use it
+# to determine what the constructor *should* return:
+$formatter = DateTime::Format::ISO8601->new();
+
+for my $test (0 .. $#values)
+{
+ my ($input, $is_error) = @{$values[$test]};
+
+ $obj = RPC::XML::datetime_iso8601->new($input);
+ if (! $is_error)
+ {
+ my $match = $formatter->parse_datetime($input);
+ $match =~ s/-//g;
+
+ isa_ok($obj, 'RPC::XML::datetime_iso8601', "Input $test \$obj");
+ is($obj->value, $match, "Input '$input' yielded correct value");
+ }
+ else
+ {
+ ok(! ref($obj), "Input $test yielded no object");
+ like($RPC::XML::ERROR, qr/Malformed data [(]$input[)]/,
+ "Input '$input' yielded correct error message");
+ }
+}
+
+exit 0;
+
+__DATA__
+# Format is:
+# <Input value>|<Error if set>
+#
+# If the second field is non-blank, then the input should yield an error
+#
+# I am skipping some of the sillier formats, as I don't care if people use them
+# and get unexpected results. Caveat Programmer, and all that...
+20110820
+2011-08-20
+2011-08
+2011
+110820
+11-08-20
+-1108
+-11-08
+--0820
+--08-20
+--08
+---20
+2011232
+2011-232
+11232
+11-232
+-232
+2011W336
+2011-W33-6
+2011W33
+2011-W33
+11W336
+11-W33-6
+11W33
+11-W33
+-1W336
+-1-W33-6
+-1W33
+-1-W33
+-W336
+-W33-6
+17:55:55
+17:55
+175555,50
+17:55:55,50
+175555.50
+1755.50
+17:55.50
+17.50
+-55:00
+-5500,50
+-55.50
+--00.0
+175555Z
+17:55:55Z
+1755Z
+17:55Z
+17Z
+175555.0Z
+17:55:55.0Z
+175555-0700
+17:55:55-07:00
+175555-07
+17:55:55-07
+175555.0-0700
+17:55:55.0-07:00
+17,01|bad
+20110820175555|bad

0 comments on commit c2e33bc

Please sign in to comment.