File Coverage

blib/lib/App/NDTools/Slurp.pm
Criterion Covered Total %
statement 143 184 77.7
branch 57 88 64.7
condition 10 12 83.3
subroutine 22 22 100.0
pod 0 7 0.0
total 232 313 74.1


line stmt bran cond sub pod time code
1             package App::NDTools::Slurp;
2              
3             # input/output related subroutines for NDTools
4              
5 17     17   94 use strict;
  17         24  
  17         583  
6 17     17   78 use warnings FATAL => 'all';
  17         28  
  17         493  
7 17     17   87 use parent qw(Exporter);
  17         36  
  17         124  
8 16     16   7295 use open qw(:std :utf8);
  16         16214  
  16         79  
9              
10 16     16   1962 use File::Basename qw(basename);
  16         25  
  16         1106  
11 16     16   8231 use JSON qw();
  16         120214  
  16         407  
12 16     16   99 use Scalar::Util qw(readonly);
  16         28  
  16         746  
13              
14 16     16   78 use App::NDTools::INC;
  16         29  
  16         80  
15 16     16   6085 use App::NDTools::Util qw(is_number);
  16         31  
  16         790  
16 16     16   6238 use Log::Log4Cli;
  16         148618  
  16         1802  
17              
18             our @EXPORT_OK = qw(
19             s_decode
20             s_dump
21             s_dump_file
22             s_encode
23             s_fmt_by_uri
24             s_load
25             s_load_uri
26             );
27              
28             our %FORMATS = (
29             JSON => {
30             allow_nonref => 1,
31             canonical => 1,
32             pretty => 1,
33             relaxed => 1,
34             },
35             );
36              
37             use constant {
38 16         53 TRUE => JSON::true,
39             FALSE => JSON::false,
40 16     16   111 };
  16         24  
41              
42             sub _decode_yaml($) {
43 18     18   1744 require YAML::XS;
44              
45 18         10648 my $data = YAML::XS::Load($_[0]);
46              
47             # YAML::XS decode boolean vals as PL_sv_yes and PL_sv_no, both - read only
48             # at least until https://github.com/ingydotnet/yaml-libyaml-pm/issues/25
49             # second thing here: get rid of dualvars: YAML::XS load numbers as
50             # dualvars, but JSON::XS dumps them as strings =(
51              
52 18         82 my @stack = (\$data);
53 18         27 my $ref;
54              
55 18         57 while ($ref = shift @stack) {
56 49 100       59 if (ref ${$ref} eq 'ARRAY') {
  49 100       103  
    100          
57 4         6 for (0 .. $#{${$ref}}) {
  4         5  
  4         9  
58 13 100       14 if (ref ${$ref}->[$_]) {
  13 100       23  
    100          
59 3         4 push @stack, \${$ref}->[$_];
  3         7  
60 10         17 } elsif (readonly ${$ref}->[$_]) {
61 4 100       5 splice @{${$ref}}, $_, 1, (${$ref}->[$_] ? TRUE : FALSE);
  4         5  
  4         5  
  4         11  
62 6         12 } elsif (is_number ${$ref}->[$_]) {
63 3         5 ${$ref}->[$_] += 0;
  3         5  
64             }
65             }
66 45         86 } elsif (ref ${$ref} eq 'HASH') {
67 43         47 for (keys %{${$ref}}) {
  43         44  
  43         114  
68 117 100       131 if (ref ${$ref}->{$_}) {
  117 100       187  
    100          
69 28         34 push @stack, \${$ref}->{$_};
  28         67  
70 89         165 } elsif (readonly ${$ref}->{$_}) {
71 4 100       4 ${$ref}->{$_} = delete ${$ref}->{$_} ? TRUE : FALSE;
  4         8  
  4         9  
72 85         175 } elsif (is_number ${$ref}->{$_}) {
73 19         29 ${$ref}->{$_} += 0;
  19         47  
74             }
75             }
76 2         9 } elsif (is_number ${$ref}) {
77 1         2 ${$ref} += 0;
  1         3  
78             }
79             }
80              
81 18         47 return $data;
82             }
83              
84             sub _encode_yaml($) {
85 9     9   55 require YAML::XS;
86 9         14 my $modern_yaml_xs = eval { YAML::XS->VERSION(0.67) };
  9         216  
87              
88             # replace booleans for YAML::XS (accepts only boolean and JSON::PP::Boolean
89             # since 0.67 and PL_sv_yes/no in earlier versions). No roundtrip for
90             # versions < 0.67: 1 and 0 used for booleans (there is no way to set
91             # PL_sv_yes/no into arrays/hashes without XS code)
92              
93 9         28 my ($false, $true) = (0, 1);
94              
95 9 50       36 local $YAML::XS::Boolean = "JSON::PP" if ($modern_yaml_xs);
96              
97 9 50       25 if ($modern_yaml_xs) {
98 4 50   4   2609 return YAML::XS::Dump($_[0]) if (ref TRUE eq 'JSON::PP::Boolean');
  4     2   45999  
  4         367  
  2         11  
  2         3  
  2         165  
  9         281  
99              
100 0         0 require JSON::PP;
101 0         0 ($false, $true) = (JSON::PP::false(), JSON::PP::true());
102             }
103              
104 0         0 my @stack = (\$_[0]);
105 0         0 my $ref;
106 0         0 my $bool_type = ref TRUE;
107              
108 0         0 while ($ref = shift @stack) {
109 0 0       0 if (ref ${$ref} eq 'ARRAY') {
  0 0       0  
    0          
110 0         0 for (0 .. $#{${$ref}}) {
  0         0  
  0         0  
111 0 0       0 if (ref ${$ref}->[$_]) {
  0 0       0  
112 0         0 push @stack, \${$ref}->[$_];
  0         0  
113 0         0 } elsif (ref ${$ref}->[$_] eq $bool_type) {
114 0 0       0 ${$ref}->[$_] = ${$ref}->[$_] ? $true : $false;
  0         0  
  0         0  
115             }
116             }
117 0         0 } elsif (ref ${$ref} eq 'HASH') {
118 0         0 for (keys %{${$ref}}) {
  0         0  
  0         0  
119 0 0       0 if (ref ${$ref}->{$_}) {
  0 0       0  
120 0         0 push @stack, \${$ref}->{$_};
  0         0  
121 0         0 } elsif (ref ${$ref}->{$_} eq $bool_type) {
122 0 0       0 ${$ref}->{$_} = ${$ref}->{$_} ? $true : $false;
  0         0  
  0         0  
123             }
124             }
125 0         0 } elsif (ref ${$ref} eq $bool_type) {
126 0 0       0 ${$ref} = ${$ref} ? $true : $false;
  0         0  
  0         0  
127             }
128             }
129              
130 0         0 return YAML::XS::Dump($_[0]);
131             }
132              
133             sub s_decode($$;$) {
134 307     307 0 753 my ($data, $fmt, $opts) = @_;
135 307         528 my $format = uc($fmt);
136              
137 307 100       688 if ($format eq 'JSON') {
    100          
    50          
138 288 50       371 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  288         1096  
  288         1520  
139 288         765 $data = eval {
140             JSON->new(
141             )->allow_nonref($o->{allow_nonref}
142             )->canonical($o->{canonical}
143             )->pretty($o->{pretty}
144             )->relaxed($o->{relaxed}
145 288         6801 )->decode($data);
146             };
147             } elsif ($format eq 'YAML') {
148 18         29 $data = eval { _decode_yaml($data) };
  18         51  
149             } elsif ($format eq 'RAW') {
150             ;
151             } else {
152 0         0 die_fatal "Unable to decode '$fmt' (not supported)";
153             }
154              
155 307 100       801 die_fatal "Failed to decode '$fmt': " . $@, 4 if $@;
156              
157 306         1321 return $data;
158             }
159              
160             sub s_dump(@) {
161 110     110 0 274 my ($uri, $fmt, $opts) = (shift, shift, shift);
162              
163 110 50       290 $uri = \*STDOUT if ($uri eq '-');
164              
165 110 100       313 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
166 110         223 my $data = join('', map { s_encode($_, $fmt, $opts) } @_);
  115         270  
167              
168 110 100       311 if (ref $uri eq 'GLOB') {
169 38         1472 print $uri $data;
170             } else {
171 72         175 s_dump_file($uri, $data);
172             }
173             }
174              
175             sub s_dump_file($$) {
176 72     72 0 156 my ($file, $data) = @_;
177              
178 72 50       4610 open(my $fh, '>', $file) or die_fatal "Failed to open '$file' ($!)", 2;
179 72         730 print $fh $data;
180 72         4685 close($fh);
181             }
182              
183             sub s_encode($$;$) {
184 123     123 0 254 my ($data, $fmt, $opts) = @_;
185 123         209 my $format = uc($fmt);
186              
187 123 100 100     444 if ($format eq 'JSON' or $format eq 'RAW' and ref $data) {
    100 100        
    50          
188 111 100       153 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  111         332  
  111         514  
189 111         265 $data = eval {
190             JSON->new(
191             )->allow_nonref($o->{allow_nonref}
192             )->canonical($o->{canonical}
193             )->pretty($o->{pretty}
194 111         2221 )->encode($data);
195             };
196             } elsif ($format eq 'YAML') {
197 9         18 $data = eval { _encode_yaml($data) };
  9         25  
198             } elsif ($format eq 'RAW') {
199 3         7 $data .= "\n";
200             } else {
201 0         0 die_fatal "Unable to encode to '$fmt' (not supported)";
202             }
203              
204 123 50       405 die_fatal "Failed to encode structure to $fmt: " . $@, 4 if $@;
205              
206 123         513 return $data;
207             }
208              
209             sub s_fmt_by_uri($) {
210 347     347 0 12062 my @names = split(/\./, basename(shift));
211 347 100 66     1858 if (@names and @names > 1) {
212 346         891 my $ext = uc(pop @names);
213 346 100 66     1369 return 'YAML' if ($ext eq 'YML' or $ext eq 'YAML');
214             }
215              
216 346         761 return 'JSON'; # by default
217             }
218              
219             sub s_load($$;@) {
220 302     302 0 628 my ($uri, $fmt, %opts) = @_;
221              
222 302 50       693 $uri = \*STDIN if ($uri eq '-');
223 302         603 my $data = s_load_uri($uri);
224 300 100       1071 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
225              
226 300         721 return s_decode($data, $fmt);
227             }
228              
229             sub s_load_uri($) {
230 302     302 0 443 my $uri = shift;
231 302         356 my $data;
232              
233 302 50       609 if (ref $uri eq 'GLOB') {
234 0         0 $data = do { local $/; <$uri> };
  0         0  
  0         0  
235             } else {
236 302 100       11214 open(my $fh, '<', $uri) or
237             die_fatal "Failed to open file '$uri' ($!)", 2;
238 300         922 $data = do { local $/; <$fh> }; # load whole file
  300         1215  
  300         7259  
239 300         3210 close($fh);
240             }
241              
242 300         963 return $data;
243             }
244              
245             1;