File Coverage

blib/lib/JSON/Builder.pm
Criterion Covered Total %
statement 176 194 90.7
branch 28 40 70.0
condition 8 15 53.3
subroutine 34 34 100.0
pod 5 9 55.5
total 251 292 85.9


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             JSON::Builder - to build large JSON with temp files when memory limit, and compress optionaly.
4              
5             =head1 SYNOPSIS
6              
7             use JSON::Builder;
8            
9             my $json = JSON::XS->new()->utf8(1)->ascii(1);
10             my ($fh, $filename) = tempfile();
11             unlink $filename;
12            
13             my $builder = JSON::Builder->new(json => $json, fh => $fh);
14             or
15             my $builder = JSON::Builder::Compress->new(json => $json, fh => $fh); # Compress, Base64
16            
17             my $fv = $builder->val( { a => 'b', c => 'd' } );
18            
19             my $l = $builder->list();
20             $l->add( { 1 => 'a', 2 => 'b' } );
21             $l->add( { 1 => 'c', 2 => 'd' } );
22             my $fl = $l->end();
23            
24             my $o = $builder->obj();
25             $o->add( o1 => ['a', 'b'] );
26             $o->add( o2 => ['c', 'd'] );
27             my $fo = $o->end();
28            
29             my %d = (
30             one => 1,
31             v => $fv,
32             l => $fl,
33             o => $fo,
34             );
35            
36             $builder->encode(\%d);
37            
38             # print for test
39             $fh->flush();
40             $fh->seek(0,0);
41             print <$fh>;
42              
43             =head1 MOTIVATION
44              
45             Task: to create JSON while having the memory limitations.
46              
47             If you have only one large value in JSON, or, large values are created one by one, you can use the streaming generator. Otherwise, you should use such a perl structure where large elements are the filehandle with the json fragments. When a perl structure is transformed into json, it bypasses and large elements are excluded from the files. The result json is written into the file.
48              
49             =head1 DESCRIPTION
50              
51             =head2 JSON::Builder
52              
53             =head3 new
54              
55             The constructor accepts the following arguments:
56              
57             =over
58              
59             =item json
60              
61             JSON object with the encode and allow_nonref methods support, e.g. JSON::XS.
62              
63             =item fh
64              
65             The filehandle of the file where the result should be written into.
66              
67             =item read_in
68              
69             LENGTH of L function. Optional.
70              
71             =back
72              
73             my $builder = JSON::Builder->new(json => $json, fh => $fh);
74              
75             =head3 val
76              
77             It turns the data to JSON, saves JSON into the variable file created and returns the filehandle of this temporary file:
78              
79             my $fv = $builder->val( { a => 'b', c => 'd' } );
80              
81             =head3 list
82              
83             Its returns the object JSON::Builder::List
84              
85             =head3 obj
86              
87             Its returns the object JSON::Builder::Obj
88              
89             =head3 encode
90              
91             Turns the passed data structure into JSON.
92              
93             my %d = (
94             one => 1,
95             v => $fv, # file handler if $builder->val(...)
96             l => $fl, # file handler of JSON::Builder::List
97             o => $fo, # file handler of JSON::Builder::Obj
98             );
99              
100             $builder->encode(\%d)
101              
102             =head2 JSON::Builder::List
103              
104             It is aimed to write the JSON elements list into the temporary file.
105              
106             my $l = $builder->list();
107             $l->add( { 1 => 'a', 2 => 'b' } );
108             $l->add( { 1 => 'c', 2 => 'd' } );
109             my $fl = $l->end();
110              
111             =head3 new
112              
113             Don't use the constructor directly: use the object list method JSON::Builder.
114              
115             =head3 add
116              
117             It adds the element:
118              
119             =head3 end
120              
121             It returns the filehandle of the file with the JSON list.
122              
123             =head2 JSON::Builder::Obj
124              
125             It is for writing the JSON Obj to the temporary file.
126              
127             my $o = $builder->obj();
128             $o->add( o1 => ['a', 'b'] );
129             $o->add( o2 => ['c', 'd'] );
130             my $fo = $o->end();
131              
132             =head3 new
133              
134             Don't use the constructor directly: use the object obj method JSON::Builder.
135              
136             =head3 add
137              
138             Its adds the key-value
139              
140             =head3 end
141              
142             It returns the filehandle of the file with the JSON object.
143              
144             =head2 JSON::Builder::Compress
145              
146             To ensure that the results file includes the JSON packed, use JSON::Builder::Compress instead of JSON::Builder.
147             The packing algorithm: deflate ةع Compress::Zlib.
148             The results of that is encoded with the help of encode_base64url ةع MIME::Base64.
149              
150             JSON::Builder::Compress constructor can additionally take optional arguments:
151              
152             =over
153              
154             =item fh_plain
155              
156             Filehandle to save not compressed json.
157              
158             =item encode_sub
159              
160             Sub to encode chunk of compressed data. Default is sub { MIME::Base64::encode_base64url($_[0], "") }.
161              
162             =item encode_chunk_size
163              
164             Size of chunk of compressed data. Default is 57 (see MIME::Base64)
165              
166             =back
167              
168              
169             =head2 Inheritance
170              
171             If you want to use your own processing algorithm of the JSON portions, you should redeclarate the init, write, write_flush methods for the JSON::Builder object.
172              
173             =head1 AUTHOR
174              
175             Nick Kostyria
176              
177             =head1 COPYRIGHT AND LICENSE
178              
179             Copyright (C) 2013 by Nick Kostyria
180              
181             This library is free software; you can redistribute it and/or modify
182             it under the same terms as Perl itself, either Perl version 5.14.2 or,
183             at your option, any later version of Perl 5 you may have available.
184              
185             =cut
186              
187             package JSON::Builder;
188 1     1   111498 use strict;
  1         1  
  1         27  
189 1     1   3 use warnings;
  1         2  
  1         29  
190              
191             our $VERSION = '0.04';
192              
193 1     1   3 use Carp;
  1         9  
  1         53  
194 1     1   3 use File::Temp qw(tempfile);
  1         1  
  1         407  
195              
196             sub new {
197 3     3 1 5230 my $proto = shift;
198 3   33     12 my $class = ref($proto) || $proto;
199              
200 3         7 my $self = { @_ };
201              
202 3         9 $$self{json}->allow_nonref(1);
203              
204 3         4 bless $self, $class;
205              
206 3         6 $self->init();
207              
208 3         6 return $self;
209             }
210              
211              
212             sub init {
213 1     1 0 1 my $self = shift;
214             }
215              
216              
217             sub val {
218 3     3 1 19 my $self = shift;
219 3         3 my ($val) = @_;
220            
221 3         3 my $json_val = eval { $$self{json}->encode($val) };
  3         21  
222 3 50       7 if ($@) {
223 0         0 carp $@;
224 0         0 return;
225             }
226              
227 3         7 my ($fh) = tempfile(UNLINK => 1);
228 3         680 print $fh $json_val;
229 3         75 $fh->flush;
230 3         18 $fh->seek(0,0);
231              
232 3         17 return $fh;
233             }
234              
235              
236             sub list {
237 6     6 1 21 my $self = shift;
238 6         26 JSON::Builder::List->new(%$self);
239             }
240              
241              
242             sub obj {
243 6     6 1 12 my $self = shift;
244 6         28 JSON::Builder::Obj->new(%$self);
245             }
246              
247              
248             sub encode {
249 3     3 1 28 my $self = shift;
250 3         3 my ($d) = @_;
251              
252 3         4 my $json = $$self{json};
253 3         2 my $fh = $$self{fh};
254              
255 3         5 $self->kv($d);
256 3         6 $self->write_flush();
257              
258 3         52 $fh->flush;
259 3         9 $fh->seek(0,0);
260             }
261              
262              
263             sub kv {
264 21     21 0 18 my $self = shift;
265 21         13 my ($d) = @_;
266              
267 21 50       46 if (ref $d eq "ARRAY") {
    100          
    100          
    50          
268 0         0 $self->write("[");
269 0         0 my $i = @$d;
270 0         0 foreach (@$d) {
271 0         0 $self->kv($_);
272 0 0       0 $self->write(",") if --$i;
273             }
274 0         0 $self->write("]");
275             } elsif (ref $d eq "HASH") {
276 3         4 my $json = $$self{json};
277 3         6 $self->write("{");
278 3         3 my $i = keys %$d;
279 3         9 foreach (keys %$d) {
280 18         32 $self->write($json->encode($_), ':');
281 18         24 $self->kv($$d{$_});
282 18 100       34 $self->write(",") if --$i;
283             }
284 3         6 $self->write("}");
285             } elsif (ref $d eq "GLOB") {
286 15   50     81 while (read($d, my $buf, $$self{read_in} || 57000)) {
287 15         16 $self->write($buf);
288             }
289             } elsif (not ref $d) {
290 3         4 my $json = $$self{json};
291 3         9 $self->write($json->encode($d));
292             }
293             }
294              
295              
296             sub write {
297 19     19 0 10 my $self = shift;
298 19         14 print { $$self{fh} } @_;
  19         36  
299             }
300              
301             sub write_flush {
302 1     1 0 1 my $self = shift;
303             }
304              
305              
306             package JSON::Builder::List;
307 1     1   3 use strict;
  1         1  
  1         32  
308 1     1   6 use warnings;
  1         1  
  1         39  
309              
310 1     1   4 use File::Temp qw(tempfile);
  1         1  
  1         204  
311              
312             sub new {
313 6     6   6 my $proto = shift;
314 6   33     20 my $class = ref($proto) || $proto;
315              
316 6         17 my $self = { @_, first => 1 };
317              
318 6         22 ($$self{fh}, my $filename) = tempfile();
319 6         1271 unlink $filename;
320              
321 6         16 bless $self, $class;
322 6         16 return $self;
323             }
324              
325              
326             sub add {
327 6     6   25 my $self = shift;
328 6         7 my ($val) = @_;
329              
330 6         7 my $json_val = eval { $$self{json}->encode($val) };
  6         22  
331 6 50       11 if ($@) {
332 0         0 carp $@;
333 0         0 return;
334             }
335              
336 6 100       8 if ($$self{first}) {
337 3         5 $$self{first} = 0;
338 3         2 print { $$self{fh} } "[", $json_val;
  3         22  
339             } else {
340 3         4 print { $$self{fh} } ",", $json_val;
  3         6  
341             }
342             }
343              
344              
345             sub end {
346 6     6   9 my $self = shift;
347 6         6 my $fh = $$self{fh};
348              
349 6 100       11 if ($$self{first}) {
350 3         4 $$self{first} = 0;
351 3         25 print $fh "[";
352             }
353 6         8 print $fh "]";
354              
355 6         104 $fh->flush;
356 6         16 $fh->seek(0,0);
357 6         33 return $fh;
358             }
359              
360              
361              
362             package JSON::Builder::Obj;
363 1     1   3 use strict;
  1         1  
  1         18  
364 1     1   5 use warnings;
  1         1  
  1         19  
365              
366 1     1   3 use File::Temp qw(tempfile);
  1         1  
  1         228  
367              
368             sub new {
369 6     6   7 my $proto = shift;
370 6   33     19 my $class = ref($proto) || $proto;
371              
372 6         21 my $self = { @_, first => 1 };
373              
374 6         13 ($$self{fh}, my $filename) = tempfile();
375 6         1306 unlink $filename;
376              
377 6         16 bless $self, $class;
378 6         18 return $self;
379             }
380              
381              
382             sub add {
383 6     6   22 my $self = shift;
384 6         4 my ($key, $val) = @_;
385              
386 6         7 my $json_key = eval { $$self{json}->encode($key) };
  6         22  
387 6 50       9 if ($@) {
388 0         0 carp $@;
389 0         0 return;
390             }
391              
392 6         3 my $json_val = eval { $$self{json}->encode($val) };
  6         13  
393 6 50       9 if ($@) {
394 0         0 carp $@;
395 0         0 return;
396             }
397              
398 6 100       8 if ($$self{first}) {
399 3         4 $$self{first} = 0;
400 3         2 print { $$self{fh} } "{", $json_key, ":", $json_val;
  3         28  
401             } else {
402 3         3 print { $$self{fh} } ",", $json_key, ":", $json_val;
  3         5  
403             }
404             }
405              
406              
407             sub end {
408 6     6   10 my $self = shift;
409 6         7 my $fh = $$self{fh};
410              
411 6 100       10 if ($$self{first}) {
412 3         4 $$self{first} = 0;
413 3         26 print $fh "{";
414             }
415 6         4 print $fh "}";
416              
417 6         111 $fh->flush;
418 6         17 $fh->seek(0,0);
419 6         34 return $fh;
420             }
421              
422              
423              
424             package JSON::Builder::Compress; # Compress, Base64
425 1     1   4 use strict;
  1         1  
  1         18  
426 1     1   5 use warnings;
  1         1  
  1         19  
427 1     1   3 use base qw(JSON::Builder);
  1         0  
  1         75  
428              
429 1     1   8 use Compress::Zlib;
  1         1  
  1         202  
430 1     1   4 use MIME::Base64 qw(encode_base64url);
  1         1  
  1         324  
431              
432             sub init {
433 2     2   2 my $self = shift;
434            
435 2         7 $$self{x} = deflateInit();
436 2         497 $$self{write_buf} = "";
437              
438 1     1   4 $$self{encode_sub} ||= sub { encode_base64url($_[0], "") },
439 2   100     14 $$self{encode_chunk_size} ||= 57;
      100        
440             }
441              
442              
443             sub write {
444 38     38   28 my $self = shift;
445              
446 38         40 my $buf = join "", @_;
447              
448 38 100       48 print { $$self{fh_plain} } $buf if $$self{fh_plain};
  19         24  
449              
450 38         55 my ($output, $status) = $$self{x}->deflate($buf);
451 38 50       212 $status == Z_OK or die "deflation failed\n";
452              
453 38 100       130 if ($output) {
454 2         5 my $write_buf = join "", $$self{write_buf}, $output;
455 2         1 my $encode_chunk_size = $$self{encode_chunk_size};
456 2         5 my $l = int(length($write_buf)/ $encode_chunk_size) * $encode_chunk_size;
457 2 50       5 if ($l) {
458 0         0 my $buf_head = substr $write_buf, 0, $l;
459 0         0 $$self{write_buf} = substr $write_buf, $l;
460 0         0 print { $$self{fh} } $$self{encode_sub}->($buf_head);
  0         0  
461             } else {
462 2         5 $$self{write_buf} = $write_buf;
463             }
464             }
465             };
466              
467              
468             sub write_flush {
469 2     2   2 my $self = shift;
470              
471 2         5 my ($output, $status) = $$self{x}->flush();
472 2 50       119 $status == Z_OK or die "deflation failed\n";
473              
474 2 50       9 if ($output) {
475 2         4 $$self{write_buf} .= $output;
476             }
477              
478 2         2 print { $$self{fh} } $$self{encode_sub}->($$self{write_buf});
  2         6  
479              
480 2         70 $$self{write_buf} = "";
481             }
482              
483              
484             1;