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   32070 use 5.010001;
  7         20  
4 7     7   25 use strict;
  7         8  
  7         119  
5 7     7   20 use warnings;
  7         8  
  7         162  
6              
7 7     7   39 use English qw( -no_match_vars );
  7         7  
  7         43  
8 7     7   2174 use Exporter 5.57 qw( import );
  7         91  
  7         192  
9 7         369 use File::DataClass::Constants qw( CYGWIN EXCEPTION_CLASS MSOFT STORAGE_BASE
10 7     7   384 STORAGE_EXCEPTIONS );
  7         72  
11 7     7   3219 use Hash::Merge qw( merge );
  7         13473  
  7         355  
12 7     7   38 use List::Util qw( first );
  7         7  
  7         331  
13 7     7   3177 use Module::Pluggable::Object;
  7         24024  
  7         197  
14 7     7   38 use Module::Runtime qw( require_module );
  7         9  
  7         29  
15 7     7   235 use Scalar::Util qw( blessed );
  7         11  
  7         286  
16 7     7   27 use Try::Tiny;
  7         8  
  7         304  
17 7     7   24 use Unexpected::Functions qw( is_class_loaded Unspecified );
  7         7  
  7         88  
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 10335 my ($class, $opts) = @_; $opts //= {};
  42         163  
100              
101 42 100 100     538 not $opts->{ignore_loaded} and is_class_loaded( $class ) and return 1;
102              
103 9     4   444 try { require_module( $class ) } catch { throw( $_ ) };
  9         417  
  1         438  
104              
105 8 100       3318 is_class_loaded( $class )
106             or throw( 'Class [_1] loaded but package undefined', [ $class ] );
107              
108 7         205 return 1;
109             }
110              
111             { my $_extension_map = { '_map_loaded' => 0 };
112              
113             sub extension_map (;$$) {
114 17     17 1 1672 my ($class, $extensions) = @_;
115              
116 17 100       45 if (defined $class) {
117 5 50       14 if (defined $extensions) { # uncoverable branch false
118 5 100       19 is_arrayref( $extensions ) or $extensions = [ $extensions ];
119              
120 5         12 for my $extn (@{ $extensions }) {
  5         11  
121 6   100     35 $_extension_map->{ $extn } //= [];
122             is_member( $class, $_extension_map->{ $extn } )
123 6 100       19 or push @{ $_extension_map->{ $extn } }, $class;
  5         25  
124             }
125             }
126              
127 5         14 return;
128             }
129              
130 12 100       54 $_extension_map->{ '_map_loaded' } and return $_extension_map;
131              
132 3         5 my $base = STORAGE_BASE;
133 3         6 my $exceptions = STORAGE_EXCEPTIONS;
134 3         38 my $finder = Module::Pluggable::Object->new
135             ( except => [ $exceptions ], search_path => [ $base ], require => 1, );
136              
137 3         33 $finder->plugins; $_extension_map->{ '_map_loaded' } = 1;
  3         7560  
138              
139 3         48 return $_extension_map;
140             }
141             }
142              
143             sub first_char ($) {
144 1499     1499 1 4230 return substr $_[ 0 ], 0, 1;
145             }
146              
147             sub is_arrayref (;$) {
148 737 100 100 737 1 3049 return $_[ 0 ] && ref $_[ 0 ] eq 'ARRAY' ? 1 : 0;
149             }
150              
151             sub is_coderef (;$) {
152 647 100 100 647 1 3112 return $_[ 0 ] && ref $_[ 0 ] eq 'CODE' ? 1 : 0;
153             }
154              
155             sub is_hashref (;$) {
156 1025 100 100 1025 1 5442 return $_[ 0 ] && ref $_[ 0 ] eq 'HASH' ? 1 : 0;
157             }
158              
159             sub is_member (;@) {
160 34 100   34 1 56 my ($candidate, @args) = @_; $candidate or return;
  34         63  
161              
162 33 100       65 is_arrayref $args[ 0 ] and @args = @{ $args[ 0 ] };
  31         55  
163              
164 33 100   25   170 return (first { $_ eq $candidate } @args) ? 1 : 0;
  25         123  
165             }
166              
167             sub is_mswin () {
168 544 50   544 1 3961 return $LC_OSNAME eq MSOFT ? 1 : 0;
169             }
170              
171             sub is_ntfs () {
172 505 50 33 505 1 2258 return is_mswin || $LC_OSNAME eq CYGWIN ? 1 : 0;
173             }
174              
175             sub is_stale (;$$$) {
176 86     86 1 85 my ($data, $cache_mtime, $path_mtime) = @_;
177              
178             # Assume NTFS does not support mtime
179 86 50       125 is_ntfs() and return 1; # uncoverable branch true
180              
181 86   100     435 my $is_def = defined $data && defined $path_mtime && defined $cache_mtime;
182              
183 86 100 100     364 return (!$is_def || ($path_mtime > $cache_mtime)) ? 1 : 0;
184             }
185              
186             sub map_extension2class ($) {
187 8     8 1 314 my $map = extension_map();
188              
189 8 100       81 return exists $map->{ $_[ 0 ] } ? $map->{ $_[ 0 ] } : undef;
190             }
191              
192             sub merge_attributes ($$;$) {
193 7     7 1 361 my ($dest, $src, $attrs) = @_; my $class = blessed $src;
  7         24  
194              
195 7   66     9 for (grep { not exists $dest->{ $_ } or not defined $dest->{ $_ } }
  14         41  
196 7 100       26 @{ $attrs || [] }) {
197 13 100       143 my $v = $class ? ($src->can( $_ ) ? $src->$_() : undef) : $src->{ $_ };
    100          
198              
199 13 100       126 defined $v and $dest->{ $_ } = $v;
200             }
201              
202 7         14 return $dest;
203             }
204              
205             sub merge_file_data ($$) {
206 5     5 1 362 my ($existing, $new) = @_;
207              
208 5         8 for (keys %{ $new }) {
  5         14  
209             $existing->{ $_ } = exists $existing->{ $_ }
210             ? merge( $existing->{ $_ }, $new->{ $_ } )
211 11 100       29 : $new->{ $_ };
212             }
213              
214 5         2787 return;
215             }
216              
217             sub merge_for_update (;$$$) {
218 21     21 1 10491 my ($dest_ref, $src, $filter) = @_; my $updated = 0;
  21         28  
219              
220 21 100       48 $dest_ref or throw( Unspecified, [ 'destination reference' ] );
221              
222 20   100 3   25 ${ $dest_ref } //= {}; $src //= {}; $filter //= sub { keys %{ $_[ 0 ] } };
  20   100     52  
  20   100     44  
  20         54  
  3         3  
  3         10  
223              
224 20         45 for my $k ($filter->( $src )) {
225 24 100       60 if (defined $src->{ $k }) {
    100          
226 22         25 my $res = $_merge_attr->( \${ $dest_ref }->{ $k }, $src->{ $k } );
  22         66  
227              
228 22   100     78 $updated ||= $res;
229             }
230 2         5 elsif (exists ${ $dest_ref }->{ $k }) {
231 1         2 delete ${ $dest_ref }->{ $k }; $updated = 1;
  1         1  
  1         3  
232             }
233             }
234              
235 20         69 return $updated;
236             }
237              
238             sub qualify_storage_class ($) {
239 21     21 1 53 return STORAGE_BASE.'::'.$_[ 0 ];
240             }
241              
242             sub supported_extensions () {
243 1     1 1 335 return grep { not m{ \A _ }mx } keys %{ extension_map() };
  3         9  
  1         3  
244             }
245              
246             sub thread_id () {
247             # uncoverable branch true
248 111 50   111 1 265 return exists $INC{ 'threads.pm' } ? threads->tid() : 0;
249             }
250              
251             sub throw (;@) {
252 47     47 1 351 EXCEPTION_CLASS->throw( @_ );
253             }
254              
255             1;
256              
257             __END__