File Coverage

blib/lib/TOML/Tiny/Writer.pm
Criterion Covered Total %
statement 135 147 91.8
branch 51 70 72.8
condition 4 6 66.6
subroutine 15 15 100.0
pod 0 7 0.0
total 205 245 83.6


line stmt bran cond sub pod time code
1             package TOML::Tiny::Writer;
2             $TOML::Tiny::Writer::VERSION = '0.16';
3 285     285   2256 use strict;
  285         737  
  285         9340  
4 285     285   1647 use warnings;
  285         721  
  285         8609  
5 285     285   1601 no warnings qw(experimental);
  285         679  
  285         8388  
6 285     285   3188 use v5.18;
  285         1148  
7              
8 285     285   1914 use B qw(SVf_IOK SVf_NOK svref_2object);
  285         699  
  285         20927  
9 285     285   2296 use Data::Dumper qw(Dumper);
  285         834  
  285         16497  
10 285     285   2141 use TOML::Tiny::Grammar qw($BareKey $DateTime $SpecialFloat);
  285         3147  
  285         30502  
11 285     285   123543 use TOML::Tiny::Util qw(is_strict_array);
  285         932  
  285         354395  
12              
13             my @KEYS;
14              
15             sub to_toml {
16 765     765 0 1492 my $data = shift;
17 765 50       2141 my $param = ref($_[1]) eq 'HASH' ? $_[1] : undef;
18              
19 765         1549 my $ref = ref $data;
20 765 100       1843 if ($ref eq 'HASH') {
21 277         1338 return to_toml_table($data, $param);
22             }
23              
24 488 100       1229 if ($ref eq 'ARRAY') {
25 65         318 return to_toml_array($data, $param);
26             }
27              
28 423 50       1177 if ($ref eq '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 423 100       1012 if ($ref eq 'JSON::PP::Boolean') {
39 15 100       170 return $$data ? 'true' : 'false';
40             }
41              
42 408 50       949 if ($ref eq 'Types::Serializer::Boolean') {
43 0 0       0 return $data ? 'true' : 'false';
44             }
45              
46 408 50       925 if ($ref eq 'DateTime') {
47 0         0 return strftime_rfc3339($data);
48             }
49              
50 408 100       996 if ($ref eq 'Math::BigInt') {
51 1         21 return $data->bstr;
52             }
53              
54 407 100       929 if ($ref eq 'Math::BigFloat') {
55 6 50 33     37 if ($data->is_inf || $data->is_nan) {
56 0         0 return lc $data->bstr;
57             } else {
58 6         142 return $data->bstr;
59             }
60             }
61              
62 401 50       1008 if ($ref eq '') {
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 401 100       3365 if (svref_2object(\$data)->FLAGS & (SVf_IOK | SVf_NOK)) {
67 179 50       882 return 'inf' if Math::BigFloat->new($data)->is_inf;
68 179 50       48662 return '-inf' if Math::BigFloat->new($data)->is_inf('-');
69 179 50       21594 return 'nan' if Math::BigFloat->new($data)->is_nan;
70 179         19384 return $data;
71             }
72             #return $data if svref_2object(\$data)->FLAGS & (SVf_IOK | SVf_NOK);
73 222 100       2065 return $data if $data =~ /$DateTime/;
74 198 100       3057 return lc($data) if $data =~ /^$SpecialFloat$/;
75              
76 189         634 return to_toml_string($data);
77             }
78              
79 0         0 die 'unhandled: '.Dumper($ref);
80             }
81              
82             sub to_toml_inline_table {
83 5     5 0 12 my ($data, $param) = @_;
84 5         9 my @buff;
85              
86 5         49 for my $key (keys %$data) {
87 6         16 my $value = $data->{$key};
88              
89 6 50       31 if (ref $value eq 'HASH') {
90 0         0 push @buff, $key . '=' . to_toml_inline_table($value);
91             } else {
92 6         19 push @buff, $key . '=' . to_toml($value);
93             }
94             }
95              
96 5         30 return '{' . join(', ', @buff) . '}';
97             }
98              
99             sub to_toml_table {
100 277     277 0 767 my ($data, $param) = @_;
101 277         1013 my @buff_assign;
102             my @buff_tables;
103              
104             # Generate simple key/value pairs for scalar data
105 277         1169 for my $k (grep{ ref($data->{$_}) !~ /HASH|ARRAY/ } sort keys %$data) {
  540         2580  
106 323         890 my $key = to_toml_key($k);
107 323         1249 my $val = to_toml($data->{$k}, $param);
108 323         2259 push @buff_assign, "$key=$val";
109             }
110              
111             # For arrays, generate an array of tables if all elements of the array are
112             # hashes. For mixed arrays, generate an inline array.
113 277         1323 ARRAY: for my $k (grep{ ref $data->{$_} eq 'ARRAY' } sort keys %$data) {
  540         2840  
114             # Empty table
115 57 50       127 if (!@{$data->{$k}}) {
  57         264  
116 0         0 my $key = to_toml_key($k);
117 0         0 push @buff_assign, "$key=[]";
118 0         0 next ARRAY;
119             }
120              
121             # Mixed array
122 57 100       123 if (grep{ ref $_ ne 'HASH' } @{$data->{$k}}) {
  121         374  
  57         246  
123 37         135 my $key = to_toml_key($k);
124 37         203 my $val = to_toml($data->{$k}, $param);
125 37         228 push @buff_assign, "$key=$val";
126             }
127             # Array of tables
128             else {
129 20         68 push @KEYS, $k;
130              
131 20         32 for (@{ $data->{$k} }) {
  20         78  
132 37         88 push @buff_tables, '', '[[' . join('.', map{ to_toml_key($_) } @KEYS) . ']]';
  45         118  
133 37         156 push @buff_tables, to_toml($_);
134             }
135              
136 20         51 pop @KEYS;
137             }
138             }
139              
140             # Sub-tables
141 277         1087 for my $k (grep{ ref $data->{$_} eq 'HASH' } sort keys %$data) {
  540         1572  
142 160 100       297 if (!keys(%{$data->{$k}})) {
  160         714  
143             # Empty table
144 24         73 my $key = to_toml_key($k);
145 24         96 push @buff_assign, "$key={}";
146             } else {
147             # Generate [table]
148 136         303 push @KEYS, $k;
149 136         311 push @buff_tables, '', '[' . join('.', map{ to_toml_key($_) } @KEYS) . ']';
  270         529  
150 136         571 push @buff_tables, to_toml($data->{$k}, $param);
151 136         288 pop @KEYS;
152             }
153             }
154              
155 277         1852 join "\n", @buff_assign, @buff_tables;
156             }
157              
158             sub to_toml_array {
159 66     66 0 4599 my ($data, $param) = @_;
160              
161 66 100 100     388 if (@$data && $param->{strict}) {
162 1         7 my ($ok, $err) = is_strict_array($data);
163 1 50       7 die "toml: found heterogenous array, but strict is set ($err)\n" unless $ok;
164             }
165              
166 66         143 my @items;
167              
168 66         167 for my $item (@$data) {
169 127 100       392 if (ref $item eq 'HASH') {
170 5         25 push @items, to_toml_inline_table($item, $param);
171             } else {
172 122         374 push @items, to_toml($item, $param);
173             }
174             }
175              
176 66         190 return "[\n" . join("\n", map{ " $_," } @items) . "\n]";
  127         626  
177             }
178              
179             sub to_toml_key {
180 699     699 0 1192 my $str = shift;
181              
182 699 100       5260 if ($str =~ /^$BareKey$/) {
183 672         15722 return $str;
184             }
185              
186             # Escape control characters
187 27         1097 $str =~ s/([\p{General_Category=Control}])/'\\u00' . unpack('H2', $1)/eg;
  3         19  
188              
189             # Escape unicode characters
190             #$str =~ s/($NonASCII)/'\\u00' . unpack('H2', $1)/eg;
191              
192 27 100       99 if ($str =~ /^"/) {
193 4         20 return qq{'$str'};
194             } else {
195 23         105 return qq{"$str"};
196             }
197             }
198              
199             sub to_toml_string {
200 189     189 0 832 state $escape = {
201             "\n" => '\n',
202             "\r" => '\r',
203             "\t" => '\t',
204             "\f" => '\f',
205             "\b" => '\b',
206             "\"" => '\"',
207             "\\" => '\\\\',
208             "\'" => '\\\'',
209             };
210              
211 189         527 my ($arg) = @_;
212 189         683 $arg =~ s/(["\\\b\f\n\r\t])/$escape->{$1}/g;
213 189         556 $arg =~ s/([\p{General_Category=Control}])/'\\u00' . unpack('H2', $1)/eg;
  2         14  
214              
215 189         1478 return '"' . $arg . '"';
216             }
217              
218             #-------------------------------------------------------------------------------
219             # Adapted from DateTime::Format::RFC3339.
220             #-------------------------------------------------------------------------------
221             sub strftime_rfc3339 {
222 6     6 0 37262 my ($dt) = @_;
223 6         10 my $tz;
224              
225             #-----------------------------------------------------------------------------
226             # Calculate the time zone offset for non-UTC time zones.
227             #
228             # TOML uses RFC3339 for datetimes, but supports a "local datetime" which
229             # excludes the timezone offset. A DateTime with a floating time zone
230             # indicates a TOML local datetime.
231             #
232             # DateTime::Format::RFC3339 requires a time zone, however, and defaults to
233             # +00:00 for floating time zones. To support local datetimes in output,
234             # format the datetime as RFC3339 and strip the timezone when encountering a
235             # floating time zone.
236             #-----------------------------------------------------------------------------
237 6 100       17 if ($dt->time_zone_short_name eq 'floating') {
    100          
238 1         24 $tz = '';
239             } elsif ($dt->time_zone->is_utc) {
240 1         35 $tz = 'Z';
241             } else {
242 4 100       415 my $sign = $dt->offset < 0 ? '-' : '+';
243 4         367 my $secs = abs $dt->offset;
244              
245 4         361 my $mins = int($secs / 60);
246 4         7 $secs %= 60;
247              
248 4         6 my $hours = int($mins / 60);
249 4         7 $mins %= 60;
250              
251 4 100       9 if ($secs) {
252 1         6 $dt = $dt->clone;
253 1         19 $dt->set_time_zone('UTC');
254 1         259 $tz = 'Z';
255             } else {
256 3         17 $tz = sprintf '%s%02d:%02d', $sign, $hours, $mins;
257             }
258             }
259              
260 6 50       20 my $format = $dt->nanosecond ? '%Y-%m-%dT%H:%M:%S.%9N' : '%Y-%m-%dT%H:%M:%S';
261 6         45 return $dt->strftime($format) . $tz;
262             }
263              
264             1;
265              
266             __END__
267              
268             =pod
269              
270             =encoding UTF-8
271              
272             =head1 NAME
273              
274             TOML::Tiny::Writer
275              
276             =head1 VERSION
277              
278             version 0.16
279              
280             =head1 AUTHOR
281              
282             Jeff Ober <sysread@fastmail.fm>
283              
284             =head1 COPYRIGHT AND LICENSE
285              
286             This software is copyright (c) 2023 by Jeff Ober.
287              
288             This is free software; you can redistribute it and/or modify it under
289             the same terms as the Perl 5 programming language system itself.
290              
291             =cut