File Coverage

blib/lib/App/Greple/xlate/Cache.pm
Criterion Covered Total %
statement 20 119 16.8
branch 0 44 0.0
condition 0 28 0.0
subroutine 7 24 29.1
pod 0 9 0.0
total 27 224 12.0


line stmt bran cond sub pod time code
1             package App::Greple::xlate::Cache;
2              
3 1     1   1420 use v5.14;
  1         4  
4 1     1   37 use warnings;
  1         4  
  1         61  
5              
6 1     1   5 use Data::Dumper;
  1         2  
  1         74  
7 1     1   8 use JSON;
  1         2  
  1         7  
8 1     1   146 use List::Util qw(pairmap mesh);
  1         2  
  1         88  
9 1     1   7 use Hash::Util qw(lock_keys);
  1         2  
  1         10  
10              
11             sub TIEHASH {
12 0     0     my $self = shift;
13 0           my $obj = $self->new(name => @_);
14 0           $obj;
15             }
16              
17             sub EXISTS {
18 0     0     my($obj, $key) = @_;
19 0           $obj->access($key);
20 0 0         exists $obj->current->{$key} or exists $obj->saved->{$key};
21             }
22              
23             sub FETCH {
24 0     0     my($obj, $key) = @_;
25 0           $obj->access($key);
26 0           $obj->get($key);
27             }
28              
29             sub STORE {
30 0     0     my($obj, $key, $val) = @_;
31 0           $obj->access($key);
32 0           $obj->set($key, $val);
33             }
34              
35             sub DESTROY {
36 0     0     my $obj = shift;
37 0           $obj->update;
38             }
39              
40             my %default = (
41             name => '', # cache filename
42             saved => undef, # saved hash
43             current => undef, # current using hash
44             clear => 0, # clean up cache data
45             accessed => {}, # accessed keys
46             order => [], # accessed keys in order
47             accumulate => 0, # do not delete unused entry
48             force_update => 0, # update cache file anyway
49             updated => 0, # number of updated entries
50             format => 'list', # saving cache file format
51             );
52              
53             for my $key (keys %default) {
54 1     1   367 no strict 'refs';
  1         2  
  1         1615  
55 0     0     *{$key} = sub :lvalue { $_[0]->{$key} }
56             }
57              
58             sub new {
59 0     0 0   my $class = shift;
60 0           my $obj = bless { %default }, $class;
61 0           lock_keys %{$obj};
  0            
62 0     0     pairmap { $obj->{$a} = $b } @_;
  0            
63 0 0         $obj->open if $obj->name;
64 0           $obj;
65             }
66              
67             sub access {
68 0     0 0   my $obj = shift;
69 0           my $key = shift;
70 0 0         push @{$obj->order}, $key if not $obj->accessed->{$key}++;
  0            
71             }
72              
73             sub get {
74 0     0 0   my $obj = shift;
75 0           my $key = shift;
76 0   0       $obj->current->{$key} //= delete $obj->saved->{$key};
77             }
78              
79             sub set {
80 0     0 0   my $obj = shift;
81             pairmap {
82 0 0 0 0     if (ref $a eq 'ARRAY' and ref $b eq 'ARRAY') {
83 0 0         @$a == @$b or die;
84 0           $obj->set(mesh $a, $b);
85             } else {
86 0   0       my $c = $obj->current->{$a} //= delete $obj->saved->{$a};
87 0 0 0       if (not defined $c or $c ne $b) {
88 0           $obj->current->{$a} = $b;
89 0           $obj->updated++;
90             }
91             }
92 0           } @_;
93 0           $obj;
94             }
95              
96             sub json {
97 0     0 0   JSON->new->utf8->canonical->pretty;
98             }
99              
100             sub open {
101 0     0 0   my $obj = shift;
102 0   0       my $file = $obj->name || return;
103 0 0         if ($obj->clear) {
104 0 0         warn "created $file\n" unless -f $file;
105 0 0         open my $fh, '>', $file or die "$file: $!\n";
106 0           print $fh "{}\n";
107             }
108 0   0       my $json_obj //= &json;
109 0 0         if (CORE::open my $fh, $file) {
110 0           my $data = do { local $/; <$fh> };
  0            
  0            
111 0 0         my $json = $data eq '' ? {} : $json_obj->decode($data);
112 0           $obj->{saved} = do {
113 0 0         if (ref $json eq 'HASH') { $json }
  0 0          
114 0           elsif (ref $json eq 'ARRAY') { +{ map @{$_}[0,1], @$json } }
  0            
115 0           else { die "unexpected json data." }
116             };
117 0           warn "read cache from $file\n";
118             } else {
119 0           $obj->{saved} = {};
120             }
121 0           $obj;
122             }
123              
124             sub update {
125 0     0 0   my $obj = shift;
126 0   0       my $file = $obj->name || return;
127 0 0 0       if (not $obj->force_update and $obj->updated == 0) {
128 0 0         if (%{$obj->saved} == 0) {
  0 0          
129 0           return;
130             } elsif ($obj->accumulate) {
131 0           for (keys %{$obj->saved}) {
  0            
132 0   0       $obj->current->{$_} //= delete $obj->saved->{$_};
133             }
134             }
135             }
136 0           while (my($k, $v) = each %{$obj->current}) {
  0            
137 0 0         delete $obj->current->{$k} if not defined $v;
138             }
139 0 0         %{$obj->current} > 0 or return;
  0            
140 0   0       my $json_obj //= &json; # this is necessary to be called from DESTROY
141 0 0         if (CORE::open my $fh, '>', $file) {
142 0 0         my $data = $obj->format eq 'list' ? $obj->list_data : $obj->hash_data;
143 0           my $json = $json_obj->encode($data);
144 0           print $fh $json;
145 0           warn "write cache to $file\n";
146             } else {
147 0           warn "$file: $!\n";
148             }
149             }
150              
151             sub hash_data {
152 0     0 0   my $obj = shift;
153 0           $obj->current;
154             }
155              
156             sub list_data {
157 0     0 0   my $obj = shift;
158 0           my %hash = %{$obj->current};
  0            
159 0           my @list;
160 0           for my $key (@{$obj->order}) {
  0            
161 0 0         if (exists $hash{$key}) {
162 0           push @list, [ $key => delete $hash{$key} ];
163             } else {
164 0           warn "$key: not in cache.";
165             }
166             }
167 0           for my $key (sort keys %hash) {
168 0           warn "$key: not in order list.";
169 0           push @list, [ $key => delete $hash{$key} ];
170             }
171 0 0         die if %hash;
172 0           \@list;
173             }
174              
175             1;