File Coverage

lib/File/DataClass/Functions.pm
Criterion Covered Total %
statement 119 119 100.0
branch 49 54 96.3
condition 34 37 91.8
subroutine 34 34 100.0
pod 18 18 100.0
total 254 262 98.0


line stmt bran cond sub pod time code
1             package File::DataClass::Functions;
2              
3 7     7   34316 use 5.010001;
  7         31  
4 7     7   36 use strict;
  7         14  
  7         149  
5 7     7   44 use warnings;
  7         15  
  7         200  
6              
7 7     7   38 use English qw( -no_match_vars );
  7         14  
  7         47  
8 7     7   2236 use Exporter 5.57 qw( import );
  7         132  
  7         225  
9 7         430 use File::DataClass::Constants qw( CYGWIN EXCEPTION_CLASS MSOFT STORAGE_BASE
10 7     7   383 STORAGE_EXCEPTIONS );
  7         44  
11 7     7   3459 use Hash::Merge qw( merge );
  7         12980  
  7         396  
12 7     7   50 use List::Util qw( first );
  7         15  
  7         362  
13 7     7   3522 use Module::Pluggable::Object;
  7         33566  
  7         219  
14 7     7   51 use Module::Runtime qw( require_module );
  7         19  
  7         38  
15 7     7   314 use Scalar::Util qw( blessed );
  7         16  
  7         297  
16 7     7   37 use Try::Tiny;
  7         16  
  7         415  
17 7     7   40 use Unexpected::Functions qw( is_class_loaded Unspecified );
  7         14  
  7         71  
18              
19             our @EXPORT_OK = qw( ensure_class_loaded extension_map first_char
20             is_arrayref is_coderef is_hashref is_member is_mswin
21             is_ntfs is_stale qualify_storage_class
22             map_extension2class merge_attributes merge_file_data
23             merge_for_update supported_extensions thread_id throw );
24             our %EXPORT_TAGS = ( all => [ @EXPORT_OK ], );
25              
26             my $LC_OSNAME = lc $OSNAME;
27              
28             # Private functions
29             my $_merge_attr;
30              
31             my $_merge_attr_arrays = sub {
32             my ($to, $from) = @_; my $updated = 0;
33              
34             for (0 .. $#{ $to }) {
35             if (defined $from->[ $_ ]) {
36             my $res = $_merge_attr->( \$to->[ $_ ], $from->[ $_ ] );
37              
38             $updated ||= $res;
39             }
40             elsif ($to->[ $_ ]) { splice @{ $to }, $_; $updated = 1; last }
41             }
42              
43             if (@{ $from } > @{ $to }) {
44             push @{ $to }, (splice @{ $from }, $#{ $to } + 1); $updated = 1;
45             }
46              
47             return $updated;
48             };
49              
50             my $_merge_attr_hashes = sub {
51             my ($to, $from) = @_; my $updated = 0;
52              
53             for (grep { exists $from->{ $_ } } keys %{ $to }) {
54             if (defined $from->{ $_ }) {
55             my $res = $_merge_attr->( \$to->{ $_ }, $from->{ $_ } );
56              
57             $updated ||= $res;
58             }
59             else { delete $to->{ $_ }; delete $from->{ $_ }; $updated = 1 }
60             }
61              
62             for (grep { not exists $to->{ $_ } } keys %{ $from }) {
63             if (defined $from->{ $_ }) {
64             $to->{ $_ } = $from->{ $_ }; $updated = 1;
65             }
66             }
67              
68             return $updated;
69             };
70              
71             $_merge_attr = sub {
72             my ($to_ref, $from) = @_; my $to = ${ $to_ref }; my $updated = 0;
73              
74             if ($to and ref $to eq 'HASH') {
75             $updated = $_merge_attr_hashes->( $to, $from );
76             }
77             elsif ($to and ref $to eq 'ARRAY') {
78             $updated = $_merge_attr_arrays->( $to, $from );
79             }
80             elsif (defined $to and $to ne $from) {
81             $updated = 1; ${ $to_ref } = $from;
82             }
83             elsif (not defined $to) {
84             if (ref $from eq 'HASH') {
85             scalar keys %{ $from } > 0 and $updated = 1
86             and ${ $to_ref } = $from;
87             }
88             elsif (ref $from eq 'ARRAY') {
89             scalar @{ $from } > 0 and $updated = 1 and ${ $to_ref } = $from;
90             }
91             else { $updated = 1; ${ $to_ref } = $from }
92             }
93              
94             return $updated;
95             };
96              
97             # Public functions
98             sub ensure_class_loaded ($;$) {
99 42   100 42 1 11710 my ($class, $opts) = @_; $opts //= {};
  42         244  
100              
101 42 100 100     268 not $opts->{ignore_loaded} and is_class_loaded( $class ) and return 1;
102              
103 9     4   447 try { require_module( $class ) } catch { throw( $_ ) };
  9         525  
  1         435  
104              
105 8 100       3528 is_class_loaded( $class )
106             or throw( 'Class [_1] loaded but package undefined', [ $class ] );
107              
108 7         237 return 1;
109             }
110              
111             { my $_extension_map = { '_map_loaded' => 0 };
112              
113             sub extension_map (;$$) {
114 17     17 1 2510 my ($class, $extensions) = @_;
115              
116 17 100       64 if (defined $class) {
117 5 50       22 if (defined $extensions) { # uncoverable branch false
118 5 100       21 is_arrayref( $extensions ) or $extensions = [ $extensions ];
119              
120 5         13 for my $extn (@{ $extensions }) {
  5         18  
121 6   100     47 $_extension_map->{ $extn } //= [];
122             is_member( $class, $_extension_map->{ $extn } )
123 6 100       23 or push @{ $_extension_map->{ $extn } }, $class;
  5         34  
124             }
125             }
126              
127 5         21 return;
128             }
129              
130 12 100       121 $_extension_map->{ '_map_loaded' } and return $_extension_map;
131              
132 3         12 my $base = STORAGE_BASE;
133 3         7 my $exceptions = STORAGE_EXCEPTIONS;
134 3         37 my $finder = Module::Pluggable::Object->new
135             ( except => [ $exceptions ], search_path => [ $base ], require => 1, );
136              
137 3         42 $finder->plugins; $_extension_map->{ '_map_loaded' } = 1;
  3         10696  
138              
139 3         54 return $_extension_map;
140             }
141             }
142              
143             sub first_char ($) {
144 1499     1499 1 6234 return substr $_[ 0 ], 0, 1;
145             }
146              
147             sub is_arrayref (;$) {
148 737 100 100 737 1 4329 return $_[ 0 ] && ref $_[ 0 ] eq 'ARRAY' ? 1 : 0;
149             }
150              
151             sub is_coderef (;$) {
152 647 100 100 647 1 3957 return $_[ 0 ] && ref $_[ 0 ] eq 'CODE' ? 1 : 0;
153             }
154              
155             sub is_hashref (;$) {
156 1025 100 100 1025 1 7257 return $_[ 0 ] && ref $_[ 0 ] eq 'HASH' ? 1 : 0;
157             }
158              
159             sub is_member (;@) {
160 34 100   34 1 100 my ($candidate, @args) = @_; $candidate or return;
  34         103  
161              
162 33 100       93 is_arrayref $args[ 0 ] and @args = @{ $args[ 0 ] };
  31         87  
163              
164 33 100   25   211 return (first { $_ eq $candidate } @args) ? 1 : 0;
  25         166  
165             }
166              
167             sub is_mswin () {
168 544 50   544 1 5246 return $LC_OSNAME eq MSOFT ? 1 : 0;
169             }
170              
171             sub is_ntfs () {
172 505 50 33 505 1 3159 return is_mswin || $LC_OSNAME eq CYGWIN ? 1 : 0;
173             }
174              
175             sub is_stale (;$$$) {
176 86     86 1 180 my ($data, $cache_mtime, $path_mtime) = @_;
177              
178             # Assume NTFS does not support mtime
179 86 50       207 is_ntfs() and return 1; # uncoverable branch true
180              
181 86   100     448 my $is_def = defined $data && defined $path_mtime && defined $cache_mtime;
182              
183 86 100 100     453 return (!$is_def || ($path_mtime > $cache_mtime)) ? 1 : 0;
184             }
185              
186             sub map_extension2class ($) {
187 8     8 1 327 my $map = extension_map();
188              
189 8 100       101 return exists $map->{ $_[ 0 ] } ? $map->{ $_[ 0 ] } : undef;
190             }
191              
192             sub merge_attributes ($$;$) {
193 7     7 1 427 my ($dest, $src, $attrs) = @_; my $class = blessed $src;
  7         26  
194              
195 7   66     17 for (grep { not exists $dest->{ $_ } or not defined $dest->{ $_ } }
  14         52  
196 7 100       28 @{ $attrs || [] }) {
197 13 100       105 my $v = $class ? ($src->can( $_ ) ? $src->$_() : undef) : $src->{ $_ };
    100          
198              
199 13 100       154 defined $v and $dest->{ $_ } = $v;
200             }
201              
202 7         21 return $dest;
203             }
204              
205             sub merge_file_data ($$) {
206 5     5 1 439 my ($existing, $new) = @_;
207              
208 5         12 for (keys %{ $new }) {
  5         18  
209             $existing->{ $_ } = exists $existing->{ $_ }
210             ? merge( $existing->{ $_ }, $new->{ $_ } )
211 11 100       44 : $new->{ $_ };
212             }
213              
214 5         3024 return;
215             }
216              
217             sub merge_for_update (;$$$) {
218 21     21 1 11767 my ($dest_ref, $src, $filter) = @_; my $updated = 0;
  21         34  
219              
220 21 100       56 $dest_ref or throw( Unspecified, [ 'destination reference' ] );
221              
222 20   100 3   35 ${ $dest_ref } //= {}; $src //= {}; $filter //= sub { keys %{ $_[ 0 ] } };
  20   100     71  
  20   100     60  
  20         68  
  3         5  
  3         12  
223              
224 20         58 for my $k ($filter->( $src )) {
225 24 100       73 if (defined $src->{ $k }) {
    100          
226 22         37 my $res = $_merge_attr->( \${ $dest_ref }->{ $k }, $src->{ $k } );
  22         76  
227              
228 22   100     92 $updated ||= $res;
229             }
230 2         7 elsif (exists ${ $dest_ref }->{ $k }) {
231 1         3 delete ${ $dest_ref }->{ $k }; $updated = 1;
  1         2  
  1         3  
232             }
233             }
234              
235 20         90 return $updated;
236             }
237              
238             sub qualify_storage_class ($) {
239 21     21 1 76 return STORAGE_BASE.'::'.$_[ 0 ];
240             }
241              
242             sub supported_extensions () {
243 1     1 1 368 return grep { not m{ \A _ }mx } keys %{ extension_map() };
  3         13  
  1         4  
244             }
245              
246             sub thread_id () {
247             # uncoverable branch true
248 111 50   111 1 367 return exists $INC{ 'threads.pm' } ? threads->tid() : 0;
249             }
250              
251             sub throw (;@) {
252 47     47 1 406 EXCEPTION_CLASS->throw( @_ );
253             }
254              
255             1;
256              
257             __END__