File Coverage

lib/Hash/Persistent.pm
Criterion Covered Total %
statement 100 108 92.5
branch 44 56 78.5
condition 10 13 76.9
subroutine 12 12 100.0
pod 3 3 100.0
total 169 192 88.0


line stmt bran cond sub pod time code
1             package Hash::Persistent;
2              
3             our $VERSION = '1.02'; # VERSION
4             # ABSTRACT: nested hashref serializable to the file
5              
6              
7 2     2   95100 use strict;
  2         4  
  2         67  
8 2     2   10 use warnings;
  2         4  
  2         77  
9              
10 2     2   10 use autodie qw( open close chmod rename );
  2         2  
  2         14  
11              
12 2     2   2077 use Data::Dumper;
  2         9552  
  2         113  
13 2     2   939 use Storable qw(thaw nfreeze);
  2         4440  
  2         110  
14 2     2   1018 use JSON;
  2         15507  
  2         7  
15 2     2   196 use Carp;
  2         2  
  2         104  
16              
17 2     2   837 use Lock::File 1.01 qw(lockfile);
  2         8835  
  2         1515  
18              
19             my %defaults = (
20             read_only => 0,
21             auto_commit => 0,
22             format => 'auto',
23             lock => 1,
24             write_only => 0,
25             );
26              
27             my $meta = {};
28              
29             sub new {
30 43     43 1 1037338 my $class = shift;
31 43         102 my ($fname, $options, $lock_options) = @_;
32              
33 43   50     186 $lock_options ||= {};
34              
35 43   100     142 $options ||= {};
36 43         306 my $_self = {%defaults, %$options};
37              
38 43 100 100     152 if ($options->{read_only} and $options->{auto_commit}) {
39 1         172 croak "Only one of 'read_only' and 'auto_commit' options can be true";
40             }
41              
42 42 100       75 if ($_self->{read_only}) {
43 5         6 $_self->{auto_commit} = 0;
44 5         6 $_self->{lock} = 0;
45             }
46              
47 42 100       75 if ($_self->{lock}) {
48 37 100       77 $lock_options = $_self->{lock} if ref $_self->{lock};
49 37         271 my $lock = lockfile("$fname.lock", { mode => $_self->{mode}, blocking => 1, remove => 1, %$lock_options });
50 37 100       4984 unless (defined $lock) {
51 1         34 return;
52             }
53 36         73 $_self->{lock} = $lock;
54             }
55 41         56 $_self->{fname} = $fname;
56              
57 41         103 my $self;
58 41 100 100     423 if (-e $fname and not $_self->{write_only}) {
59 23         64 open my $fh, '<', $fname;
60 23         1668 my $data;
61 23         73 local $/;
62 23         242 my $str = <$fh>;
63 23 100       113 if ($str =~ m{^\$data = }) {
    100          
64 5         309 eval $str;
65 5 50       25 die "Can't eval $fname: $@" unless $data;
66 5 50       16 die "Invalid data in $fname: $data" unless ref $data;
67 5         9 $self = $data;
68 5 100       62 $_self->{format} = 'dumper' if $_self->{format} eq 'auto';
69             } elsif ($str =~ /^{/) {
70 14         666 $self = JSON->new->decode($str);
71 14 100       182 $_self->{format} = 'json' if $_self->{format} eq 'auto';
72             }
73             else {
74 4         20 $self = thaw($str);
75 4 100       162 $_self->{format} = 'storable' if $_self->{format} eq 'auto';
76             }
77             } else {
78 18 100       52 $_self->{format} = 'json' if $_self->{format} eq 'auto'; # default format for new files
79 18         25 $self = {};
80             }
81              
82 41         75 bless $self => $class;
83 41         123 $meta->{$self} = $_self;
84 41         121 return $self;
85             }
86              
87             sub commit {
88 27     27 1 1025 my $self = shift;
89 27         41 my $_self = $meta->{$self};
90              
91 27 50       63 if ($_self->{removed}) {
92 0         0 croak "$_self->{fname} is already removed and can't be commited";
93             }
94 27 100       53 if ($_self->{read_only}) {
95 1         79 croak "Can't commit to $_self->{fname}, object is read only";
96             }
97              
98 26         32 my $fname = $_self->{fname};
99 26         42 my $tmp_fname = "$fname.tmp";
100 26         86 open my $tmp, '>', $tmp_fname;
101              
102 26         5697 my $serialized;
103 26 100       96 if ($_self->{format} eq 'dumper') {
    100          
104 6         82 my $dumper = Data::Dumper->new([ { %$self } ], [ qw(data) ]);
105 6         342 $dumper->Terse(0); # somebody could enable terse mode globally; TODO - explicitly specify other options too?
106 6         55 $dumper->Purity(1);
107 6         49 $dumper->Sortkeys(1);
108 6         38 $serialized = $dumper->Dump;
109             }
110             elsif ($_self->{format} eq 'json') {
111 13         231 $serialized = JSON->new->encode({ %$self });
112             }
113             else {
114 7         50 $serialized = nfreeze({ %$self });
115             }
116 25 50       604 print {$tmp} $serialized or die "print failed: $!";
  25         182  
117              
118 25 100       70 chmod $_self->{mode}, $tmp_fname if defined $_self->{mode};
119 25         340 close $tmp;
120 25         3232 rename $tmp_fname => $fname;
121             }
122              
123             sub DESTROY {
124 41     41   4792 local $@;
125 41         52 my $self = shift;
126              
127 41         99 my $_self = $meta->{$self};
128 41 50 33     129 if ($_self->{auto_commit} and not $self->{removed}) {
129 0         0 my $commited = eval {
130 0         0 $self->commit();
131 0         0 1;
132             };
133 0         0 delete $meta->{$self}; # delete object anyway, commited or not
134 0 0       0 unless ($commited) {
135 0         0 ERROR $@;
136             }
137             }
138             else {
139 41         225 delete $meta->{$self};
140             }
141             }
142              
143             sub remove {
144 2     2 1 52 my $self = shift;
145              
146 2         9 my $_self = $meta->{$self};
147 2 50       5 if ($_self->{read_only}) {
148 0         0 croak "Can't remove $_self->{fname}, object is read only";
149             }
150 2 100       15 if (-e $_self->{fname}) {
151 1 50       43 unlink $_self->{fname} or die "Can't remove $_self->{fname}: $!";
152             }
153 2 50       6 if ($_self->{lock}) {
154 2         11 my $lock_fname = $_self->{lock}->name;
155 2 50       19 if (-e $lock_fname) {
156 2 50       40 unlink $lock_fname or die "Can't remove $lock_fname: $!";
157             }
158             }
159 2         4 $_self->{removed} = 1;
160 2         9 delete $self->{$_} for keys %$self;
161             }
162              
163             1;
164              
165             __END__