File Coverage

blib/lib/DBIx/Squirrel/util.pm
Criterion Covered Total %
statement 77 115 66.9
branch 35 76 46.0
condition 12 27 44.4
subroutine 19 26 73.0
pod 13 13 100.0
total 156 257 60.7


line stmt bran cond sub pod time code
1 9     9   131 use strict;
  9         19  
  9         357  
2 9     9   42 use warnings;
  9         15  
  9         425  
3 9     9   138 use 5.010_001;
  9         29  
4              
5             package # hide from PAUSE
6             DBIx::Squirrel::util;
7              
8             =pod
9              
10             =encoding UTF-8
11              
12             =head1 NAME
13              
14             DBIx::Squirrel::util - Utilities
15              
16             =head1 DESCRIPTION
17              
18             A collection of helper functions used by other DBIx::Squirrel packages.
19              
20             =cut
21              
22             our @ISA = qw(Exporter);
23             our @EXPORT;
24             our %EXPORT_TAGS = ( all => [
25             our @EXPORT_OK = qw(
26             callbacks
27             callbacks_args
28             carpf
29             cluckf
30             confessf
31             decrypt
32             get_file_contents
33             global_destruct_phase
34             has_callbacks
35             slurp
36             uncompress
37             unmarshal
38             utf8decode
39             )
40             ] );
41              
42 9     9   58 use Carp ();
  9         17  
  9         191  
43 9     9   5479 use Compress::Bzip2 ();
  9         108742  
  9         316  
44 9     9   4347 use Devel::GlobalDestruction ();
  9         22551  
  9         297  
45 9     9   4650 use Dotenv ();
  9         188850  
  9         299  
46 9     9   5054 use Encode ();
  9         158329  
  9         456  
47 9     9   126 use Exporter ();
  9         48  
  9         358  
48 9     9   5178 use JSON::Syck ();
  9         33629  
  9         285  
49 9     9   5732 use DBIx::Squirrel::Crypt::Fernet ();
  9         52  
  9         14833  
50              
51             if ( -e '.env' ) {
52             Dotenv->load();
53             }
54              
55             =head2 EXPORTS
56              
57             Nothing is exported by default.
58              
59             =cut
60              
61              
62             =head3 C
63              
64             @callbacks = callbacks(\@array);
65             $count = callbacks(\@array);
66              
67             When called in list-context, this function removes and returns any trailing
68             CODEREFs found in the array referenced by the only argument. Be mindful that
69             this operation potentially alters the referenced array.
70              
71             When called in scalar-context then the function returns a non-zero count of
72             the number of trailing CODEREFs found, or C if there were none. When
73             called in scalar-context then the array is not altered, even if there were
74             trailing CODEREFs.
75              
76             =cut
77              
78             sub callbacks {
79 22 50 33 22 1 99 return unless 1 == @_ && UNIVERSAL::isa( $_[0], 'ARRAY' );
80 22         55 goto &_callbacks;
81             }
82              
83             sub _callbacks {
84 22 100   22   49 return unless my @splice = _has_callbacks( $_[0] );
85 8 50       15 return $splice[1] unless wantarray;
86 8         10 return splice @{ $_[0] }, $splice[0], $splice[1];
  8         37  
87             }
88              
89              
90             =head3 C
91              
92             (\@callbacks, @arguments) = callbacks_args(@argments);
93              
94             When using C, some calls allow the caller to reshape results
95             before they are returned, using transformation pipelines. A transformation
96             pipeline is one or more contiguous code-references presented at the end of
97             a call's argument list.
98              
99             Th C function inspects an array of arguments, moving any
100             trailing code-references from the source array into a separate array — the
101             transformation pipeline. It returns a reference to that array, followed by
102             any remaining arguments, to the caller.
103              
104             (\@callbacks, @arguments) = &callbacks_args;
105              
106             The terse C<&>-sigil calling style causes C to use the
107             calling function's C<@_> array.
108              
109             =cut
110              
111             sub callbacks_args {
112 22 100   22 1 350 return [], @_ unless my @callbacks = callbacks( \@_ );
113 8         34 return \@callbacks, @_;
114             }
115              
116              
117             =head3 C
118              
119             Emits a warning without a stack-trace.
120              
121             carpf();
122              
123             The warning will be set to C<$@> if it contains something useful. Otherwise
124             an "Unhelpful warning" will be emitted.
125              
126             carpf($message);
127             carpf(\@message);
128              
129             The warning will be set to C<$message>, or the concatenated C<@message> array,
130             or C<$@>, if there is no viable message. If there is still no viable message
131             then an "Unhelpful warning" is emitted.
132              
133             During concatenation, the elements of the C<@message> array are separated
134             by a single space. The intention is to allow for long warning messages to be
135             split apart in a tidier manner.
136              
137             carpf($format, @arguments);
138             carpf(\@format, @arguments);
139              
140             The warning is composed using a C format-string (C<$format>), together
141             with any remaining arguments. Alternatively, the format-string may be produced
142             by concatenating the C<@format> array whose elements are separated by a single
143             space.
144              
145             =cut
146              
147             sub carpf {
148 4     4 1 2829 @_ = do {
149 4 100       10 if (@_) {
150 3         3 my $format = do {
151 3 50       16 if ( UNIVERSAL::isa( $_[0], 'ARRAY' ) ) {
152 0         0 join ' ', @{ +shift };
  0         0  
153             }
154             else {
155 3         5 shift;
156             }
157             };
158 3 100       6 if (@_) {
159 1         5 sprintf $format, @_;
160             }
161             else {
162 2 50 66     9 $format or $@ or 'Unhelpful warning';
163             }
164             }
165             else {
166 1 50       6 $@ or 'Unhelpful warning';
167             }
168             };
169 4         41 goto &Carp::carp;
170             }
171              
172              
173             =head3 C
174              
175             Emits a warning with a stack-trace.
176              
177             cluckf();
178              
179             The warning will be set to C<$@> if it contains something useful. Otherwise
180             an "Unhelpful warning" will be emitted.
181              
182             cluckf($message);
183             cluckf(\@message);
184              
185             The warning will be set to C<$message>, or the concatenated C<@message> array,
186             or C<$@>, if there is no viable message. If there is still no viable message
187             then an "Unhelpful warning" is emitted.
188              
189             During concatenation, the elements of the C<@message> array are separated
190             by a single space. The intention is to allow for long warning messages to be
191             split apart in a tidier manner.
192              
193             cluckf($format, @arguments);
194             cluckf(\@format, @arguments);
195              
196             The warning is composed using a C format-string (C<$format>), together
197             with any remaining arguments. Alternatively, the format-string may be produced
198             by concatenating the C<@format> array whose elements are separated by a single
199             space.
200              
201             =cut
202              
203             sub cluckf {
204 5     5 1 329685 @_ = do {
205 5 100       14 if (@_) {
206 4         8 my $format = do {
207 4 50       22 if ( UNIVERSAL::isa( $_[0], 'ARRAY' ) ) {
208 0         0 join ' ', @{ +shift };
  0         0  
209             }
210             else {
211 4         10 shift;
212             }
213             };
214 4 100       11 if (@_) {
215 1         4 sprintf $format, @_;
216             }
217             else {
218 3 50 66     16 $format or $@ or 'Unhelpful warning';
219             }
220             }
221             else {
222 1 50       6 $@ or 'Unhelpful warning';
223             }
224             };
225 5         84 goto &Carp::cluck;
226             }
227              
228              
229             =head3 C
230              
231             Throws and exception with a stack-trace.
232              
233             confessf();
234              
235             The error will be set to C<$@> if it contains something useful (effectivly
236             re-throwing the previous exception). Otherwise it will an "Unknown error"
237             exception is thrown.
238              
239             confessf($message);
240             confessf(\@message);
241              
242             The error will be set to C<$message>, or the concatenated C<@message> array,
243             or C<$@>, if there is no viable message. If there is still no viable message
244             then an "Unknown error" is thrown.
245              
246             During concatenation, the elements of the C<@message> array are separated
247             by a single space. The intention is to allow for long error messages to be
248             split apart in a tidier manner.
249              
250             confessf($format, @arguments);
251             confessf(\@format, @arguments);
252              
253             The error message is composed using a C format-string (C<$format>),
254             together with any remaining arguments. Alternatively, the format-string may
255             be produced by concatenating the C<@format> array whose elements are separated
256             by a single space.
257              
258             =cut
259              
260             sub confessf {
261 8     8 1 4524 @_ = do {
262 8 100       18 if (@_) {
263 6         8 my $format = do {
264 6 50       26 if ( UNIVERSAL::isa( $_[0], 'ARRAY' ) ) {
265 0         0 join ' ', @{ +shift };
  0         0  
266             }
267             else {
268 6         11 shift;
269             }
270             };
271 6 100       14 if (@_) {
272 1         4 sprintf $format, @_;
273             }
274             else {
275 5 100 100     22 $format or $@ or 'Unknown error';
276              
277             }
278             }
279             else {
280 2 100       12 $@ or 'Unknown error';
281             }
282             };
283 8         87 goto &Carp::confess;
284             }
285              
286              
287             =head3 C
288              
289             $buffer = decrypt($fernet_key);
290             $buffer = decrypt($buffer, $fernet_key);
291              
292             Decrypts a Fernet-encrypted buffer, returning the decrypted data.
293              
294             A Fernet key can be provided as the second argument, and this can be a
295             Base64-encoded string or a C instance. If no
296             second argument is defined, the function will fall back to using the
297             C environment variable, and if that isn't defined then an
298             exception will be thrown.
299              
300             If C<$buffer> is omitted then C<$_> will be used.
301              
302             =cut
303              
304             sub decrypt {
305 0     0 1 0 my $fernet = pop;
306 0 0       0 my $buffer = @_ ? shift : $_;
307 0 0       0 unless ( defined $fernet ) {
308 0 0       0 unless ( defined $ENV{FERNET_KEY} ) {
309 0         0 confessf [
310             "Neither a Fernet key nor a Fernet object have been",
311             "defined. Decryption is impossible",
312             ];
313             }
314 0         0 $fernet = $ENV{FERNET_KEY};
315             }
316 0 0       0 $fernet = DBIx::Squirrel::Crypt::Fernet->new($fernet)
317             unless UNIVERSAL::isa( $fernet, 'DBIx::Squirrel::Crypt::Fernet' );
318 0         0 return $_ = $fernet->decrypt($buffer);
319             }
320              
321              
322             =head3 C
323              
324             $contents = get_file_contents($filename[, \%options]);
325              
326             Return the entire contents of a file to the caller.
327              
328             The file is read in raw (binary) mode. What happens to the contents after
329             reading depends on the file's name and/or the contents of C<%options>:
330              
331             =over
332              
333             =item *
334              
335             If ".encrypted" forms part of the file's name or the C option is
336             true, then the file contents will be decrypted after they have been read
337             using the Fernet key provided in the C option or the C
338             environment variable.
339              
340             =item *
341              
342             If ".bz2" forms part of the file's name or the C option is
343             true, then the file contents will be uncompressed after they have been read
344             and possibly decrypted.
345              
346             =item *
347              
348             If ".json" forms part of the file's name or the C option is
349             true, then the file contents will be unmarshalled after they have been read,
350             possibly decrypted, and possibly uncompressed.
351              
352             =item *
353              
354             If the C option is true, then the file contents will be decoded
355             as a UTF-8 string.
356              
357             =back
358              
359             =cut
360              
361             sub get_file_contents {
362 0     0 1 0 my $filename = shift;
363 0 0       0 my $options = { utf8decode => !!1, %{ shift || {} } };
  0         0  
364 0         0 my $contents = slurp($filename);
365             $contents = decrypt( $contents, $options->{fernet} )
366 0 0 0     0 if $filename =~ /\.encrypted\b/ || $options->{decrypt};
367             $contents = uncompress($contents)
368 0 0 0     0 if $filename =~ /\.bz2\b/ || $options->{uncompress};
369             return unmarshal($contents)
370 0 0 0     0 if $filename =~ /\.json\b/ || $options->{unmarshal};
371             return utf8decode($contents)
372 0 0       0 if $options->{utf8decode};
373 0         0 return $_ = $contents;
374             }
375              
376              
377             =head3 C
378              
379             $bool = global_destruct_phase();
380              
381             Detects whether the Perl program is in the Global Destruct Phase. Knowing
382             this can make C methods safer. Perl versions older than 5.14
383             don't support the ${^GLOBAL_PHASE} variable, so provide a shim that
384             works regardless of Perl version.
385              
386             =cut
387              
388             sub global_destruct_phase {
389 0     0 1 0 return Devel::GlobalDestruction::in_global_destruction();
390             }
391              
392              
393             =head3 C
394              
395             ($position, $count) = has_callbacks(\@array);
396              
397             When called in list-context, this function returns the starting position
398             and a count of the trailing CODEREFs found in the array referenced in the
399             only argument. If no trailing CODEREFs were found then the function will
400             return an empty list.
401              
402             When called in scalar-context then a truthy value indicating the presence
403             of callbacks will be returned.
404              
405             =cut
406              
407             sub has_callbacks {
408 6 50 33 6 1 5655 return unless 1 == @_ && UNIVERSAL::isa( $_[0], 'ARRAY' );
409 6         10 goto &_has_callbacks;
410             }
411              
412             sub _has_callbacks {
413 28     28   35 my $n = my $s = scalar @{ $_[0] };
  28         44  
414 28   100     110 $n-- while $n && UNIVERSAL::isa( $_[0][ $n - 1 ], 'CODE' );
415 28 100       135 return if $n == $s;
416 10 100       36 return $n ? ( $n, $s - $n ) : ( 0, $s ) if wantarray;
    50          
417 0           return $n;
418             }
419              
420              
421             =head3 C
422              
423             $buffer = slurp();
424             $buffer = slurp($filename);
425              
426             Reads the entirety of the specified file in raw mode, returning the contents.
427              
428             If C<$filename> is omitted then C<$_> will be used.
429              
430             =cut
431              
432             sub slurp {
433 0 0   0 1   my $filename = @_ ? shift : $_;
434 0 0         open my $fh, '<:raw', $filename
435             or confessf "$! - $filename";
436 0           read $fh, my $buffer, -s $filename;
437 0           close $fh;
438 0           return $_ = $buffer;
439             }
440              
441              
442             =head3 C
443              
444             $buffer = uncompress();
445             $buffer = uncompress($buffer);
446              
447             Uncompresses a Bzip2-compressed buffer, returning the uncompressed data.
448              
449             If C<$buffer> is omitted then C<$_> will be used.
450              
451             =cut
452              
453             sub uncompress {
454 0 0   0 1   my $buffer = @_ ? shift : $_;
455 0           return $_ = Compress::Bzip2::memBunzip($buffer);
456             }
457              
458              
459             =head3 C
460              
461             $data = unmarshal($json);
462             $data = unmarshal($json, $decode);
463              
464             Unmarshals a JSON-encoded buffer into the data-structure it represents. By
465             default, UTF-8 binaries are properly decoded, and this behaviour can be
466             inhibited by setting C<$decode> to false.
467              
468             =cut
469              
470             sub unmarshal {
471 0     0 1   my $json = shift;
472 0 0         my $decode = @_ ? !!shift : !!1;
473 0           local $JSON::Syck::ImplicitUnicode = $decode;
474 0 0         return $_ = JSON::Syck::Load( $decode ? utf8decode($json) : $json );
475             }
476              
477              
478             =head3 C
479              
480             $string = utf8decode();
481             $string = utf8decode($buffer);
482              
483             Decode a byte buffer, returning a UTF-8 string.
484              
485             If C<$buffer> is omitted then C<$_> will be used.
486              
487             =cut
488              
489             sub utf8decode {
490 0 0   0 1   my $buffer = @_ ? shift : $_;
491 0           return $_ = Encode::decode_utf8( $buffer, @_ );
492             }
493              
494             =head1 AUTHORS
495              
496             Iain Campbell
497              
498             =head1 COPYRIGHT AND LICENSE
499              
500             The DBIx::Squirrel module is Copyright (c) 2020-2025 Iain Campbell.
501             All rights reserved.
502              
503             You may distribute under the terms of either the GNU General Public
504             License or the Artistic License, as specified in the Perl 5.10.0 README file.
505              
506             =head1 SUPPORT / WARRANTY
507              
508             DBIx::Squirrel is free Open Source software. IT COMES WITHOUT WARRANTY OF ANY KIND.
509              
510             =cut
511              
512             1;