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 20     20   136 use strict;
  20         42  
  20         812  
6 20     20   118 use warnings FATAL => 'all';
  20         37  
  20         738  
7 20     20   109 use parent qw(Exporter);
  20         37  
  20         172  
8 19     19   9841 use open qw(:std :utf8);
  19         22726  
  19         113  
9              
10 19     19   2775 use File::Basename qw(basename);
  19         41  
  19         1526  
11 19     19   11999 use JSON qw();
  19         169933  
  19         571  
12 19     19   141 use Scalar::Util qw(readonly);
  19         40  
  19         1101  
13              
14 19     19   116 use App::NDTools::INC;
  19         41  
  19         117  
15 19     19   8723 use App::NDTools::Util qw(is_number);
  19         49  
  19         1095  
16 19     19   8050 use Log::Log4Cli;
  19         211746  
  19         2546  
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             space_before => 0,
35             },
36             );
37              
38             use constant {
39 19         82 TRUE => JSON::true,
40             FALSE => JSON::false,
41 19     19   162 };
  19         38  
42              
43             sub _decode_yaml($) {
44 18     18   2020 require YAML::XS;
45              
46 18         13003 my $data = YAML::XS::Load($_[0]);
47              
48             # YAML::XS decode boolean vals as PL_sv_yes and PL_sv_no, both - read only
49             # at least until https://github.com/ingydotnet/yaml-libyaml-pm/issues/25
50             # second thing here: get rid of dualvars: YAML::XS load numbers as
51             # dualvars, but JSON::XS dumps them as strings =(
52              
53 18         93 my @stack = (\$data);
54 18         31 my $ref;
55              
56 18         66 while ($ref = shift @stack) {
57 49 100       76 if (ref ${$ref} eq 'ARRAY') {
  49 100       122  
    100          
58 4         8 for (0 .. $#{${$ref}}) {
  4         6  
  4         13  
59 13 100       16 if (ref ${$ref}->[$_]) {
  13 100       28  
    100          
60 3         5 push @stack, \${$ref}->[$_];
  3         11  
61 10         22 } elsif (readonly ${$ref}->[$_]) {
62 4 100       6 splice @{${$ref}}, $_, 1, (${$ref}->[$_] ? TRUE : FALSE);
  4         6  
  4         8  
  4         14  
63 6         15 } elsif (is_number ${$ref}->[$_]) {
64 3         5 ${$ref}->[$_] += 0;
  3         8  
65             }
66             }
67 45         109 } elsif (ref ${$ref} eq 'HASH') {
68 43         60 for (keys %{${$ref}}) {
  43         57  
  43         139  
69 117 100       180 if (ref ${$ref}->{$_}) {
  117 100       239  
    100          
70 28         45 push @stack, \${$ref}->{$_};
  28         77  
71 89         210 } elsif (readonly ${$ref}->{$_}) {
72 4 100       6 ${$ref}->{$_} = delete ${$ref}->{$_} ? TRUE : FALSE;
  4         18  
  4         12  
73 85         201 } elsif (is_number ${$ref}->{$_}) {
74 19         32 ${$ref}->{$_} += 0;
  19         56  
75             }
76             }
77 2         10 } elsif (is_number ${$ref}) {
78 1         3 ${$ref} += 0;
  1         4  
79             }
80             }
81              
82 18         57 return $data;
83             }
84              
85             sub _encode_yaml($) {
86 9     9   61 require YAML::XS;
87 9         22 my $modern_yaml_xs = eval { YAML::XS->VERSION(0.67) };
  9         253  
88              
89             # replace booleans for YAML::XS (accepts only boolean and JSON::PP::Boolean
90             # since 0.67 and PL_sv_yes/no in earlier versions). No roundtrip for
91             # versions < 0.67: 1 and 0 used for booleans (there is no way to set
92             # PL_sv_yes/no into arrays/hashes without XS code)
93              
94 9         36 my ($false, $true) = (0, 1);
95              
96 9 50       51 local $YAML::XS::Boolean = "JSON::PP" if ($modern_yaml_xs);
97              
98 9 50       32 if ($modern_yaml_xs) {
99 4 50   4   2967 return YAML::XS::Dump($_[0]) if (ref TRUE eq 'JSON::PP::Boolean');
  4     2   55816  
  4         419  
  2         13  
  2         8  
  2         188  
  9         326  
100              
101 0         0 require JSON::PP;
102 0         0 ($false, $true) = (JSON::PP::false(), JSON::PP::true());
103             }
104              
105 0         0 my @stack = (\$_[0]);
106 0         0 my $ref;
107 0         0 my $bool_type = ref TRUE;
108              
109 0         0 while ($ref = shift @stack) {
110 0 0       0 if (ref ${$ref} eq 'ARRAY') {
  0 0       0  
    0          
111 0         0 for (0 .. $#{${$ref}}) {
  0         0  
  0         0  
112 0 0       0 if (ref ${$ref}->[$_]) {
  0 0       0  
113 0         0 push @stack, \${$ref}->[$_];
  0         0  
114 0         0 } elsif (ref ${$ref}->[$_] eq $bool_type) {
115 0 0       0 ${$ref}->[$_] = ${$ref}->[$_] ? $true : $false;
  0         0  
  0         0  
116             }
117             }
118 0         0 } elsif (ref ${$ref} eq 'HASH') {
119 0         0 for (keys %{${$ref}}) {
  0         0  
  0         0  
120 0 0       0 if (ref ${$ref}->{$_}) {
  0 0       0  
121 0         0 push @stack, \${$ref}->{$_};
  0         0  
122 0         0 } elsif (ref ${$ref}->{$_} eq $bool_type) {
123 0 0       0 ${$ref}->{$_} = ${$ref}->{$_} ? $true : $false;
  0         0  
  0         0  
124             }
125             }
126 0         0 } elsif (ref ${$ref} eq $bool_type) {
127 0 0       0 ${$ref} = ${$ref} ? $true : $false;
  0         0  
  0         0  
128             }
129             }
130              
131 0         0 return YAML::XS::Dump($_[0]);
132             }
133              
134             sub s_decode($$;$) {
135 360     360 0 966 my ($data, $fmt, $opts) = @_;
136 360         815 my $format = uc($fmt);
137              
138 360 100       908 if ($format eq 'JSON') {
    100          
    50          
139 341 50       558 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  341         1474  
  341         2315  
140 341         1091 $data = eval {
141             JSON->new(
142             )->allow_nonref($o->{allow_nonref}
143             )->relaxed($o->{relaxed}
144 341         8749 )->decode($data);
145             };
146             } elsif ($format eq 'YAML') {
147 18         37 $data = eval { _decode_yaml($data) };
  18         59  
148             } elsif ($format eq 'RAW') {
149             ;
150             } else {
151 0         0 die_fatal "Unable to decode '$fmt' (not supported)";
152             }
153              
154 360 100       1137 die_fatal "Failed to decode '$fmt': " . $@, 4 if $@;
155              
156 355         1758 return $data;
157             }
158              
159             sub s_dump(@) {
160 129     129 0 975 my ($uri, $fmt, $opts) = splice @_, 0, 3;
161              
162 129 50       456 $uri = \*STDOUT if ($uri eq '-');
163              
164 129 100       477 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
165 129         330 my $data = join('', map { s_encode($_, $fmt, $opts) } @_);
  134         358  
166              
167 129 100       445 if (ref $uri eq 'GLOB') {
168 41         1751 print $uri $data;
169             } else {
170 88         266 s_dump_file($uri, $data);
171             }
172             }
173              
174             sub s_dump_file($$) {
175 88     88 0 265 my ($file, $data) = @_;
176              
177 88 50       7093 open(my $fh, '>', $file) or die_fatal "Failed to open '$file' ($!)", 2;
178 88         1022 print $fh $data;
179 88         16885 close($fh);
180             }
181              
182             sub s_encode($$;$) {
183 142     142 0 382 my ($data, $fmt, $opts) = @_;
184 142         315 my $format = uc($fmt);
185              
186 142 100 100     613 if ($format eq 'JSON' or $format eq 'RAW' and ref $data) {
    100 100        
    50          
187 130 100       241 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  130         474  
  130         799  
188 130         352 $data = eval {
189             JSON->new(
190             )->allow_nonref($o->{allow_nonref}
191             )->canonical($o->{canonical}
192             )->pretty($o->{pretty}
193             )->space_before($o->{space_before}
194 130         3630 )->encode($data);
195             };
196             } elsif ($format eq 'YAML') {
197 9         22 $data = eval { _encode_yaml($data) };
  9         33  
198             } elsif ($format eq 'RAW') {
199 3         8 $data .= "\n";
200             } else {
201 0         0 die_fatal "Unable to encode to '$fmt' (not supported)";
202             }
203              
204 142 50       559 die_fatal "Failed to encode structure to $fmt: " . $@, 4 if $@;
205              
206 142         668 return $data;
207             }
208              
209             sub s_fmt_by_uri($) {
210 414     414 0 17138 my @names = split(/\./, basename(shift));
211 414 100 66     2718 if (@names and @names > 1) {
212 413         1299 my $ext = uc($names[-1]);
213 413 100 66     1851 return 'YAML' if ($ext eq 'YML' or $ext eq 'YAML');
214             }
215              
216 413         1137 return 'JSON'; # by default
217             }
218              
219             sub s_load($$;@) {
220 353     353 0 902 my ($uri, $fmt, %opts) = @_;
221              
222 353 50       920 $uri = \*STDIN if ($uri eq '-');
223 353         851 my $data = s_load_uri($uri);
224 351 100       1546 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
225              
226 351         1066 return s_decode($data, $fmt);
227             }
228              
229             sub s_load_uri($) {
230 353     353 0 602 my $uri = shift;
231 353         529 my $data;
232              
233 353 50       909 if (ref $uri eq 'GLOB') {
234 0         0 $data = do { local $/; <$uri> };
  0         0  
  0         0  
235             } else {
236 353 100       15072 open(my $fh, '<', $uri) or
237             die_fatal "Failed to open file '$uri' ($!)", 2;
238 351         1201 $data = do { local $/; <$fh> }; # load whole file
  351         1670  
  351         9771  
239 351         4291 close($fh);
240             }
241              
242 351         1410 return $data;
243             }
244              
245             1;