File Coverage

lib/Hash/Persistent.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


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