File Coverage

blib/lib/TOML/Tiny/Writer.pm
Criterion Covered Total %
statement 133 148 89.8
branch 35 52 67.3
condition 3 6 50.0
subroutine 15 15 100.0
pod 0 7 0.0
total 186 228 81.5


line stmt bran cond sub pod time code
1             package TOML::Tiny::Writer;
2             $TOML::Tiny::Writer::VERSION = '0.14';
3 284     284   2325 use strict;
  284         737  
  284         11517  
4 284     284   1690 use warnings;
  284         5066  
  284         9358  
5 284     284   1604 no warnings qw(experimental);
  284         609  
  284         13982  
6 284     284   4548 use v5.18;
  284         1178  
7              
8 284     284   1823 use B qw(svref_2object SVf_IOK SVf_NOK);
  284         642  
  284         22081  
9 284     284   2113 use Data::Dumper;
  284         625  
  284         16182  
10 284     284   2033 use TOML::Tiny::Grammar;
  284         600  
  284         71908  
11 284     284   2310 use TOML::Tiny::Util qw(is_strict_array);
  284         653  
  284         382078  
12              
13             my @KEYS;
14              
15             sub to_toml {
16 757     757 0 1405 my $data = shift;
17 757 50       1994 my $param = ref($_[1]) eq 'HASH' ? $_[1] : undef;
18              
19 757         1698 for (ref $data) {
20 757         1759 when ('HASH') {
21 275         1494 return to_toml_table($data, $param);
22             }
23              
24 482         860 when ('ARRAY') {
25 64         485 return to_toml_array($data, $param);
26             }
27              
28 418         904 when ('SCALAR') {
29 0 0       0 if ($$data eq '1') {
    0          
30 0         0 return 'true';
31             } elsif ($$data eq '0') {
32 0         0 return 'false';
33             } else {
34 0         0 return to_toml($$_, $param);
35             }
36             }
37              
38 418         778 when ('JSON::PP::Boolean') {
39 15 100       117 return $$data ? 'true' : 'false';
40             }
41              
42 403         759 when ('Types::Serializer::Boolean') {
43 0 0       0 return $data ? 'true' : 'false';
44             }
45              
46 403         711 when ('DateTime') {
47 0         0 return strftime_rfc3339($data);
48             }
49              
50 403         626 when ('Math::BigInt') {
51 1         6 return $data->bstr;
52             }
53              
54 402         684 when ('Math::BigFloat') {
55 6 50 33     41 if ($data->is_inf || $data->is_nan) {
56 0         0 return lc $data->bstr;
57             } else {
58 6         141 return $data->bstr;
59             }
60             }
61              
62 396         712 when ('') {
63             # Thanks to ikegami on Stack Overflow for the trick!
64             # https://stackoverflow.com/questions/12686335/how-to-tell-apart-numeric-scalars-and-string-scalars-in-perl/12693984#12693984
65             # note: this must come before any regex can flip this flag off
66 396 100       3581 if (svref_2object(\$data)->FLAGS & (SVf_IOK | SVf_NOK)) {
67 176 50       835 return 'inf' if Math::BigFloat->new($data)->is_inf;
68 176 50       24647 return '-inf' if Math::BigFloat->new($data)->is_inf('-');
69 176 50       21391 return 'nan' if Math::BigFloat->new($data)->is_nan;
70 176         18897 return $data;
71             }
72             #return $data if svref_2object(\$data)->FLAGS & (SVf_IOK | SVf_NOK);
73 220 100       1790 return $data if $data =~ /$DateTime/;
74 196 100       1215 return lc($data) if $data =~ /$SpecialFloat/;
75              
76 187         494 return to_toml_string($data);
77             }
78              
79 0         0 default{
80 0         0 die 'unhandled: '.Dumper($_);
81             }
82             }
83             }
84              
85             sub to_toml_inline_table {
86 5     5 0 9 my ($data, $param) = @_;
87 5         9 my @buff;
88              
89 5         14 for my $key (keys %$data) {
90 6         15 my $value = $data->{$key};
91              
92 6 50       23 if (ref $value eq 'HASH') {
93 0         0 push @buff, $key . '=' . to_toml_inline_table($value);
94             } else {
95 6         21 push @buff, $key . '=' . to_toml($value);
96             }
97             }
98              
99 5         28 return '{' . join(', ', @buff) . '}';
100             }
101              
102             sub to_toml_table {
103 275     275 0 776 my ($data, $param) = @_;
104 275         522 my @buff_assign;
105             my @buff_tables;
106              
107             # Generate simple key/value pairs for scalar data
108 275         1206 for my $k (grep{ ref($data->{$_}) !~ /HASH|ARRAY/ } sort keys %$data) {
  538         2481  
109 321         1475 my $key = to_toml_key($k);
110 321         1277 my $val = to_toml($data->{$k}, $param);
111 321         1824 push @buff_assign, "$key=$val";
112             }
113              
114             # For arrays, generate an array of tables if all elements of the array are
115             # hashes. For mixed arrays, generate an inline array.
116 275         1210 ARRAY: for my $k (grep{ ref $data->{$_} eq 'ARRAY' } sort keys %$data) {
  538         1984  
117             # Empty table
118 57 50       141 if (!@{$data->{$k}}) {
  57         256  
119 0         0 my $key = to_toml_key($k);
120 0         0 push @buff_assign, "$key=[]";
121 0         0 next ARRAY;
122             }
123              
124             # Mixed array
125 57 100       127 if (grep{ ref $_ ne 'HASH' } @{$data->{$k}}) {
  121         395  
  57         414  
126 37         162 my $key = to_toml_key($k);
127 37         200 my $val = to_toml($data->{$k}, $param);
128 37         206 push @buff_assign, "$key=$val";
129             }
130             # Array of tables
131             else {
132 20         57 push @KEYS, $k;
133              
134 20         108 for (@{ $data->{$k} }) {
  20         72  
135 37         87 push @buff_tables, '', '[[' . join('.', map{ to_toml_key($_) } @KEYS) . ']]';
  45         97  
136 37         158 push @buff_tables, to_toml($_);
137             }
138              
139 20         51 pop @KEYS;
140             }
141             }
142              
143             # Sub-tables
144 275         1050 for my $k (grep{ ref $data->{$_} eq 'HASH' } sort keys %$data) {
  538         1450  
145 160 100       378 if (!keys(%{$data->{$k}})) {
  160         506  
146             # Empty table
147 24         85 my $key = to_toml_key($k);
148 24         106 push @buff_assign, "$key={}";
149             } else {
150             # Generate [table]
151 136         289 push @KEYS, $k;
152 136         258 push @buff_tables, '', '[' . join('.', map{ to_toml_key($_) } @KEYS) . ']';
  270         512  
153 136         646 push @buff_tables, to_toml($data->{$k}, $param);
154 136         303 pop @KEYS;
155             }
156             }
157              
158 275         1626 join "\n", @buff_assign, @buff_tables;
159             }
160              
161             sub to_toml_array {
162 64     64 0 174 my ($data, $param) = @_;
163              
164 64 50 66     376 if (@$data && $param->{strict}) {
165 0         0 my ($ok, $err) = is_strict_array($data);
166 0 0       0 die "toml: found heterogenous array, but strict is set ($err)\n" unless $ok;
167             }
168              
169 64         134 my @items;
170              
171 64         189 for my $item (@$data) {
172 123 100       325 if (ref $item eq 'HASH') {
173 5         15 push @items, to_toml_inline_table($item, $param);
174             } else {
175 118         378 push @items, to_toml($item, $param);
176             }
177             }
178              
179 64         239 return "[\n" . join("\n", map{ " $_," } @items) . "\n]";
  123         597  
180             }
181              
182             sub to_toml_key {
183 697     697 0 1688 my $str = shift;
184              
185 697 100       5808 if ($str =~ /^$BareKey$/) {
186 670         16218 return $str;
187             }
188              
189             # Escape control characters
190 27         1312 $str =~ s/([\p{General_Category=Control}])/'\\u00' . unpack('H2', $1)/eg;
  3         73  
191              
192             # Escape unicode characters
193             #$str =~ s/($NonASCII)/'\\u00' . unpack('H2', $1)/eg;
194              
195 27 100       101 if ($str =~ /^"/) {
196 4         23 return qq{'$str'};
197             } else {
198 23         105 return qq{"$str"};
199             }
200             }
201              
202             sub to_toml_string {
203 187     187 0 716 state $escape = {
204             "\n" => '\n',
205             "\r" => '\r',
206             "\t" => '\t',
207             "\f" => '\f',
208             "\b" => '\b',
209             "\"" => '\"',
210             "\\" => '\\\\',
211             "\'" => '\\\'',
212             };
213              
214 187         428 my ($arg) = @_;
215 187         603 $arg =~ s/(["\\\b\f\n\r\t])/$escape->{$1}/g;
216 187         346 $arg =~ s/([\p{General_Category=Control}])/'\\u00' . unpack('H2', $1)/eg;
  2         12  
217              
218 187         692 return '"' . $arg . '"';
219             }
220              
221             #-------------------------------------------------------------------------------
222             # Adapted from DateTime::Format::RFC3339.
223             #-------------------------------------------------------------------------------
224             sub strftime_rfc3339 {
225 6     6 0 35526 my ($dt) = @_;
226 6         11 my $tz;
227              
228             #-----------------------------------------------------------------------------
229             # Calculate the time zone offset for non-UTC time zones.
230             #
231             # TOML uses RFC3339 for datetimes, but supports a "local datetime" which
232             # excludes the timezone offset. A DateTime with a floating time zone
233             # indicates a TOML local datetime.
234             #
235             # DateTime::Format::RFC3339 requires a time zone, however, and defaults to
236             # +00:00 for floating time zones. To support local datetimes in output,
237             # format the datetime as RFC3339 and strip the timezone when encountering a
238             # floating time zone.
239             #-----------------------------------------------------------------------------
240 6 100       15 if ($dt->time_zone_short_name eq 'floating') {
    100          
241 1         21 $tz = '';
242             } elsif ($dt->time_zone->is_utc) {
243 1         22 $tz = 'Z';
244             } else {
245 4 100       418 my $sign = $dt->offset < 0 ? '-' : '+';
246 4         373 my $secs = abs $dt->offset;
247              
248 4         362 my $mins = int($secs / 60);
249 4         7 $secs %= 60;
250              
251 4         7 my $hours = int($mins / 60);
252 4         6 $mins %= 60;
253              
254 4 100       10 if ($secs) {
255 1         5 $dt = $dt->clone;
256 1         16 $dt->set_time_zone('UTC');
257 1         270 $tz = 'Z';
258             } else {
259 3         16 $tz = sprintf '%s%02d:%02d', $sign, $hours, $mins;
260             }
261             }
262              
263 6 50       21 my $format = $dt->nanosecond ? '%Y-%m-%dT%H:%M:%S.%9N' : '%Y-%m-%dT%H:%M:%S';
264 6         40 return $dt->strftime($format) . $tz;
265             }
266              
267             1;
268              
269             __END__
270              
271             =pod
272              
273             =encoding UTF-8
274              
275             =head1 NAME
276              
277             TOML::Tiny::Writer
278              
279             =head1 VERSION
280              
281             version 0.14
282              
283             =head1 AUTHOR
284              
285             Jeff Ober <sysread@fastmail.fm>
286              
287             =head1 COPYRIGHT AND LICENSE
288              
289             This software is copyright (c) 2021 by Jeff Ober.
290              
291             This is free software; you can redistribute it and/or modify it under
292             the same terms as the Perl 5 programming language system itself.
293              
294             =cut