chiark / gitweb /
Merge commit 'fd2a22d3d98dc195d27c4825e2ac28879e230b9f' into HEAD
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 6 May 2020 19:28:14 +0000 (20:28 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 6 May 2020 19:28:14 +0000 (20:28 +0100)
TOML-Tiny/README.pod
TOML-Tiny/cpanfile
TOML-Tiny/lib/TOML/Tiny.pm
TOML-Tiny/lib/TOML/Tiny/Faithful.pm [new file with mode: 0644]
TOML-Tiny/lib/TOML/Tiny/Tokenizer.pm
TOML-Tiny/lib/TOML/Tiny/Writer.pm
TOML-Tiny/t/faithful.t [new file with mode: 0644]

index 911f6db41c8d6e3da2b495df82b43ebf65936c96..b04c46de8bffb84a7c1efe1ea1c5bbb8fdebe261 100644 (file)
@@ -134,8 +134,9 @@ If you wish to override this, you can provide your own routine to generate value
 =item inflate_integer
 
 TOML integers are 64 bit and may not match the size of the compiled perl's
-internal integer type. By default, integers are left as-is as perl strings
-which may be upgraded as needed by the caller.
+internal integer type. By default, integers other than smallish
+decimal integers are left as-is as perl strings which may be upgraded
+as needed by the caller.
 
   my $parser = TOML::Tiny->new(
     inflate_integer => sub{
@@ -168,6 +169,31 @@ wish to require strictly typed arrays (for C<TOML>'s definition of "type",
 anyway), C<strict_arrays> will produce an error when encountering arrays with
 heterogenous types.
 
+=item no_string_guessing
+
+When encoding a Perl scalar it is not always clear what the TOML type
+of the value is supposed to be.  By default, C<TOML::Tiny> will, for
+unblessed scalars, guess based on the scalar's appearance.  Strings
+that look like numbers, or like datetimes, will be encoded as such.
+
+With no_string_guessing, C<TOML::Tiny> will look at the perl innards
+to find the currently stored value type.  If it is a number, the
+scalar will be encoded as a number.  If it's a string, as a string.
+Dates and times which weren't built with DateTime come out as strings.
+
+Specifying C<inflate_float>, C<inflate_integer>, and
+C<inflate_datetime> is likely to be helpful with this option.
+
+=item datetime_formatter
+
+When encoding a DateTime object, by default C<TOML::Tiny> will use the
+default formatter.  This is not right for TOML which requires RFC3339.
+If you have C<DateTime::Format::RFC3339> available, use this instead:
+
+  my $parser = TOML::Tiny->new(
+    datetime_formatter => DateTime::Format::RFC3339->new(),
+  );
+
 =back
 
 =head2 decode
index ee63b90306e9b9cd4f3336e5c842004ae973b3cf..5388c769dcb75f48a1b313e9dc5995084148b4ca 100644 (file)
@@ -10,6 +10,8 @@ recommends 'Types::Serialiser' => 0;
 on test => sub{
   requires 'Data::Dumper'              => '0';
   requires 'DateTime::Format::RFC3339' => '0';
+  requires 'DateTime::Format::ISO8601' => '0';
+  requires 'Types::Serialiser'         => '0';
   requires 'Math::BigInt'              => '>= 1.999718';
   requires 'TOML::Parser'              => '0';
   requires 'Test2::V0'                 => '0';
index 6cd88be24cf5019375e1a1c7362eac76f9b6111f..89acb36695a4c6a365e21f23ef57ccb6fb5e40d3 100644 (file)
@@ -52,6 +52,8 @@ sub encode {
   my ($self, $data) = @_;
   TOML::Tiny::Writer::to_toml($data,
     strict_arrays => $self->{strict_arrays},
+    datetime_formatter => $self->{datetime_formatter},
+    no_string_guessing => $self->{no_string_guessing},
   );
 }
 
@@ -190,8 +192,9 @@ If you wish to override this, you can provide your own routine to generate value
 =item inflate_integer
 
 TOML integers are 64 bit and may not match the size of the compiled perl's
-internal integer type. By default, integers are left as-is as perl strings
-which may be upgraded as needed by the caller.
+internal integer type. By default, integers other than smallish
+decimal integers are left as-is as perl strings which may be upgraded
+as needed by the caller.
 
   my $parser = TOML::Tiny->new(
     inflate_integer => sub{
@@ -224,6 +227,31 @@ wish to require strictly typed arrays (for C<TOML>'s definition of "type",
 anyway), C<strict_arrays> will produce an error when encountering arrays with
 heterogenous types.
 
+=item no_string_guessing
+
+When encoding a Perl scalar it is not always clear what the TOML type
+of the value is supposed to be.  By default, C<TOML::Tiny> will, for
+unblessed scalars, guess based on the scalar's appearance.  Strings
+that look like numbers, or like datetimes, will be encoded as such.
+
+With no_string_guessing, C<TOML::Tiny> will look at the perl innards
+to find the currently stored value type.  If it is a number, the
+scalar will be encoded as a number.  If it's a string, as a string.
+Dates and times which weren't built with DateTime come out as strings.
+
+Specifying C<inflate_float>, C<inflate_integer>, and
+C<inflate_datetime> is likely to be helpful with this option.
+
+=item datetime_formatter
+
+When encoding a DateTime object, by default C<TOML::Tiny> will use the
+default formatter.  This is not right for TOML which requires RFC3339.
+If you have C<DateTime::Format::RFC3339> available, use this instead:
+
+  my $parser = TOML::Tiny->new(
+    datetime_formatter => DateTime::Format::RFC3339->new(),
+  );
+
 =back
 
 =head2 decode
diff --git a/TOML-Tiny/lib/TOML/Tiny/Faithful.pm b/TOML-Tiny/lib/TOML/Tiny/Faithful.pm
new file mode 100644 (file)
index 0000000..e372d5d
--- /dev/null
@@ -0,0 +1,108 @@
+
+package TOML::Tiny::Faithful;
+use parent TOML::Tiny;
+use DateTime::Format::ISO8601;
+use DateTime::Format::RFC3339;
+
+use DateTime;
+use Types::Serialiser; # ensures that Parser DTRT with booleans
+
+our @EXPORT = qw(
+  from_toml
+  to_toml
+);
+
+sub _options {
+  inflate_datetime => sub {
+    # RFC3339 bombs out if there is no timezone, so we parse with 8601
+    DateTime::Format::ISO8601->parse_datetime(shift)
+  },
+  inflate_integer => sub { 
+    use bignum;
+    my $s = shift;
+    $s =~ m/^0o/
+       ? Math::BigInt->from_oct($')
+       : Math::BigInt->new($s);
+  },
+  inflate_float => sub { 0. + shift; },
+  no_string_guessing => 1,
+  datetime_formatter => TOML::Tiny::Faithful::DateTime::Formatter->new(),
+}
+
+sub new {
+  my ($class, %param) = @_;
+  bless TOML::Tiny->new(_options(), %param), $class;
+}
+sub from_toml {
+  my $source = shift;
+  TOML::Tiny::from_toml($source, _options(), @_);
+}
+sub to_toml {
+  my $source = shift;
+  TOML::Tiny::to_toml($source, _options(), @_);
+}
+
+package TOML::Tiny::Faithful::DateTime::Formatter;
+use DateTime::Format::RFC3339;
+
+our $base = DateTime::Format::RFC3339->new();
+
+sub new ($) {
+    my ($class) = @_;
+    bless { }, $class;
+}
+
+sub format_datetime {
+    my ($self,$dt) = @_;
+    # RFC3339 always prints a timezone.  This is correct for RFC3339
+    # but in our application we sometimes have "local datetime"s
+    # where the time_zone is DateTime::TimeZone::Floating.
+    # We could use ISO8601 but it never prints the nanoseconds.
+    # It is easier to strip the timezone offset than add the ns.
+    my $r = DateTime::Format::RFC3339->new()->format_datetime($dt);
+    if ((ref $dt->time_zone()) =~ m/Floating/) {
+       $r =~ s/\+[0-9:.]+$//;
+    }
+    $r
+}
+
+1;
+
+=head1 SYNOPSIS
+
+  use TOML::Tiny::Faithful qw(from_toml to_toml);
+
+  binmode STDIN,  ':encoding(UTF-8)';
+  binmode STDOUT, ':encoding(UTF-8)';
+
+  # Decoding TOML
+  my $toml = do{ local $/; <STDIN> };
+  my ($parsed, $error) = from_toml $toml;
+
+  # Encoding TOML
+  say to_toml({
+    stuff => {
+      about => ['other', 'stuff'],
+    },
+  });
+
+  # Object API
+  my $parser = TOML::Tiny::Faithful->new;
+  my $data = $parser->decode($toml);
+  say $parser->encode($data);
+
+
+=head1 DESCRIPTION
+
+C<TOML::Tiny::Faithful> is a trivial wrapper around C<TOML::Tiny>
+which sets C<inflate_integer>, C<inflate_float>, C<inflate_datetime>,
+C<no_string_guessing> and C<datetime_formatter> to try to make the
+TOML output faithful to any input TOML.
+
+=head1 SEE ALSO
+
+=over
+
+=item L<TOML::Tiny>
+
+=back
index e2deb91d6ae213328149d0200a071746735178e1..b9b3ee414a13f7f1575a1bcb7650721803b693e2 100644 (file)
@@ -178,7 +178,7 @@ sub tokenize_float {
 
 sub tokenize_integer {
   $_[1] =~ tr/_+//d;
-  $_[1];
+  $_[1] !~ m/^0[xob]/ && $_[1] + 0 eq $_[0] ? $_[1] + 0 : "$_[1]"
 }
 
 sub tokenize_string {
index 66249a855f8b19359e6a4647aa698829549f8be9..4e2275add7c5fead85b3ea332dda437dcc96c553 100644 (file)
@@ -12,6 +12,8 @@ use TOML::Tiny::Util qw(is_strict_array);
 
 my @KEYS;
 
+use B qw( svref_2object SVf_IOK SVf_NOK );
+
 sub to_toml {
   my $data = shift;
   my %param = @_;
@@ -108,7 +110,8 @@ sub to_toml {
     }
 
     when (/DateTime/) {
-      return $data->stringify;
+      my $formatter = $param{datetime_formatter};
+      return $formatter ? $formatter->format_datetime($data) : "$data";
     }
 
     when ('Math::BigInt') {
@@ -119,6 +122,20 @@ sub to_toml {
       return $data->bnstr;
     }
 
+    when (!! $param{no_string_guessing}) {
+      # Thanks to ikegami on Stack Overflow for the trick!
+      # https://stackoverflow.com/questions/12686335/how-to-tell-apart-numeric-scalars-and-string-scalars-in-perl/12693984#12693984
+
+      my $sv = svref_2object(\$data);
+      my $svflags = $sv->FLAGS;
+
+      if ($svflags & (SVf_IOK | SVf_NOK)) {
+       return $data;
+      } else {
+       return to_toml_string($data);
+      }
+    }
+
     when ('') {
       for ($data) {
         when (looks_like_number($_)) {
diff --git a/TOML-Tiny/t/faithful.t b/TOML-Tiny/t/faithful.t
new file mode 100644 (file)
index 0000000..195c1db
--- /dev/null
@@ -0,0 +1,60 @@
+use utf8;
+use Test2::V0;
+use Data::Dumper;
+use DateTime;
+use DateTime::Format::RFC3339;
+use Math::BigInt;
+use Math::BigFloat;
+use TOML::Tiny::Faithful;
+
+binmode STDIN,  ':encoding(UTF-8)';
+binmode STDOUT, ':encoding(UTF-8)';
+
+my $input = q{
+datetime=2020-05-04T16:37:02.905408062+01:00
+datetimes="2020-05-04T16:37:02.905408062+01:00"
+float=3.14
+floats="3.14"
+uint=3
+uints="3"
+nint=-4
+nints="-4"
+bigint=1852528528562625752750
+bigints="1852528528562625752750"
+hex=0x12
+oct=0o751
+bin=0b11010110
+boolf=false
+boolt=true
+boolfs="false"
+boolts="true"
+dtlocal=1979-05-27T00:32:00.643144312
+dtlocals="1979-05-27T00:32:00.643144312"
+};
+
+sub norm ($) {
+  join "\n", (
+    sort
+    map {
+      s{=0o(\d+)$}{ '='.oct($1) }e;
+      s{=(0[xb]\w+)$}{ '='.eval($1) }e;
+      $_;
+    }
+    grep /./,
+    split /\n/, $_[0]
+  ), ''
+}
+
+my $parsed = from_toml($input);
+my $actual = norm(to_toml($parsed));
+my $expected = norm($input);
+
+is($actual, $expected, 'round trip') or do{
+  diag 'EXPECTED:';
+  diag Dumper($expected);
+
+  diag 'ACTUAL:';
+  diag Dumper($actual);
+};
+
+done_testing;