File Coverage

blib/lib/App/NDTools/Slurp.pm
Criterion Covered Total %
statement 138 179 77.0
branch 57 88 64.7
condition 10 12 83.3
subroutine 21 21 100.0
pod 0 7 0.0
total 226 307 73.6


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   117 use strict;
  17         43  
  17         688  
6 17     17   93 use warnings FATAL => 'all';
  17         32  
  17         610  
7 17     17   108 use parent qw(Exporter);
  17         41  
  17         143  
8 16     16   9231 use open qw(:std :utf8);
  16         20166  
  16         103  
9              
10 16     16   2410 use File::Basename qw(basename);
  16         40  
  16         1351  
11 16     16   10102 use JSON qw();
  16         148385  
  16         555  
12 16     16   125 use Scalar::Util qw(isdual readonly);
  16         33  
  16         1040  
13              
14 16     16   109 use App::NDTools::INC;
  16         35  
  16         97  
15 16     16   7527 use Log::Log4Cli;
  16         177347  
  16         2223  
16              
17             our @EXPORT_OK = qw(
18             s_decode
19             s_dump
20             s_dump_file
21             s_encode
22             s_fmt_by_uri
23             s_load
24             s_load_uri
25             );
26              
27             our %FORMATS = (
28             JSON => {
29             allow_nonref => 1,
30             canonical => 1,
31             pretty => 1,
32             relaxed => 1,
33             },
34             );
35              
36             use constant {
37 16         61 TRUE => JSON::true,
38             FALSE => JSON::false,
39 16     16   129 };
  16         33  
40              
41             sub _decode_yaml($) {
42 18     18   2992 require YAML::XS;
43              
44 18         14460 my $data = YAML::XS::Load($_[0]);
45              
46             # YAML::XS decode boolean vals as PL_sv_yes and PL_sv_no, both - read only
47             # at least until https://github.com/ingydotnet/yaml-libyaml-pm/issues/25
48             # second thing here: get rid of dualvars: YAML::XS load numbers as
49             # dualvars, but JSON::XS dumps them as strings =(
50              
51 18         94 my @stack = (\$data);
52 18         33 my $ref;
53              
54 18         69 while ($ref = shift @stack) {
55 49 100       85 if (ref ${$ref} eq 'ARRAY') {
  49 100       124  
    100          
56 4         7 for (0 .. $#{${$ref}}) {
  4         6  
  4         14  
57 13 100       17 if (ref ${$ref}->[$_]) {
  13 100       35  
    100          
58 3         6 push @stack, \${$ref}->[$_];
  3         15  
59 10         23 } elsif (readonly ${$ref}->[$_]) {
60 4 100       5 splice @{${$ref}}, $_, 1, (${$ref}->[$_] ? TRUE : FALSE);
  4         11  
  4         7  
  4         15  
61 6         18 } elsif (isdual ${$ref}->[$_]) {
62 3         49 ${$ref}->[$_] += 0;
  3         12  
63             }
64             }
65 45         102 } elsif (ref ${$ref} eq 'HASH') {
66 43         67 for (keys %{${$ref}}) {
  43         54  
  43         145  
67 117 100       158 if (ref ${$ref}->{$_}) {
  117 100       220  
    100          
68 28         44 push @stack, \${$ref}->{$_};
  28         99  
69 89         206 } elsif (readonly ${$ref}->{$_}) {
70 4 100       6 ${$ref}->{$_} = delete ${$ref}->{$_} ? TRUE : FALSE;
  4         16  
  4         10  
71 85         239 } elsif (isdual ${$ref}->{$_}) {
72 19         30 ${$ref}->{$_} += 0;
  19         54  
73             }
74             }
75 2         11 } elsif (isdual ${$ref}) {
76 1         2 ${$ref} += 0;
  1         4  
77             }
78             }
79              
80 18         70 return $data;
81             }
82              
83             sub _encode_yaml($) {
84 9     9   68 require YAML::XS;
85              
86             # replace booleans for YAML::XS (accepts only boolean and JSON::PP::Boolean
87             # since 0.67 and PL_sv_yes/no in earlier versions). No roundtrip for
88             # versions < 0.67: 1 and 0 used for booleans (there is no way to set
89             # PL_sv_yes/no into arrays/hashes without XS code)
90              
91 9         27 my ($false, $true) = (0, 1);
92              
93 9 50       73 local $YAML::XS::Boolean = "JSON::PP" if ($YAML::XS::VERSION >= 0.67);
94              
95 9 50       38 if ($YAML::XS::VERSION >= 0.67) {
96 4 50   4   3122 return YAML::XS::Dump($_[0]) if (ref TRUE eq 'JSON::PP::Boolean');
  4     2   57060  
  4         344  
  2         19  
  2         6  
  2         151  
  9         369  
97              
98 0         0 require JSON::PP;
99 0         0 ($false, $true) = (JSON::PP::false(), JSON::PP::true());
100             }
101              
102 0         0 my @stack = (\$_[0]);
103 0         0 my $ref;
104 0         0 my $bool_type = ref TRUE;
105              
106 0         0 while ($ref = shift @stack) {
107 0 0       0 if (ref ${$ref} eq 'ARRAY') {
  0 0       0  
    0          
108 0         0 for (0 .. $#{${$ref}}) {
  0         0  
  0         0  
109 0 0       0 if (ref ${$ref}->[$_]) {
  0 0       0  
110 0         0 push @stack, \${$ref}->[$_];
  0         0  
111 0         0 } elsif (ref ${$ref}->[$_] eq $bool_type) {
112 0 0       0 ${$ref}->[$_] = ${$ref}->[$_] ? $true : $false;
  0         0  
  0         0  
113             }
114             }
115 0         0 } elsif (ref ${$ref} eq 'HASH') {
116 0         0 for (keys %{${$ref}}) {
  0         0  
  0         0  
117 0 0       0 if (ref ${$ref}->{$_}) {
  0 0       0  
118 0         0 push @stack, \${$ref}->{$_};
  0         0  
119 0         0 } elsif (ref ${$ref}->{$_} eq $bool_type) {
120 0 0       0 ${$ref}->{$_} = ${$ref}->{$_} ? $true : $false;
  0         0  
  0         0  
121             }
122             }
123 0         0 } elsif (ref ${$ref} eq $bool_type) {
124 0 0       0 ${$ref} = ${$ref} ? $true : $false;
  0         0  
  0         0  
125             }
126             }
127              
128 0         0 return YAML::XS::Dump($_[0]);
129             }
130              
131             sub s_decode($$;$) {
132 282     282 0 866 my ($data, $fmt, $opts) = @_;
133 282         625 my $format = uc($fmt);
134              
135 282 100       762 if ($format eq 'JSON') {
    100          
    50          
136 263 50       465 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  263         1159  
  263         1831  
137 263         827 $data = eval {
138             JSON->new(
139             )->allow_nonref($o->{allow_nonref}
140             )->canonical($o->{canonical}
141             )->pretty($o->{pretty}
142             )->relaxed($o->{relaxed}
143 263         8055 )->decode($data);
144             };
145             } elsif ($format eq 'YAML') {
146 18         39 $data = eval { _decode_yaml($data) };
  18         81  
147             } elsif ($format eq 'RAW') {
148             ;
149             } else {
150 0         0 die_fatal "Unable to decode '$fmt' (not supported)";
151             }
152              
153 282 100       1011 die_fatal "Failed to decode '$fmt': " . $@, 4 if $@;
154              
155 281         1490 return $data;
156             }
157              
158             sub s_dump(@) {
159 110     110 0 338 my ($uri, $fmt, $opts) = (shift, shift, shift);
160              
161 110 50       370 $uri = \*STDOUT if ($uri eq '-');
162              
163 110 100       350 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
164 110         297 my $data = join('', map { s_encode($_, $fmt, $opts) } @_);
  115         329  
165              
166 110 100       422 if (ref $uri eq 'GLOB') {
167 38         1667 print $uri $data;
168             } else {
169 72         230 s_dump_file($uri, $data);
170             }
171             }
172              
173             sub s_dump_file($$) {
174 72     72 0 210 my ($file, $data) = @_;
175              
176 72 50       6074 open(my $fh, '>', $file) or die_fatal "Failed to open '$file' ($!)", 2;
177 72         848 print $fh $data;
178 72         6166 close($fh);
179             }
180              
181             sub s_encode($$;$) {
182 122     122 0 357 my ($data, $fmt, $opts) = @_;
183 122         338 my $format = uc($fmt);
184              
185 122 100 100     583 if ($format eq 'JSON' or $format eq 'RAW' and ref $data) {
    100 100        
    50          
186 110 100       203 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  110         428  
  110         686  
187 110         321 $data = eval {
188             JSON->new(
189             )->allow_nonref($o->{allow_nonref}
190             )->canonical($o->{canonical}
191             )->pretty($o->{pretty}
192 110         2847 )->encode($data);
193             };
194             } elsif ($format eq 'YAML') {
195 9         21 $data = eval { _encode_yaml($data) };
  9         37  
196             } elsif ($format eq 'RAW') {
197 3         9 $data .= "\n";
198             } else {
199 0         0 die_fatal "Unable to encode to '$fmt' (not supported)";
200             }
201              
202 122 50       831 die_fatal "Failed to encode structure to $fmt: " . $@, 4 if $@;
203              
204 122         690 return $data;
205             }
206              
207             sub s_fmt_by_uri($) {
208 323     323 0 14125 my @names = split(/\./, basename(shift));
209 323 100 66     2164 if (@names and @names > 1) {
210 322         1022 my $ext = uc(pop @names);
211 322 100 66     1615 return 'YAML' if ($ext eq 'YML' or $ext eq 'YAML');
212             }
213              
214 322         889 return 'JSON'; # by default
215             }
216              
217             sub s_load($$;@) {
218 278     278 0 754 my ($uri, $fmt, %opts) = @_;
219              
220 278 50       805 $uri = \*STDIN if ($uri eq '-');
221 278         701 my $data = s_load_uri($uri);
222 276 100       1223 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
223              
224 276         896 return s_decode($data, $fmt);
225             }
226              
227             sub s_load_uri($) {
228 278     278 0 546 my $uri = shift;
229 278         452 my $data;
230              
231 278 50       693 if (ref $uri eq 'GLOB') {
232 0         0 $data = do { local $/; <$uri> };
  0         0  
  0         0  
233             } else {
234 278 100       12783 open(my $fh, '<', $uri) or
235             die_fatal "Failed to open file '$uri' ($!)", 2;
236 276         1000 $data = do { local $/; <$fh> }; # load whole file
  276         1415  
  276         8070  
237 276         3589 close($fh);
238             }
239              
240 276         1094 return $data;
241             }
242              
243             1;