chiark / gitweb /
Add support for generating TOML from a data structure
authorJeff Ober <jober@ziprecruiter.com>
Wed, 8 Jan 2020 21:06:03 +0000 (16:06 -0500)
committerJeff Ober <jober@ziprecruiter.com>
Wed, 8 Jan 2020 21:06:03 +0000 (16:06 -0500)
cpanfile
lib/TOML/Tiny.pm
lib/TOML/Tiny/Writer.pm [new file with mode: 0644]
t/writer.t [new file with mode: 0644]

index f3e9b048138b46167412ae468aa6d63bde4af618..ad0bd7495f0e6043088c0790edcc9b96035baae3 100644 (file)
--- a/cpanfile
+++ b/cpanfile
@@ -1,4 +1,5 @@
 requires 'perl' => '>= 5.014';
+requires 'Scalar::Util' => '>= 1.14';
 
 recommends 'Types::Serialiser' => 0;
 
index ef1b5624117faaf00f1c7d48647b1c90ad77baf1..b67199b5cd208ed46c8574af2bd996a568d842c8 100644 (file)
@@ -3,7 +3,11 @@ package TOML::Tiny;
 
 use strict;
 use warnings;
+use feature qw(switch);
+no warnings qw(experimental);
+
 use TOML::Tiny::Parser;
+use TOML::Tiny::Writer;
 
 use parent 'Exporter';
 
@@ -16,11 +20,16 @@ sub from_toml {
   my $source = shift;
   my $parser = TOML::Tiny::Parser->new(@_);
   my $toml = eval{ $parser->parse($source) };
-  return ($toml, $@);
+  if (wantarray) {
+    return ($toml, $@);
+  } else {
+    die $@ if $@;
+    return $toml;
+  }
 }
 
 sub to_toml {
-  my $data = shift;
+  goto \&TOML::Tiny::Writer::to_toml;
 }
 
 1;
diff --git a/lib/TOML/Tiny/Writer.pm b/lib/TOML/Tiny/Writer.pm
new file mode 100644 (file)
index 0000000..11e2bb3
--- /dev/null
@@ -0,0 +1,146 @@
+package TOML::Tiny::Writer;
+
+use strict;
+use warnings;
+
+use feature qw(switch state);
+no warnings qw(experimental);
+
+use Data::Dumper;
+use Scalar::Util qw(looks_like_number);
+use TOML::Tiny::Grammar;
+
+my @KEYS;
+
+sub to_toml {
+  my $data = shift;
+  my @buff;
+
+  for (ref $data) {
+    when ('HASH') {
+      for my $k (grep{ ref($data->{$_}) !~ /HASH|ARRAY/ } sort keys %$data) {
+        my $key = to_toml_key($k);
+        my $val = to_toml($data->{$k});
+        push @buff, "$key=$val";
+      }
+
+      for my $k (grep{ ref $data->{$_} eq 'ARRAY' } sort keys %$data) {
+        my @inline;
+        my @table_array;
+
+        for my $v (@{$data->{$k}}) {
+          if (ref $v eq 'HASH') {
+            push @table_array, $v;
+          } else {
+            push @inline, $v;
+          }
+        }
+
+        if (@inline) {
+          my $key = to_toml_key($k);
+          my $val = to_toml(\@inline);
+          push @buff, "$key=$val";
+        }
+
+        if (@table_array) {
+          push @KEYS, $k;
+
+          for (@table_array) {
+            push @buff, '', '[[' . join('.', map{ to_toml_key($_) } @KEYS) . ']]';
+
+            for my $k (sort keys %$_) {
+              my $key = to_toml_key($k);
+              my $val = to_toml($_->{$k});
+              push @buff, "$key=$val";
+            }
+          }
+
+          pop @KEYS;
+        }
+      }
+
+      for my $k (grep{ ref $data->{$_} eq 'HASH' } sort keys %$data) {
+        push @KEYS, $k;
+        push @buff, '', '[' . join('.', map{ to_toml_key($_) } @KEYS) . ']';
+        push @buff, to_toml($data->{$k});
+        pop @KEYS;
+      }
+
+    }
+
+    when ('ARRAY') {
+      push @buff, '[' . join(', ', map{ to_toml($_) } @$data) . ']';
+    }
+
+    when ('SCALAR') {
+      if ($$_ eq '1') {
+        return 'true';
+      } elsif ($$_ eq '0') {
+        return 'false';
+      } else {
+        push @buff, to_toml($$_);
+      }
+    }
+
+    when (/JSON::PP::Boolean/) {
+      return $$data ? 'true' : 'false';
+    }
+
+    when ('') {
+      for ($data) {
+        when (looks_like_number($_)) {
+          return $data;
+        }
+
+        when (/(?&DateTime) $TOML/x) {
+          return $data;
+        }
+
+        default{
+          return to_toml_string($data);
+        }
+      }
+    }
+
+    when (defined) {
+      die 'unhandled: '.Dumper($_);
+    }
+
+    default{
+      die 'unhandled: '.Dumper($_);
+    }
+  }
+
+  join "\n", @buff;
+}
+
+sub to_toml_key {
+  my $str = shift;
+
+  if ($str =~ /^[-_A-Za-z0-9]+$/) {
+    return $str;
+  }
+
+  return qq{"$str"};
+}
+
+sub to_toml_string {
+  state $escape = {
+    "\n" => '\n',
+    "\r" => '\r',
+    "\t" => '\t',
+    "\f" => '\f',
+    "\b" => '\b',
+    "\"" => '\"',
+    "\\" => '\\\\',
+    "\'" => '\\\'',
+  };
+
+  my ($arg) = @_;
+  $arg =~ s/([\x22\x5c\n\r\t\f\b])/$escape->{$1}/g;
+  $arg =~ s/([\x00-\x08\x0b\x0e-\x1f])/'\\u00' . unpack('H2', $1)/eg;
+
+  return '"' . $arg . '"';
+}
+
+1;
diff --git a/t/writer.t b/t/writer.t
new file mode 100644 (file)
index 0000000..e6bdf7c
--- /dev/null
@@ -0,0 +1,58 @@
+use Test2::V0;
+use TOML::Tiny;
+
+my $src  = do{ local $/; <DATA> };
+my $data = from_toml($src);
+my $toml = to_toml($data);
+my $got  = from_toml($toml);
+
+is $got, $data, 'to_toml <=> from_toml';
+
+done_testing;
+
+__DATA__
+# This is a TOML document.
+
+title = "TOML Example"
+
+[owner]
+name = "Tom Preston-Werner"
+dob = 1979-05-27T07:32:00-08:00 # First class dates
+
+[database]
+server = "192.168.1.1"
+ports = [ 8001, 8001, 8002 ]
+connection_max = 5000
+enabled = true
+options = {"quote-keys"=false}
+
+[servers]
+
+  # Indentation (tabs and/or spaces) is allowed but not required
+  [servers.alpha]
+  ip = "10.0.0.1"
+  dc = "eqdc10"
+
+  [servers.beta]
+  ip = "10.0.0.2"
+  dc = "eqdc10"
+
+[clients]
+data = [ ["gamma", "delta"], [1, 2] ]
+
+# Line breaks are OK when inside arrays
+hosts = [
+  "alpha",
+  "omega"
+]
+
+[[products]]
+name = "Hammer"
+sku = 738594937
+
+[[products]]
+
+[[products]]
+name = "Nail"
+sku = 284758393
+color = "gray"