File Coverage

blib/lib/Bot/Cobalt/Serializer.pm
Criterion Covered Total %
statement 91 111 81.9
branch 25 58 43.1
condition 7 18 38.8
subroutine 21 22 95.4
pod 5 6 83.3
total 149 215 69.3


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Serializer;
2             $Bot::Cobalt::Serializer::VERSION = '0.021002';
3 16     16   55538 use strictures 2;
  16         2299  
  16         518  
4 16     16   2150 use Carp;
  16         20  
  16         790  
5 16     16   59 use Scalar::Util 'reftype';
  16         18  
  16         583  
6              
7             ## These two must be present anyway:
8 16     16   5752 use YAML::XS ();
  16         30554  
  16         253  
9 16     16   5977 use JSON::MaybeXS ();
  16         64172  
  16         364  
10              
11 16     16   74 use Fcntl ':flock';
  16         17  
  16         1759  
12 16     16   5662 use Time::HiRes 'sleep';
  16         12189  
  16         67  
13 16     16   2003 use Scalar::Util 'blessed';
  16         48  
  16         590  
14              
15 16     16   1431 use Bot::Cobalt::Common ':types';
  16         23  
  16         92  
16              
17 16     16   3102 use Moo;
  16         40602  
  16         83  
18              
19              
20             has Format => (
21             is => 'rw',
22             isa => Str,
23 65     65   1079 builder => sub { 'YAMLXS' },
24             trigger => sub {
25             my ($self, $format) = @_;
26             $format = uc($format);
27             confess "Unknown format $format"
28             unless grep { $_ eq $format } keys %{ $self->_types };
29             confess "Requested format $format but can't find a module for it"
30             unless $self->_check_if_avail($format)
31             },
32             );
33              
34             has _types => (
35             lazy => 1,
36             is => 'ro',
37             isa => HashRef,
38             builder => sub {
39             +{
40 4     4   1399 YAML => 'YAML::Syck',
41             YAMLXS => 'YAML::XS',
42             JSON => 'JSON::MaybeXS',
43             }
44             },
45             );
46              
47             has yamlxs_from_ref => (
48             is => 'rw',
49             lazy => 1,
50             coerce => sub { YAML::XS::Dump($_[0]) },
51             );
52              
53             has ref_from_yamlxs => (
54             is => 'rw',
55             lazy => 1,
56             coerce => sub { YAML::XS::Load($_[0]) },
57             );
58              
59             has yaml_from_ref => (
60             is => 'rw',
61             lazy => 1,
62             coerce => sub { require YAML::Syck; YAML::Syck::Dump($_[0]) },
63             );
64              
65             has ref_from_yaml => (
66             is => 'rw',
67             lazy => 1,
68             coerce => sub { require YAML::Syck; YAML::Syck::Load($_[0]) },
69             );
70              
71             has json_from_ref => (
72             is => 'rw',
73             lazy => 1,
74             coerce => sub {
75             my $jsify = JSON::MaybeXS->new(
76             utf8 => 1, allow_nonref => 1, convert_blessed => 1
77             );
78             $jsify->utf8->encode($_[0]);
79             },
80             );
81              
82             has ref_from_json => (
83             is => 'rw',
84             lazy => 1,
85             coerce => sub {
86             my $jsify = JSON::MaybeXS->new(
87             utf8 => 1, allow_nonref => 1
88             );
89             $jsify->utf8->decode($_[0])
90             },
91             );
92              
93              
94             sub BUILDARGS {
95 69     69 0 27948 my ($class, @args) = @_;
96             ## my $serializer = Bot::Cobalt::Serializer->new( %opts )
97             ## Serialize to YAML using YAML::XS:
98             ## ->new()
99             ## - or -
100             ## ->new($format)
101             ## ->new('JSON') # f.ex
102             ## - or -
103             ## ->new( Format => 'JSON' ) ## --> to JSON
104             ## - or -
105             ## ->new( Format => 'YAML' ) ## --> to YAML1.0
106 69 100       938 @args == 1 ? { Format => $args[0] } : { @args }
107             }
108              
109             sub freeze {
110             ## ->freeze($ref)
111 5     5 1 1287 my ($self, $ref) = @_;
112 5 50       13 unless (defined $ref) {
113 0         0 carp "freeze() received no data";
114             return
115 0         0 }
116              
117 5         51 my $method = lc( $self->Format );
118 5         1458 $method = $method . "_from_ref";
119              
120 5         17 $self->$method($ref)
121             }
122              
123             sub thaw {
124             ## ->thaw($data)
125 25     25 1 752 my ($self, $data) = @_;
126 25 50       70 unless (defined $data) {
127 0         0 carp "thaw() received no data";
128             return
129 0         0 }
130              
131 25         389 my $method = lc( $self->Format );
132 25         3570 $method = "ref_from_" . $method ;
133              
134 25         252 $self->$method($data)
135             }
136              
137             sub writefile {
138 3     3 1 2180 my ($self, $path, $ref, $opts) = @_;
139             ## $serializer->writefile($path, $ref [, { Opts });
140              
141 3 50       21 if (!$path) {
    50          
142 0         0 confess "writefile called without path argument"
143             } elsif (!defined $ref) {
144 0         0 confess "writefile called without data to serialize"
145             }
146              
147 3         11 my $frozen = $self->freeze($ref);
148              
149 3         45 $self->_write_serialized($path, $frozen, $opts)
150             }
151              
152             sub readfile {
153 20     20 1 920 my ($self, $path, $opts) = @_;
154             ## my $ref = $serializer->readfile($path)
155              
156 20 50       220 if (!$path) {
    50          
157 0         0 confess "readfile called without path argument";
158             } elsif (!-e $path ) {
159 0         0 confess "readfile called on nonexistant file $path";
160             }
161              
162 20         369 my $data = $self->_read_serialized($path, $opts);
163              
164 20         7343 $self->thaw($data)
165             }
166              
167             sub version {
168 0     0 1 0 my ($self) = @_;
169              
170 0         0 my $module = $self->_types->{ $self->Format };
171 0         0 { local $@; eval "require $module" }
  0         0  
  0         0  
172 0         0 return($module, $module->VERSION);
173             }
174              
175              
176              
177             sub _check_if_avail {
178 4     4   6 my ($self, $type) = @_;
179              
180 4         6 my $module;
181 4 50       57 return unless $module = $self->_types->{$type};
182              
183             {
184 4         28 local $@;
  4         4  
185 4         207 eval "require $module";
186 4 50       22 return if $@;
187             }
188              
189 4         215 return $module
190             }
191              
192              
193             sub _read_serialized {
194 20     20   26 my ($self, $path, $opts) = @_;
195 20 50       48 return unless defined $path;
196              
197 20         28 my $lock = 1;
198 20 0 33     59 if (defined $opts && ref $opts && reftype $opts eq 'HASH') {
      33        
199 0 0       0 $lock = $opts->{Locking} if defined $opts->{Locking};
200             }
201              
202 20 100 66     188 if (blessed $path && $path->can('slurp_utf8')) {
203 17         51 return $path->slurp_utf8
204             } else {
205 3 50       69 open(my $in_fh, '<:encoding(UTF-8)', $path)
206             or confess "open failed for $path: $!";
207              
208 3 50       125 if ($lock) {
209 3 50       15 flock($in_fh, LOCK_SH)
210             or confess "LOCK_SH failed for $path: $!";
211             }
212              
213 3         47 my $data = join '', <$in_fh>;
214              
215 3 50       55 flock($in_fh, LOCK_UN) if $lock;
216              
217 3 50       27 close($in_fh)
218             or carp "close failed for $path: $!";
219              
220 3         13 return $data
221             }
222             }
223              
224             sub _write_serialized {
225 3     3   35 my ($self, $path, $data, $opts) = @_;
226 3 50 33     22 return unless $path and defined $data;
227              
228 3         3 my $lock = 1;
229 3         4 my $timeout = 2;
230              
231 3 0 33     8 if (defined $opts && ref $opts && reftype $opts eq 'HASH') {
      33        
232 0 0       0 $lock = $opts->{Locking} if defined $opts->{Locking};
233 0 0       0 $timeout = $opts->{Timeout} if $opts->{Timeout};
234             }
235              
236 2 50   2   9 open(my $out_fh, '>>:encoding(UTF-8)', $path)
  2         2  
  2         11  
  3         76  
237             or confess "open failed for $path: $!";
238              
239 3 50       1953 if ($lock) {
240 3         3 my $timer = 0;
241              
242 3         18 until ( flock $out_fh, LOCK_EX | LOCK_NB ) {
243 0 0       0 confess "Failed writefile lock ($path), timed out ($timeout)"
244             if $timer > $timeout;
245              
246 0         0 sleep 0.01;
247 0         0 $timer += 0.01;
248             }
249              
250             }
251              
252 3 50       18 seek($out_fh, 0, 0)
253             or confess "seek failed for $path: $!";
254 3 50       94 truncate($out_fh, 0)
255             or confess "truncate failed for $path";
256              
257 3         12 print $out_fh $data;
258              
259 3 50       117 flock($out_fh, LOCK_UN) if $lock;
260              
261 3 50       35 close($out_fh)
262             or carp "close failed for $path: $!";
263              
264 3         19 return 1
265             }
266              
267             1;
268             __END__