File Coverage

blib/lib/TOML/Tiny/Writer.pm
Criterion Covered Total %
statement 143 154 92.8
branch 60 76 78.9
condition 4 6 66.6
subroutine 17 17 100.0
pod 0 7 0.0
total 224 260 86.1


line stmt bran cond sub pod time code
1             package TOML::Tiny::Writer;
2             $TOML::Tiny::Writer::VERSION = '0.20';
3 286     286   4014 use strict;
  286         1831  
  286         11859  
4 286     286   3028 use warnings;
  286         1969  
  286         20104  
5 286     286   1871 no warnings qw(experimental);
  286         702  
  286         11052  
6 286     286   4099 use v5.18;
  286         1129  
7              
8 286     286   1835 use B qw(SVf_IOK SVf_NOK svref_2object);
  286         582  
  286         42013  
9 286     286   1806 use Data::Dumper qw(Dumper);
  286         615  
  286         20992  
10 286     286   1781 use TOML::Tiny::Grammar qw($BareKey $DateTime $SpecialFloat);
  286         636  
  286         33789  
11 286     286   138475 use TOML::Tiny::Util qw(is_strict_array);
  286         910  
  286         27318  
12              
13 286     286   2175 use constant CORE_BOOL => defined &builtin::is_bool;
  286         512  
  286         574752  
14              
15             my @KEYS;
16              
17             sub to_toml {
18 111     111 0 282 my $data = shift;
19 111 100       555 die 'toml: data to encode must be a hashref' if ref $data ne 'HASH';
20 109         577 return _to_toml( $data, { @_ } );
21             }
22              
23             sub _to_toml ($$);
24             sub _to_toml ($$) {
25 809     809   1463 my $data = shift;
26 809         1115 my $param = shift;
27              
28 809 100       1975 die 'toml: found undefined value, which is unsupported by TOML' if ! defined $data;
29              
30 808         1460 my $ref = ref $data;
31 808 100       1811 if ($ref eq 'HASH') {
32 291         1398 return to_toml_table($data, $param);
33             }
34              
35 517 100       1125 if ($ref eq 'ARRAY') {
36 67         225 return to_toml_array($data, $param);
37             }
38              
39 450 50       1256 if ($ref eq 'SCALAR') {
40 0 0       0 if ($$data eq '1') {
    0          
41 0         0 return 'true';
42             } elsif ($$data eq '0') {
43 0         0 return 'false';
44             } else {
45 0         0 return _to_toml($$_, $param);
46             }
47             }
48              
49 450 100       1192 if ($ref eq 'JSON::PP::Boolean') {
50 17 100       125 return $$data ? 'true' : 'false';
51             }
52              
53 433 50       1051 if ($ref eq 'Types::Serializer::Boolean') {
54 0 0       0 return $data ? 'true' : 'false';
55             }
56              
57 433 100       1000 if ($ref eq 'DateTime') {
58 2         4 return strftime_rfc3339($data);
59             }
60              
61 431 100       915 if ($ref eq 'Math::BigInt') {
62 2         13 return $data->bstr;
63             }
64              
65 429 100       936 if ($ref eq 'Math::BigFloat') {
66 6 50 33     27 if ($data->is_inf || $data->is_nan) {
67 0         0 return lc $data->bstr;
68             } else {
69 6         152 return $data->bstr;
70             }
71             }
72              
73 423 50       1745 if ($ref eq '') {
74 423 100       978 if (CORE_BOOL && builtin::is_bool($data)) {
75 2 100       15 return $data ? 'true' : 'false';
76             }
77             # Thanks to ikegami on Stack Overflow for the trick!
78             # https://stackoverflow.com/questions/12686335/how-to-tell-apart-numeric-scalars-and-string-scalars-in-perl/12693984#12693984
79             # note: this must come before any regex can flip this flag off
80 421 100       3991 if (svref_2object(\$data)->FLAGS & (SVf_IOK | SVf_NOK)) {
81 186 50       970 return 'inf' if Math::BigFloat->new($data)->is_inf;
82 186 50       53036 return '-inf' if Math::BigFloat->new($data)->is_inf('-');
83 186 50       26193 return 'nan' if Math::BigFloat->new($data)->is_nan;
84 186         22124 return $data;
85             }
86 235 100       696 return to_toml_string($data) if $param->{no_string_guessing};
87             #return $data if svref_2object(\$data)->FLAGS & (SVf_IOK | SVf_NOK);
88 227 100       56991 return $data if $data =~ /^$DateTime$/;
89 203 100       3371 return lc($data) if $data =~ /^$SpecialFloat$/;
90              
91 194         574 return to_toml_string($data);
92             }
93              
94 0         0 die 'unhandled: '.Dumper($ref);
95             }
96              
97             sub to_toml_inline_table {
98 6     6 0 16 my ($data, $param) = @_;
99 6         13 my @buff;
100              
101 6         137 for my $k (keys %$data) {
102 7         41 my $key = to_toml_key($k);
103 7         18 my $val = $data->{$k};
104              
105 7 50       19 if (ref $val eq 'HASH') {
106 0         0 push @buff, $key . '=' . to_toml_inline_table($val);
107             } else {
108 7         43 push @buff, $key . '=' . _to_toml($val, $param);
109             }
110             }
111              
112 6         37 return '{' . join(', ', @buff) . '}';
113             }
114              
115             sub to_toml_table {
116 291     291 0 649 my ($data, $param) = @_;
117 291         517 my @buff_assign;
118             my @buff_tables;
119              
120             # Generate simple key/value pairs for scalar data
121 291         1218 for my $k (grep{ ref($data->{$_}) !~ /HASH|ARRAY/ } sort keys %$data) {
  576         2529  
122 348         906 my $key = to_toml_key($k);
123 348         1425 my $val = _to_toml($data->{$k}, $param);
124 348         1862 push @buff_assign, "$key=$val";
125             }
126              
127             # For arrays, generate an array of tables if all elements of the array are
128             # hashes. For mixed arrays, generate an inline array.
129 291         1130 ARRAY: for my $k (grep{ ref $data->{$_} eq 'ARRAY' } sort keys %$data) {
  576         1444  
130             # Empty table
131 59 50       98 if (!@{$data->{$k}}) {
  59         249  
132 0         0 my $key = to_toml_key($k);
133 0         0 push @buff_assign, "$key=[]";
134 0         0 next ARRAY;
135             }
136              
137             # Mixed array
138 59 100       111 if (grep{ ref $_ ne 'HASH' } @{$data->{$k}}) {
  124         339  
  59         173  
139 39         197 my $key = to_toml_key($k);
140 39         239 my $val = _to_toml($data->{$k}, $param);
141 38         152 push @buff_assign, "$key=$val";
142             }
143             # Array of tables
144             else {
145 20         56 push @KEYS, $k;
146              
147 20         35 for (@{ $data->{$k} }) {
  20         65  
148 37         73 push @buff_tables, '', '[[' . join('.', map{ to_toml_key($_) } @KEYS) . ']]';
  45         122  
149 37         144 push @buff_tables, _to_toml($_, $param);
150             }
151              
152 20         86 pop @KEYS;
153             }
154             }
155              
156             # Sub-tables
157 290         858 for my $k (grep{ ref $data->{$_} eq 'HASH' } sort keys %$data) {
  575         1215  
158 169 100       215 if (!keys(%{$data->{$k}})) {
  169         473  
159             # Empty table
160 24         62 my $key = to_toml_key($k);
161 24         64 push @buff_assign, "$key={}";
162             } else {
163             # Generate [table]
164 145         275 push @KEYS, $k;
165 145         257 push @buff_tables, '', '[' . join('.', map{ to_toml_key($_) } @KEYS) . ']';
  289         471  
166 145         523 push @buff_tables, _to_toml($data->{$k}, $param);
167 143         264 pop @KEYS;
168             }
169             }
170              
171 288         1716 join "\n", @buff_assign, @buff_tables;
172             }
173              
174             sub to_toml_array {
175 68     68 0 10474 my ($data, $param) = @_;
176              
177 68 100 100     363 if (@$data && $param->{strict}) {
178 2         8 my ($ok, $err) = is_strict_array($data);
179 2 50       4 die "toml: found heterogenous array, but strict is set ($err)\n" unless $ok;
180             }
181              
182 68         102 my @items;
183              
184 68         149 for my $item (@$data) {
185 130 100       359 if (ref $item eq 'HASH') {
186 6         38 push @items, to_toml_inline_table($item, $param);
187             } else {
188 124         351 push @items, _to_toml($item, $param);
189             }
190             }
191              
192 67         156 return "[\n" . join("\n", map{ " $_," } @items) . "\n]";
  129         521  
193             }
194              
195             sub to_toml_key {
196 752     752 0 1342 my $str = shift;
197              
198 752 100       9253 if ($str =~ /^$BareKey$/) {
199 718         2004 return $str;
200             } else {
201             # Not valid as a "bare key". Encode it as a "quoted key"
202             # (in TOML terminology), using the "literal string" format.
203 34         155 return to_toml_string($str);
204             }
205             }
206              
207             sub to_toml_string {
208 236     236 0 867 state $escape = {
209             "\n" => '\n',
210             "\r" => '\r',
211             "\t" => '\t',
212             "\f" => '\f',
213             "\b" => '\b',
214             "\"" => '\"',
215             "\\" => '\\\\',
216             "\'" => '\\\'',
217             };
218              
219 236         616 my ($arg) = @_;
220 236         722 $arg =~ s/(["\\\b\f\n\r\t])/$escape->{$1}/g;
221 236         442 $arg =~ s/([\p{General_Category=Control}])/'\\u00' . unpack('H2', $1)/eg;
  2         11  
222              
223 236         788 return '"' . $arg . '"';
224             }
225              
226             #-------------------------------------------------------------------------------
227             # Adapted from DateTime::Format::RFC3339.
228             #-------------------------------------------------------------------------------
229             sub strftime_rfc3339 {
230 8     8 0 33805 my ($dt) = @_;
231 8         11 my $tz;
232              
233             #-----------------------------------------------------------------------------
234             # Calculate the time zone offset for non-UTC time zones.
235             #
236             # TOML uses RFC3339 for datetimes, but supports a "local datetime" which
237             # excludes the timezone offset. A DateTime with a floating time zone
238             # indicates a TOML local datetime.
239             #
240             # DateTime::Format::RFC3339 requires a time zone, however, and defaults to
241             # +00:00 for floating time zones. To support local datetimes in output,
242             # format the datetime as RFC3339 and strip the timezone when encountering a
243             # floating time zone.
244             #-----------------------------------------------------------------------------
245 8 100       26 if ($dt->time_zone_short_name eq 'floating') {
    100          
246 2         33 $tz = '';
247             } elsif ($dt->time_zone->is_utc) {
248 1         17 $tz = 'Z';
249             } else {
250 5 100       391 my $sign = $dt->offset < 0 ? '-' : '+';
251 5         297 my $secs = abs $dt->offset;
252              
253 5         253 my $mins = int($secs / 60);
254 5         7 $secs %= 60;
255              
256 5         8 my $hours = int($mins / 60);
257 5         7 $mins %= 60;
258              
259 5 100       10 if ($secs) {
260 1         5 $dt = $dt->clone;
261 1         13 $dt->set_time_zone('UTC');
262 1         232 $tz = 'Z';
263             } else {
264 4         19 $tz = sprintf '%s%02d:%02d', $sign, $hours, $mins;
265             }
266             }
267              
268 8 100       24 my $format = $dt->nanosecond ? '%Y-%m-%dT%H:%M:%S.%9N' : '%Y-%m-%dT%H:%M:%S';
269 8         63 return $dt->strftime($format) . $tz;
270             }
271              
272             1;
273              
274             __END__
275              
276             =pod
277              
278             =encoding UTF-8
279              
280             =head1 NAME
281              
282             TOML::Tiny::Writer
283              
284             =head1 VERSION
285              
286             version 0.20
287              
288             =head1 AUTHOR
289              
290             Jeff Ober <sysread@fastmail.fm>
291              
292             =head1 COPYRIGHT AND LICENSE
293              
294             This software is copyright (c) 2025 by Jeff Ober.
295              
296             This is free software; you can redistribute it and/or modify it under
297             the same terms as the Perl 5 programming language system itself.
298              
299             =cut