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         37  
  20         766  
6 20     20   107 use warnings FATAL => 'all';
  20         38  
  20         729  
7 20     20   109 use parent qw(Exporter);
  20         39  
  20         156  
8 19     19   9841 use open qw(:std :utf8);
  19         23048  
  19         116  
9              
10 19     19   2867 use File::Basename qw(basename);
  19         42  
  19         1534  
11 19     19   11442 use JSON qw();
  19         163888  
  19         560  
12 19     19   132 use Scalar::Util qw(readonly);
  19         40  
  19         1053  
13              
14 19     19   118 use App::NDTools::INC;
  19         41  
  19         119  
15 19     19   9147 use App::NDTools::Util qw(is_number);
  19         51  
  19         1080  
16 19     19   7951 use Log::Log4Cli;
  19         211138  
  19         2492  
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 19         76 TRUE => JSON::true,
39             FALSE => JSON::false,
40 19     19   153 };
  19         37  
41              
42             sub _decode_yaml($) {
43 18     18   2133 require YAML::XS;
44              
45 18         12991 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         90 my @stack = (\$data);
53 18         39 my $ref;
54              
55 18         67 while ($ref = shift @stack) {
56 49 100       76 if (ref ${$ref} eq 'ARRAY') {
  49 100       124  
    100          
57 4         7 for (0 .. $#{${$ref}}) {
  4         6  
  4         12  
58 13 100       18 if (ref ${$ref}->[$_]) {
  13 100       26  
    100          
59 3         5 push @stack, \${$ref}->[$_];
  3         8  
60 10         21 } elsif (readonly ${$ref}->[$_]) {
61 4 100       5 splice @{${$ref}}, $_, 1, (${$ref}->[$_] ? TRUE : FALSE);
  4         5  
  4         9  
  4         14  
62 6         15 } elsif (is_number ${$ref}->[$_]) {
63 3         5 ${$ref}->[$_] += 0;
  3         8  
64             }
65             }
66 45         101 } elsif (ref ${$ref} eq 'HASH') {
67 43         60 for (keys %{${$ref}}) {
  43         52  
  43         137  
68 117 100       154 if (ref ${$ref}->{$_}) {
  117 100       232  
    100          
69 28         42 push @stack, \${$ref}->{$_};
  28         78  
70 89         202 } elsif (readonly ${$ref}->{$_}) {
71 4 100       8 ${$ref}->{$_} = delete ${$ref}->{$_} ? TRUE : FALSE;
  4         17  
  4         8  
72 85         202 } elsif (is_number ${$ref}->{$_}) {
73 19         35 ${$ref}->{$_} += 0;
  19         55  
74             }
75             }
76 2         11 } elsif (is_number ${$ref}) {
77 1         3 ${$ref} += 0;
  1         5  
78             }
79             }
80              
81 18         56 return $data;
82             }
83              
84             sub _encode_yaml($) {
85 9     9   68 require YAML::XS;
86 9         21 my $modern_yaml_xs = eval { YAML::XS->VERSION(0.67) };
  9         295  
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         34 my ($false, $true) = (0, 1);
94              
95 9 50       58 local $YAML::XS::Boolean = "JSON::PP" if ($modern_yaml_xs);
96              
97 9 50       33 if ($modern_yaml_xs) {
98 4 50   4   3176 return YAML::XS::Dump($_[0]) if (ref TRUE eq 'JSON::PP::Boolean');
  4     2   55632  
  4         415  
  2         12  
  2         6  
  2         228  
  9         377  
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 352     352 0 1046 my ($data, $fmt, $opts) = @_;
135 352         734 my $format = uc($fmt);
136              
137 352 100       875 if ($format eq 'JSON') {
    100          
    50          
138 333 50       555 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  333         1309  
  333         2115  
139 333         1018 $data = eval {
140             JSON->new(
141             )->allow_nonref($o->{allow_nonref}
142             )->relaxed($o->{relaxed}
143 333         8336 )->decode($data);
144             };
145             } elsif ($format eq 'YAML') {
146 18         40 $data = eval { _decode_yaml($data) };
  18         57  
147             } elsif ($format eq 'RAW') {
148             ;
149             } else {
150 0         0 die_fatal "Unable to decode '$fmt' (not supported)";
151             }
152              
153 352 100       1112 die_fatal "Failed to decode '$fmt': " . $@, 4 if $@;
154              
155 348         1616 return $data;
156             }
157              
158             sub s_dump(@) {
159 124     124 0 907 my ($uri, $fmt, $opts) = splice @_, 0, 3;
160              
161 124 50       394 $uri = \*STDOUT if ($uri eq '-');
162              
163 124 100       432 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
164 124         324 my $data = join('', map { s_encode($_, $fmt, $opts) } @_);
  129         688  
165              
166 124 100       421 if (ref $uri eq 'GLOB') {
167 40         1709 print $uri $data;
168             } else {
169 84         241 s_dump_file($uri, $data);
170             }
171             }
172              
173             sub s_dump_file($$) {
174 84     84 0 210 my ($file, $data) = @_;
175              
176 84 50       6465 open(my $fh, '>', $file) or die_fatal "Failed to open '$file' ($!)", 2;
177 84         942 print $fh $data;
178 84         6738 close($fh);
179             }
180              
181             sub s_encode($$;$) {
182 137     137 0 351 my ($data, $fmt, $opts) = @_;
183 137         295 my $format = uc($fmt);
184              
185 137 100 100     537 if ($format eq 'JSON' or $format eq 'RAW' and ref $data) {
    100 100        
    50          
186 125 100       204 my $o = { %{$FORMATS{JSON}}, %{$opts || {}} };
  125         455  
  125         734  
187 125         351 $data = eval {
188             JSON->new(
189             )->allow_nonref($o->{allow_nonref}
190             )->canonical($o->{canonical}
191             )->pretty($o->{pretty}
192 125         3201 )->encode($data);
193             };
194             } elsif ($format eq 'YAML') {
195 9         24 $data = eval { _encode_yaml($data) };
  9         42  
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 137 50       564 die_fatal "Failed to encode structure to $fmt: " . $@, 4 if $@;
203              
204 137         652 return $data;
205             }
206              
207             sub s_fmt_by_uri($) {
208 404     404 0 16581 my @names = split(/\./, basename(shift));
209 404 100 66     2567 if (@names and @names > 1) {
210 403         1236 my $ext = uc($names[-1]);
211 403 100 66     1731 return 'YAML' if ($ext eq 'YML' or $ext eq 'YAML');
212             }
213              
214 403         1078 return 'JSON'; # by default
215             }
216              
217             sub s_load($$;@) {
218 347     347 0 855 my ($uri, $fmt, %opts) = @_;
219              
220 347 50       881 $uri = \*STDIN if ($uri eq '-');
221 347         799 my $data = s_load_uri($uri);
222 345 100       1461 $fmt = s_fmt_by_uri($uri) unless (defined $fmt);
223              
224 345         1032 return s_decode($data, $fmt);
225             }
226              
227             sub s_load_uri($) {
228 347     347 0 579 my $uri = shift;
229 347         531 my $data;
230              
231 347 50       854 if (ref $uri eq 'GLOB') {
232 0         0 $data = do { local $/; <$uri> };
  0         0  
  0         0  
233             } else {
234 347 100       14304 open(my $fh, '<', $uri) or
235             die_fatal "Failed to open file '$uri' ($!)", 2;
236 345         1177 $data = do { local $/; <$fh> }; # load whole file
  345         1604  
  345         9450  
237 345         4117 close($fh);
238             }
239              
240 345         1268 return $data;
241             }
242              
243             1;