File Coverage

blib/lib/JSON/JSONFold.pm
Criterion Covered Total %
statement 449 555 80.9
branch 105 158 66.4
condition 62 97 63.9
subroutine 92 116 79.3
pod 12 14 85.7
total 720 940 76.6


line stmt bran cond sub pod time code
1             package JSON::JSONFold;
2              
3 5     5   737547 use strict;
  5         12  
  5         192  
4 5     5   28 use warnings;
  5         9  
  5         347  
5 5     5   113 use 5.014 ;
  5         18  
6 5     5   23 use JSON::PP ();
  5         10  
  5         146  
7              
8 5     5   30 use Exporter 'import';
  5         18  
  5         8177  
9              
10             our $VERSION = '0.1.7';
11             our @EXPORT = qw(
12             format_json write_json fold_text
13             encode_json to_json) ;
14              
15             our @EXPORT_OK = qw(
16             jsonfold_config
17             create_formatter
18             ) ;
19             # Object Orient Interface
20              
21             sub new {
22 19     19 1 71 my ($class, %overrides) = @_ ;
23              
24             # Required parameters
25 19         46 my $width = delete $overrides{width} ;
26 19         40 my $config = delete $overrides{config} ;
27              
28 19   100     142 my $gold = delete $overrides{gold} // 1 ;
29 19         59 my $indent = delete $overrides{indent} ;
30 19   33     81 my $json = delete $overrides{json} // _json_coder($gold, $indent, %overrides) ;
31 19         41 my $do_close = delete $overrides{do_close} ;
32 19   50     60 return bless {
33             json => $json,
34             width => $width,
35             config => _config($config, $width),
36             do_close => $do_close,
37             }, ref($class) || $class || __PACKAGE__ ;
38             }
39              
40             sub format {
41 18     18 1 34 my($self, $data) = @_ ;
42              
43 18         59 my $config = $self->{config} ;
44              
45 18         30 my $output = '' ;
46 18 50       258 open my $out, '>', \$output or die "open output: $!" ;
47              
48 18         51 my $stream = _stream($out, $config, 0) ;
49 18         29 my $json = $self->{json} ;
50 18         55 my $text = $json->encode($data) ;
51              
52 18         3879 $stream->write($text);
53 18         94 $stream->finish;
54 18         40 $stream->flush;
55              
56 18 50       62 close $out or die "close output: $!" ;
57 18 50       77 $output .= "\n" unless $output =~ /\n\z/;
58 18         132 return $output ;
59             }
60              
61             sub fold {
62 0     0 1 0 my ($self, $text) = @_ ;
63              
64 0         0 my $config = $self->{config} ;
65              
66 0         0 my $output = '' ;
67 0 0       0 open my $out, '>', \$output or die "open output: $!" ;
68              
69 0         0 my $stream = _stream($out, $config, 0) ;
70              
71 0         0 $stream->write($text);
72 0         0 $stream->finish;
73 0         0 $stream->flush;
74              
75 0 0       0 close $out or die "close output: $!" ;
76 0 0       0 $output .= "\n" unless $output =~ /\n\z/;
77 0         0 return $output ;
78             }
79              
80             sub write {
81 1     1 1 6 my($self, $data, $fh) = @_ ;
82              
83 1         10 my $config = $self->{config} ;
84              
85 1         3 my $do_close = $self->{do_close} ;
86 1         4 my $stream = _stream($fh, $config, $do_close) ;
87 1         1 my $json = $self->{json} ;
88              
89 1         5 my $text = $json->encode($data) ;
90 1         245 $stream->write($text);
91 1         3 $stream->finish;
92 1         3 $stream->flush;
93 1         4 my $info = $stream->stats ;
94 1         3 $stream->close() ;
95              
96 1         7 return $info ;
97             }
98              
99             # Functional Interface
100              
101             # Not exportable. Allow using DEV::JSONFold::config(...)
102             sub config {
103 0     0 0 0 my($base_config, $width, %overrides) = @_ ;
104 0   0     0 $width //= $overrides{width} ;
105 0         0 return _config($base_config, $width, %overrides) ;
106             }
107              
108             sub jsonfold_config {
109 1     1 1 5 my($base_config, $width, %overrides) = @_ ;
110 1   33     8 $width //= $overrides{width} ;
111 1         4 return _config($base_config, $width, %overrides) ;
112             }
113              
114             sub format_json {
115 0     0 1 0 my($data, $width, $config, %overrides) = @_ ;
116              
117 0         0 my $fmt = __PACKAGE__->new(width => $width, config => $config, %overrides) ;
118 0         0 my $output = $fmt->format($data) ;
119 0         0 return $output ;
120             }
121              
122             sub write_json {
123 1     1 1 5 my($data, $fh, $width, $config, %overrides) = @_ ;
124              
125 1         6 my $fmt = __PACKAGE__->new(width => $width, config => $config, %overrides) ;
126 1         6 my $info = $fmt->write($data, $fh) ;
127 1         11 return $info ;
128             }
129              
130             sub fold_text {
131 0     0 1 0 my($text, $width, $config) = @_ ;
132 0         0 my $fmt = __PACKAGE__->new(width => $width, config => $config) ;
133              
134 0         0 return $fmt->fold($text) ;
135             }
136              
137             sub create_formatter {
138 0     0 1 0 my($fh, $width, $config, %overrides) = @_ ;
139 0         0 my $do_close = delete $overrides{close_fp} ;
140 0         0 return _stream($fh, _config($config, $width, %overrides), $do_close) ;
141             }
142              
143             # Helper for Function/OO interface methods
144              
145             sub _config {
146 20     20   53 my ($preset, $width, %overrides) = @_ ;
147 20 50       82 $overrides{width} = $width if $width ;
148 20         56 return JSON::JSONFold::Config::config($preset, %overrides) ;
149             }
150              
151             sub _stream {
152 19     19   54 my ($fp, $config, $close_fp) = @_ ;
153 19         122 return JSON::JSONFold::Writer->new($fp, $config, $close_fp) ;
154             }
155              
156             sub _json_coder {
157 19     19   46 my ($gold, $indent, %opt) = @_;
158             # Must have valid indent, otherwise cannot parse the data
159 19         136 my $json = JSON::PP->new->pretty ;
160 19 50       1881 if ( $gold ) {
161 19   100     74 my $sort_keys = $opt{sort_keys} // 1 ;
162 19   50     102 $indent //=2 ;
163 19         347 $json->allow_nonref->canonical($sort_keys);
164 19         720 $json->space_before(0)->space_after(1);
165             }
166 19 50       571 $json->indent_length($indent) if defined $indent ;
167 19         170 return $json;
168             }
169              
170             # JSON compatible API - OO
171              
172             sub encode {
173 0     0 1 0 my ($self, $data) = @_ ;
174 0         0 return $self->format($data) ;
175             }
176              
177              
178             # JSON compatiable API - Functional
179              
180             sub encode_json {
181 18     18 1 477698 my ($data, $opts) = @_ ;
182 18 100       93 my %overrides = %$opts if $opts ;
183 18         43 my $width = delete $overrides{width} ;
184 18         35 my $compact = delete $overrides{compact} ;
185              
186 18         147 my $fmt = __PACKAGE__->new(width => $width, config => $compact, %overrides) ;
187 18         62 my $output = $fmt->format($data) ;
188 18         124 return $output ;
189             }
190              
191             # Same as encode_json - for users of legacy "JSON" wrapper.
192              
193             sub to_json {
194 0     0 1 0 my ($data, $opts) = @_ ;
195 0 0       0 my %overrides = %$opts if $opts ;
196 0         0 my $width = delete $overrides{width} ;
197 0         0 my $compact = delete $overrides{compact} ;
198              
199 0         0 my $fmt = __PACKAGE__->new(width => $width, config => $compact, %overrides) ;
200 0         0 my $output = $fmt->format($data) ;
201 0         0 return $output ;
202             }
203            
204             sub run {
205 0     0 0 0 JSON::JSONFold::CLI::run() ;
206             }
207              
208             package JSON::JSONFold::Kind;
209              
210 5     5   54 use strict ;
  5         9  
  5         149  
211 5     5   23 use warnings ;
  5         10  
  5         255  
212 5     5   50 use Exporter 'import';
  5         38  
  5         332  
213              
214             our @EXPORT_OK = qw(
215             KIND_NONE
216             KIND_DICT
217             KIND_LIST
218             %OPENING_KIND
219             %CLOSING_KIND
220             );
221              
222 5     5   40 use constant KIND_NONE => 0;
  5         45  
  5         473  
223 5     5   37 use constant KIND_DICT => 1;
  5         10  
  5         288  
224 5     5   35 use constant KIND_LIST => 2;
  5         9  
  5         532  
225              
226             our %OPENING_KIND = (
227             '{' => KIND_DICT,
228             '[' => KIND_LIST,
229             );
230              
231             our %CLOSING_KIND = (
232             '}' => KIND_DICT,
233             '},' => KIND_DICT,
234             ']' => KIND_LIST,
235             '],' => KIND_LIST,
236             );
237              
238             # -------------------------------------------------------------------------
239             # Internal package: immutable-ish configuration record
240             # -------------------------------------------------------------------------
241              
242             package JSON::JSONFold::Config;
243              
244 5     5   28 use strict;
  5         10  
  5         139  
245 5     5   31 use warnings;
  5         8  
  5         263  
246 5     5   30 use Exporter 'import';
  5         11  
  5         201  
247              
248 5     5   44 use constant MAX_ARRAY_ITEMS => 1000;
  5         11  
  5         404  
249 5     5   32 use constant MAX_OBJ_ITEMS => 1000;
  5         10  
  5         2609  
250 5     5   30 use constant MAX_NESTING => 10;
  5         25  
  5         2022  
251 5     5   32 use constant DEFAULT_WIDTH => 100;
  5         14  
  5         2492  
252              
253             our $SEQ;
254             use constant {
255 5         977 C_OFF => $SEQ++,
256             C_WIDTH => $SEQ++,
257              
258             C_PACK_ARRAY_ITEMS => $SEQ++,
259             C_PACK_OBJ_ITEMS => $SEQ++,
260             C_PACK_NESTING => $SEQ++,
261              
262             C_FOLD_ARRAY_ITEMS => $SEQ++,
263             C_FOLD_OBJ_ITEMS => $SEQ++,
264             C_FOLD_NESTING => $SEQ++,
265              
266             C_JOIN_ARRAY_ITEMS => $SEQ++,
267             C_JOIN_OBJ_ITEMS => $SEQ++,
268             C_JOIN_NESTING => $SEQ++,
269             C_UNUSED_LAST => $SEQ++,
270 5     5   52 };
  5         9  
271              
272             our %PRESETS ; # values defined later
273              
274             BEGIN {
275              
276 5     5   6547 our @EXPORT = qw(
277             C_OFF
278             C_WIDTH
279             C_PACK_ARRAY_ITEMS
280             C_PACK_OBJ_ITEMS
281             C_PACK_NESTING
282             C_FOLD_ARRAY_ITEMS
283             C_FOLD_OBJ_ITEMS
284             C_FOLD_NESTING
285             C_JOIN_ARRAY_ITEMS
286             C_JOIN_OBJ_ITEMS
287             C_JOIN_NESTING
288             );
289             }
290              
291             our @FIELDS = (
292             [ 'off', C_OFF ],
293             [ 'width', C_WIDTH ],
294             [ 'pack_array_items', C_PACK_ARRAY_ITEMS ],
295             [ 'pack_obj_items', C_PACK_OBJ_ITEMS ],
296             [ 'pack_nesting', C_PACK_NESTING ],
297             [ 'fold_array_items', C_FOLD_ARRAY_ITEMS ],
298             [ 'fold_obj_items', C_FOLD_OBJ_ITEMS ],
299             [ 'fold_nesting', C_FOLD_NESTING ],
300             [ 'join_array_items', C_JOIN_ARRAY_ITEMS ],
301             [ 'join_obj_items', C_JOIN_OBJ_ITEMS ],
302             [ 'join_nesting', C_JOIN_NESTING ],
303             );
304              
305             our %NAME_TO_INDEX = map { @$_ } @FIELDS ;
306              
307             sub as_hash {
308 0     0   0 my ($self) = @_ ;
309 0         0 map { my ($name, $idx) = @$_ ; ($name => $self->[$idx]) ; } @FIELDS ;
  0         0  
  0         0  
310             }
311              
312             sub _make {
313 15     15   62 my ($class, %arg) = @_;
314 15         24 my @d;
315 15         48 $#d = $SEQ;
316 15         29 $d[C_OFF] = $arg{off} ;
317 15         25 $d[C_WIDTH] = $arg{width};
318              
319 15         35 $d[C_PACK_ARRAY_ITEMS] = $arg{pack_array_items};
320 15         22 $d[C_PACK_OBJ_ITEMS] = $arg{pack_obj_items};
321 15         36 $d[C_PACK_NESTING] = $arg{pack_nesting};
322              
323 15         52 $d[C_FOLD_ARRAY_ITEMS] = $arg{fold_array_items};
324 15         39 $d[C_FOLD_OBJ_ITEMS] = $arg{fold_obj_items};
325 15         23 $d[C_FOLD_NESTING] = $arg{fold_nesting};
326              
327 15         22 $d[C_JOIN_ARRAY_ITEMS] = $arg{join_array_items};
328 15         25 $d[C_JOIN_OBJ_ITEMS] = $arg{join_obj_items};
329 15         21 $d[C_JOIN_NESTING] = $arg{join_nesting};
330 15         62 return bless \@d, $class;
331             }
332              
333             our $NONE = __PACKAGE__->_make(
334             width => DEFAULT_WIDTH,
335              
336             pack_array_items => 0,
337             pack_obj_items => 0,
338             pack_nesting => 0,
339              
340             fold_array_items => 0,
341             fold_obj_items => 0,
342             fold_nesting => 0,
343              
344             join_array_items => 0,
345             join_obj_items => 0,
346             join_nesting => 0,
347             );
348              
349             our $DEFAULT = __PACKAGE__->_make(
350             width => DEFAULT_WIDTH,
351              
352             pack_array_items => 8,
353             pack_obj_items => 4,
354             pack_nesting => 1,
355              
356             fold_array_items => 8,
357             fold_obj_items => 4,
358             fold_nesting => 1,
359              
360             join_array_items => 8,
361             join_obj_items => 4,
362             join_nesting => 1,
363             );
364              
365             sub _replace {
366 74     74   153 my ($base, $validate) = (shift, shift) ;
367 74 50       194 return $base unless @_;
368 74 100 66     306 my $overrides = @_ == 1 && ref($_[0]) ? $_[0] : { @_ };
369 74 100       343 return $base unless %$overrides;
370              
371             # Make acopy of the original object.
372 36         183 my @d = @$base;
373 36         125 for my $key (keys %$overrides) {
374 171 100       301 unless ( exists $NAME_TO_INDEX{$key} ) {
375 1 50       2 die "unknown JSON::JSONFold config key: $key\n" if $validate ;
376 1         1 next ;
377             } ;
378 170         295 $d[$NAME_TO_INDEX{$key}] = $overrides->{$key};
379             }
380 36   50     261 return bless \@d, ref($base) || __PACKAGE__;
381             }
382              
383             # Resolve named config into actual config object.
384             sub _resolve_config {
385 39     39   62 my ($config) = @_;
386              
387 39 100       112 return $config if ref($config) ;
388              
389 19   100     45 my $name = $config // '';
390 19         55 $config = $PRESETS{$name} ;
391 19 50       38 die "unknown JSON::JSONFold preset: $name\n"
392             unless defined $config ;
393              
394 19         63 return $config ;
395             }
396              
397              
398             sub config {
399 39     39   69 my ($preset, %overrides) = @_;
400 39         81 return _replace(_resolve_config($preset), 0, \%overrides);
401             }
402              
403             sub new {
404 0     0   0 my ($class, $config, @args) = @_;
405 0         0 return config($config, @args);
406             }
407              
408             # Create preset - force validation.
409             sub _new_preset {
410 35     35   55 my $base = shift ;
411 35         82 return _replace($base, 1, @_) ;
412             }
413              
414             %PRESETS = (
415             off => __PACKAGE__->_make(off => 1),
416             '' => $DEFAULT,
417             default => $DEFAULT,
418             none => $NONE,
419             low => _new_preset($DEFAULT, fold_nesting => 0, join_nesting => 0),
420             med => _new_preset($DEFAULT, join_nesting => 0),
421             high => _new_preset($DEFAULT,
422             pack_array_items => 16, pack_obj_items => 8, pack_nesting => 4,
423             fold_array_items => 16, fold_obj_items => 8, fold_nesting => 4,
424             join_array_items => 16, join_obj_items => 8, join_nesting => 2,
425             ),
426             max => _new_preset($NONE,
427             width => 255,
428             pack_array_items => MAX_ARRAY_ITEMS, pack_obj_items => MAX_OBJ_ITEMS,
429             pack_nesting => MAX_NESTING,
430             fold_array_items => MAX_ARRAY_ITEMS, fold_obj_items => MAX_OBJ_ITEMS,
431             fold_nesting => MAX_NESTING,
432             join_array_items => MAX_ARRAY_ITEMS, join_obj_items => MAX_OBJ_ITEMS,
433             join_nesting => MAX_NESTING,
434             ),
435             pack => _new_preset($NONE,
436             pack_array_items => MAX_ARRAY_ITEMS, pack_obj_items => MAX_OBJ_ITEMS,
437             pack_nesting => MAX_NESTING,
438             ),
439             fold => _new_preset($NONE,
440             fold_array_items => MAX_ARRAY_ITEMS, fold_obj_items => MAX_OBJ_ITEMS,
441             fold_nesting => MAX_NESTING,
442             ),
443             join => _new_preset($NONE,
444             fold_array_items => MAX_ARRAY_ITEMS, fold_obj_items => MAX_OBJ_ITEMS,
445             fold_nesting => MAX_NESTING,
446             join_array_items => MAX_ARRAY_ITEMS, join_obj_items => MAX_OBJ_ITEMS,
447             join_nesting => MAX_NESTING,
448             ),
449             );
450              
451              
452             # -------------------------------------------------------------------------
453             # Internal package: one physical pretty-printed line
454             # -------------------------------------------------------------------------
455              
456             package JSON::JSONFold::Line;
457 5     5   44 use strict;
  5         8  
  5         143  
458 5     5   22 use warnings;
  5         11  
  5         310  
459 5     5   32 use Exporter 'import' ;
  5         8  
  5         263  
460              
461 5     5   26 use constant KIND_NONE => $JSON::JSONFold::Kind::KIND_NONE ;
  5         8  
  5         655  
462              
463             our $SEQ ;
464             use constant {
465 5         854 L_INDENT => $SEQ++,
466             L_TEXT => $SEQ++,
467             L_PARENT_KIND => $SEQ++,
468             L_ITEMS => $SEQ++,
469             L_LEAFS => $SEQ++,
470             L_CHILD_NESTING => $SEQ++,
471             L_OPENER => $SEQ++,
472             L_CLOSER => $SEQ++,
473             L_CAN_JOIN => $SEQ++,
474             L_CAN_PACK => $SEQ++,
475 5     5   30 } ;
  5         19  
476              
477             BEGIN {
478 5     5   3924 our @EXPORT = qw( L_INDENT
479             L_TEXT
480             L_PARENT_KIND
481             L_ITEMS
482             L_LEAFS
483             L_CHILD_NESTING
484             L_OPENER
485             L_CLOSER
486             L_CAN_JOIN
487             L_CAN_PACK
488             ) ;
489             }
490              
491             sub parse {
492 169     169   279 my ($class, $s, $parent_kind) = @_;
493 169   100     271 $parent_kind //= KIND_NONE;
494              
495 169         698 my ($spaces) = $s =~ /^(\s*)/;
496 169         286 my $body = substr($s, length($spaces));
497 169         329 $body =~ s/\s+\z//;
498              
499 169         219 my $last = substr($body, -1, 1);
500              
501 169   100     451 my $opener = $OPENING_KIND{$last} // KIND_NONE ;
502 169   100     351 my $closer = $CLOSING_KIND{$body} // KIND_NONE;
503              
504 169 100 100     529 my $is_body = $parent_kind && !$opener && !$closer ? 1 : 0;
505              
506 169         171 my @d ;
507 169         402 $#d = $SEQ ;
508 169         182 $d[L_INDENT] = length($spaces);
509 169         209 $d[L_TEXT] = $body;
510 169         162 $d[L_PARENT_KIND] = $parent_kind;
511 169         173 $d[L_ITEMS] = 1;
512 169         161 $d[L_LEAFS] = 1;
513 169         165 $d[L_CHILD_NESTING] = -1;
514 169         147 $d[L_OPENER] = $opener;
515 169         165 $d[L_CLOSER] = $closer;
516 169         167 $d[L_CAN_JOIN] = $is_body;
517 169         157 $d[L_CAN_PACK] = $is_body;
518              
519 169         407 return bless \@d, $class ;
520             }
521              
522             sub fold {
523 25     25   59 my ($class, $lines, $leafs, $child_nesting) = @_;
524 25         33 my $first_line = $lines->[0] ;
525 25         24 my @d ;
526 25         65 $#d = $SEQ ;
527 25         31 $d[L_INDENT] = $first_line->[L_INDENT] ;
528 25         44 $d[L_TEXT] = join(' ', map { $_->[L_TEXT] } @$lines) ;
  75         173  
529 25         72 $d[L_PARENT_KIND] = $first_line->[L_PARENT_KIND] ;
530 25         26 $d[L_ITEMS] = 1;
531 25         36 $d[L_LEAFS] = $leafs;
532 25         39 $d[L_CHILD_NESTING] = $child_nesting;
533 25         32 $d[L_OPENER] = KIND_NONE;
534 25         31 $d[L_CLOSER] = KIND_NONE;
535 25         27 $d[L_CAN_PACK] = 0;
536 25         97 $d[L_CAN_JOIN] = 1,
537              
538             return bless \@d, $class ;
539             }
540              
541 74     74   273 sub raw { return (' ' x $_[0][L_INDENT]) . $_[0][L_TEXT] . "\n" }
542 41     41   122 sub width { return $_[0][L_INDENT] + length($_[0][L_TEXT]) }
543              
544             sub join_line {
545 45     45   92 my ($self, $other) = @_;
546 45         105 $self->[L_TEXT] .= ' ' . $other->[L_TEXT];
547 45         49 $self->[L_ITEMS] += $other->[L_ITEMS];
548 45         50 $self->[L_LEAFS] += $other->[L_LEAFS];
549 45 50       69 if ($other->[L_CHILD_NESTING] > $self->[L_CHILD_NESTING]) {
550 0         0 $self->[L_CHILD_NESTING] = $other->[L_CHILD_NESTING];
551 0         0 $self->[L_CAN_PACK] = 0;
552             }
553 45         79 return $self;
554             }
555              
556             # -------------------------------------------------------------------------
557             # Internal package: stack frame for a currently open JSON container
558             # -------------------------------------------------------------------------
559              
560              
561              
562             package JSON::JSONFold::Frame;
563 5     5   51 use strict;
  5         9  
  5         179  
564 5     5   23 use warnings;
  5         8  
  5         271  
565 5     5   24 use Exporter 'import' ;
  5         12  
  5         192  
566              
567             BEGIN {
568 5     5   805 JSON::JSONFold::Line->import() ;
569             }
570              
571             our $SEQ ;
572             use constant {
573 5         880 F_UNUSED_FIRST => $SEQ++,
574             F_KIND => $SEQ++,
575             F_DEPTH => $SEQ++,
576             F_LINES => $SEQ++,
577             F_PACK_LIMIT => $SEQ++,
578             F_FOLD_LIMIT => $SEQ++,
579             F_JOIN_LIMIT => $SEQ++,
580             F_CONTENT_LINES => $SEQ++,
581             F_ITEMS => $SEQ++,
582             F_LEAFS => $SEQ++,
583             F_FOLD_OK => $SEQ++,
584             F_CHILD_NESTING => $SEQ++,
585             F_UNUSED_LAST => $SEQ++,
586 5     5   28 } ;
  5         22  
587              
588             BEGIN {
589 5     5   1825 our @EXPORT = qw(
590             F_UNUSED_FIRST
591             F_KIND
592             F_DEPTH
593             F_LINES
594             F_PACK_LIMIT
595             F_FOLD_LIMIT
596             F_JOIN_LIMIT
597             F_CONTENT_LINES
598             F_ITEMS
599             F_LEAFS
600             F_FOLD_OK
601             F_CHILD_NESTING
602             F_UNUSED_LAST
603             ) ;
604             }
605              
606             sub new {
607 47     47   169 my ($class, %arg) = @_;
608 47         81 my @d ;
609 47         85 $#d = $SEQ ;
610 47         64 $d[F_KIND] = $arg{kind};
611 47   50     91 $d[F_DEPTH] = $arg{depth} // 0;
612 47   50     103 $d[F_LINES] = $arg{lines} || [];
613 47   50     86 $d[F_PACK_LIMIT] = $arg{pack_limit} // 0;
614 47   50     69 $d[F_FOLD_LIMIT] = $arg{fold_limit} // 0;
615 47   50     88 $d[F_JOIN_LIMIT] = $arg{join_limit} // 0;
616 47         54 $d[F_CONTENT_LINES] = 0;
617 47         50 $d[F_ITEMS] = 0;
618 47         49 $d[F_LEAFS] = 0;
619 47         45 $d[F_FOLD_OK] = 1;
620 47         88 $d[F_CHILD_NESTING] = -1;
621 47         145 return bless \@d, $class;
622             }
623              
624             # Update Frame information based on added line
625             sub update_add {
626 104     104   149 my ($self, $line) = @_ ;
627 104         137 $self->[F_ITEMS] += $line->[L_ITEMS];
628 104         115 $self->[F_LEAFS] += $line->[L_LEAFS];
629 104 100       155 if ($line->[L_CHILD_NESTING] >= $self->[F_CHILD_NESTING]) {
630 51         82 $self->[F_CHILD_NESTING] = $line->[L_CHILD_NESTING] + 1;
631             }
632             }
633              
634 232     232   220 sub is_empty { return @{ $_[0][F_LINES] } == 0 }
  232         649  
635 101     101   133 sub last_line { return $_[0][F_LINES][-1] }
636              
637             # -------------------------------------------------------------------------
638             # Internal package: counters
639             # -------------------------------------------------------------------------
640              
641             package JSON::JSONFold::Stats;
642 5     5   48 use strict;
  5         10  
  5         137  
643 5     5   34 use warnings;
  5         9  
  5         1251  
644              
645             sub new {
646 19     19   33 my ($class) = @_;
647 19         85 return bless {
648             bytes_in => 0,
649             bytes_out => 0,
650             lines_in => 0,
651             lines_out => 0,
652             }, $class;
653             }
654              
655 0     0   0 sub bytes_in { $_[0]{bytes_in} }
656 0     0   0 sub bytes_out { $_[0]{bytes_out} }
657 0     0   0 sub lines_in { $_[0]{lines_in} }
658 0     0   0 sub lines_out { $_[0]{lines_out} }
659              
660 0     0   0 sub as_hash { return %{ $_[0] } }
  0         0  
661              
662             # -------------------------------------------------------------------------
663             # Internal package: streaming folding filter/writer
664             # -------------------------------------------------------------------------
665              
666             package JSON::JSONFold::Writer;
667 5     5   34 use strict;
  5         8  
  5         123  
668 5     5   20 use warnings;
  5         7  
  5         358  
669              
670             BEGIN {
671 5     5   232 JSON::JSONFold::Line->import() ;
672 5         238 JSON::JSONFold::Frame->import() ;
673 5         591 JSON::JSONFold::Config->import() ;
674             }
675              
676             our $SEQ ;
677             use constant {
678 5         730 W_UNUSED_FIRST => $SEQ++,
679             W_FH => $SEQ++,
680             W_CFG => $SEQ++,
681             W_PENDING => $SEQ++,
682             W_STACK => $SEQ++,
683             W_STATS => $SEQ++,
684             W_DO_CLOSE => $SEQ++,
685             W_UNUSED_LAST => $SEQ++,
686 5     5   45 } ;
  5         12  
687              
688             BEGIN {
689 5     5   14139 our @EXPORT = qw(
690             W_UNUSED_FIRST
691             W_FH
692             W_CFG
693             W_PENDING
694             W_STACK
695             W_STATS
696             W_DO_CLOSE
697             W_UNUSED_LAST
698             ) ;
699             }
700              
701             sub new {
702 19     19   45 my ($class, $fh, $config, $do_close) = @_;
703              
704 19         33 my $cfg = JSON::JSONFold::Config::config($config) ;
705 19         31 my @d ;
706 19         66 $#d = $SEQ ;
707 19         29 $d[W_FH] = $fh;
708 19 100       89 $d[W_CFG] = $cfg unless $cfg->[C_OFF] ;
709 19         32 $d[W_PENDING] = '';
710 19         26 $d[W_STACK] = [];
711 19         85 $d[W_STATS] = JSON::JSONFold::Stats->new;
712 19         28 $d[W_DO_CLOSE] = $do_close;
713 19         51 return bless \@d, $class;
714             }
715              
716 1     1   3 sub stats { return $_[0][W_STATS] }
717              
718             sub write {
719 19     19   43 my ($self, $s) = @_;
720 19 50       69 $s = '' unless defined $s;
721 19         64 my $len = length($s);
722 19         61 $self->[W_STATS]{bytes_in} += $len;
723              
724 19 100       55 unless ($self->[W_CFG]) {
725 1         4 $self->[W_STATS]{lines_in} += _count_newlines($s);
726 1         4 return $self->_write_str($s);
727             }
728              
729 18         38 my $nl = index($s, "\n");
730 18 50       46 if ($nl < 0) {
731 0         0 $self->[W_PENDING] .= $s;
732 0         0 return $len;
733             }
734              
735 18         33 my $nl2 = index($s, "\n", $nl + 1);
736 18 100       44 if ($nl2 < 0) {
737 2         3 $self->[W_STATS]{lines_in}++;
738 2         7 my $line_text = $self->[W_PENDING] . substr($s, 0, $nl);
739 2         7 $self->[W_PENDING] = substr($s, $nl + 1);
740 2         9 $self->_feed(JSON::JSONFold::Line->parse($line_text, $self->_parent_kind));
741 2         17 return $len;
742             }
743              
744             # We have multiple lines - at least 2 new lines in the new buffer
745 16         76 my @lines = split("\n", $s, -1) ;
746 16         32 $lines[0] = $self->[W_PENDING] . $lines[0] ;
747 16         29 $self->[W_PENDING] = pop @lines ;
748 16         35 for my $part ( @lines ) {
749 167         281 $self->_feed(JSON::JSONFold::Line->parse($part, $self->_parent_kind));
750              
751             }
752 16         32 $self->[W_STATS]{lines_in} += @lines;
753              
754 16         45 return $len;
755             }
756              
757             sub finish {
758 20     20   38 my ($self) = @_;
759 20 50       49 if (length $self->[W_PENDING]) {
760 0         0 $self->_feed(JSON::JSONFold::Line->parse($self->[W_PENDING], $self->_parent_kind));
761 0         0 $self->[W_PENDING] = '';
762             }
763              
764 20         24 for my $frame (@{ $self->[W_STACK] }) {
  20         39  
765 0         0 $self->_write_line($_) for @{ $frame->[F_LINES] };
  0         0  
766             }
767 20         31 @{ $self->[W_STACK] } = ();
  20         28  
768             }
769              
770             sub flush {
771 20     20   29 my ($self) = @_;
772 20         26 my $fh = $self->[W_FH];
773 20 50 33     224 $fh->flush if $fh && $fh->can('flush');
774             }
775              
776             sub close {
777 1     1   2 my ($self) = @_;
778 1         3 $self->finish;
779 1         2 $self->flush;
780 1 50       3 $self->[W_FH]->close if $self->[W_DO_CLOSE] ;
781             }
782              
783             sub _feed {
784 169     169   223 my ($self, $line) = @_;
785             # Opener
786 169 100       277 if ($line->[L_OPENER]) {
787 47         95 push @{ $self->[W_STACK] }, JSON::JSONFold::Frame->new(
788             kind => $line->[L_OPENER],
789 47         53 depth => scalar(@{ $self->[W_STACK] }),
  47         121  
790             lines => [ $line ],
791             pack_limit => $self->_pack_limit($line->[L_OPENER]),
792             fold_limit => $self->_fold_limit($line->[L_OPENER]),
793             join_limit => $self->_join_limit($line->[L_OPENER]),
794             );
795 47         100 return;
796             }
797              
798             # Closer
799 122 100       181 if ($line->[L_CLOSER]) {
800 47         107 $self->_close_frame($line, $line->[L_CLOSER]);
801 47         88 return;
802             }
803              
804             # Regular Line
805 75 100       78 if (@{ $self->[W_STACK] }) {
  75         121  
806 73         79 my $frame = $self->[W_STACK][-1];
807 73 100       131 $line->[L_CAN_PACK] = 0 if $line->[L_ITEMS] >= $frame->[F_PACK_LIMIT];
808 73 100       119 $line->[L_CAN_JOIN] = 0 if $line->[L_ITEMS] >= $frame->[F_JOIN_LIMIT];
809 73         118 $self->_add_to_frame($frame, $line);
810             } else {
811 2         8 $self->_write_line($line);
812             }
813 75         179 return ;
814             }
815              
816             sub _emit_lines {
817 79     79   105 my ($self, $lines, $depth) = @_;
818 79 50       137 return unless @$lines;
819 79 100       115 $depth = @{ $self->[W_STACK] } - 1 unless defined $depth;
  47         59  
820              
821 79 100       117 if ($depth < 0) {
822 35         69 $self->_write_line($_) for @$lines;
823             return
824 35         99 }
825              
826 44         45 my $frame = $self->[W_STACK][$depth];
827 44         103 $self->_add_to_frame($frame, $_) for @$lines;
828             return
829 44         135 }
830              
831              
832              
833             sub _add_to_frame {
834 131     131   202 my ($self, $frame, $line) = @_;
835              
836 131 100 100     175 if (!$frame->is_empty) {
    100          
837 103 100 100     235 return if $line->[L_CAN_PACK] && $self->_try_pack($frame, $line);
838 68 100 100     156 return if $line->[L_CAN_JOIN] && $self->_try_join($frame, $line);
839              
840             # If frame is empty, may be it's in "streaming" mode, which
841             # mean that lines that can not be packed/joined can be sent
842             # directly to the output:
843             } elsif (!$frame->[F_FOLD_OK] && !$line->[L_CAN_PACK] && !$line->[L_CAN_JOIN]) {
844 18         29 $self->_write_line($line);
845 18         28 return;
846             }
847              
848 68         78 push @{ $frame->[F_LINES] }, $line;
  68         112  
849              
850 68 50 66     220 if ( $frame->[F_FOLD_OK] && $line->width > $self->[W_CFG][C_WIDTH] ) {
851 0         0 $self->_mark_no_fold ;
852             }
853              
854 68 100       104 unless ($line->[L_CLOSER]) {
855 59         69 $frame->[F_CONTENT_LINES]++;
856 59         125 $frame->update_add($line) ;
857 59 100       83 if ( $frame->[F_FOLD_OK] ) {
858 41 100       85 $self->_mark_no_fold unless $self->_check_fold_limits($frame)
859             }
860             }
861              
862 68 100       183 $self->_stream_frame($frame) unless $frame->[F_FOLD_OK];
863             return
864 68         95 }
865              
866             sub _can_merge {
867 45     45   74 my ($self, $prev, $line, $limit) = @_;
868 45   33     277 return $prev->[L_INDENT] == $line->[L_INDENT]
869             && $prev->[L_ITEMS] + $line->[L_ITEMS] <= $limit
870             && $prev->[L_INDENT] + length($prev->[L_TEXT]) + 1 + length($line->[L_TEXT]) <= $self->[W_CFG][C_WIDTH];
871             }
872              
873             sub _merge_into_frame {
874 45     45   61 my ($self, $frame, $prev, $line) = @_;
875 45         82 $prev->join_line($line);
876 45         95 $frame->update_add($line) ;
877              
878 45 100       93 $prev->[L_CAN_PACK] = 0 if $prev->[L_ITEMS] >= $frame->[F_PACK_LIMIT];
879 45 100       70 $prev->[L_CAN_JOIN] = 0 if $prev->[L_ITEMS] >= $frame->[F_JOIN_LIMIT];
880 45 100       82 if ( $frame->[F_FOLD_OK] ) {
881 35 50       56 unless ( $self->_check_fold_limits($frame)) {
882 0         0 $self->_mark_no_fold ;
883 0         0 $self->_stream_frame($frame) ;
884             }
885             }
886             }
887              
888             sub _try_pack {
889 58     58   92 my ($self, $frame, $line) = @_;
890              
891 58 50 33     194 return 0 if $frame->[F_PACK_LIMIT] <= 1 || !$line->[L_CAN_PACK] ||
      33        
892             $frame->is_empty;
893              
894 58         101 my $prev = $frame->last_line;
895 58 50 66     219 return 0 unless $prev->[L_CAN_PACK]
      66        
896             && $prev->[L_CHILD_NESTING] < $self->[W_CFG][C_PACK_NESTING]
897             && $self->_can_merge($prev, $line, $frame->[F_PACK_LIMIT]);
898 35         70 $self->_merge_into_frame($frame, $prev, $line);
899             # Disable join, or pack limits reached
900 35 100       60 $prev->[L_CAN_JOIN] = 0 unless $prev->[L_CAN_PACK] ;
901 35         86 return 1;
902             }
903              
904             sub _try_join {
905 45     45   67 my ($self, $frame, $line) = @_;
906              
907 45 100 33     268 return 0 if $frame->[F_JOIN_LIMIT] <= 1 ||
      66        
      66        
908             !$line->[L_CAN_JOIN] || $line->[L_CHILD_NESTING] >= $self->[W_CFG][C_JOIN_NESTING] ||
909             $frame->is_empty;
910              
911 43         65 my $prev = $frame->last_line;
912 43 50 66     192 return 0 unless $prev->[L_CAN_JOIN]
      66        
913             && $prev->[L_CHILD_NESTING] < $self->[W_CFG][C_JOIN_NESTING]
914             && $self->_can_merge($prev, $line, $frame->[F_JOIN_LIMIT]);
915 10         27 $self->_merge_into_frame($frame, $prev, $line);
916 10         28 return 1;
917             }
918              
919             sub _check_fold_limits {
920 76     76   129 my ($self, $frame) = @_;
921              
922 76 100       137 return if $frame->[F_CONTENT_LINES] > 1 ;
923 75 100       124 return if $frame->[F_ITEMS] > $frame->[F_FOLD_LIMIT] ;
924 71 100       140 return if $frame->[F_CHILD_NESTING] >= $self->[W_CFG][C_FOLD_NESTING] ;
925 61         115 return 1 ;
926             }
927              
928             sub _close_frame {
929 47     47   67 my ($self, $closer, $closing_kind) = @_;
930 47 50       47 unless (@{ $self->[W_STACK] }) {
  47         81  
931 0         0 $self->_write_line($closer);
932 0         0 return;
933             }
934              
935             # Frame is removed/destroyed
936 47         50 my $frame = pop @{ $self->[W_STACK] };
  47         66  
937 47         57 my $lines = $frame->[F_LINES] ;
938 47         59 push @$lines, $closer ;
939             # MAYBE: $frame->[F_FOLD_OK] = 0 if $frame->[F_KIND] != $closing_kind;
940              
941 47 100       80 if ( my $folded = $self->_try_fold($frame)) {
942 25         52 $lines = [ $folded ] ;
943             }
944 47         100 $self->_emit_lines($lines);
945             }
946              
947             sub _try_fold {
948 47     47   61 my ($self, $frame) = @_;
949 47 50 66     139 return undef if !$frame->[F_FOLD_OK] || $frame->[F_CONTENT_LINES] != 1 || @{ $frame->[F_LINES] } != 3;
  25   66     84  
950              
951 25         29 my $folded_len = -1;
952 25         27 $folded_len += 1 + length($_->[L_TEXT]) for @{ $frame->[F_LINES] };
  25         68  
953 25 50       54 return undef if $frame->[F_LINES][0][L_INDENT] + $folded_len > $self->[W_CFG][C_WIDTH];
954              
955 25         64 return JSON::JSONFold::Line->fold(
956             $frame->[F_LINES],
957             $frame->[F_LEAFS],
958             $frame->[F_CHILD_NESTING],
959             );
960             }
961              
962             sub _stream_frame {
963 42     42   66 my ($self, $frame) = @_;
964 42         43 my $lines = $frame->[F_LINES];
965 42 50       61 return unless @$lines ;
966              
967 42         47 my $last = $lines->[-1] ;
968 42   100     90 my $keep_last = $last->[L_CAN_PACK] || $last->[L_CAN_JOIN] ;
969 42 100       71 pop @$lines if $keep_last ;
970 42 100       64 if ( @$lines ) {
971 32         89 $self->_emit_lines($lines, $frame->[F_DEPTH] - 1) ;
972 32         81 @$lines = ();
973             }
974 42 100       74 push @$lines, $last if $keep_last ;
975             return
976 42         65 }
977              
978             sub _mark_no_fold {
979 15     15   23 my ($self) = @_;
980 15         19 $_->[F_FOLD_OK] = 0 for @{ $self->[W_STACK] };
  15         44  
981             }
982              
983             sub _write_line {
984 74     74   97 my ($self, $line) = @_ ;
985 74         133 return $self->_write_str($line->raw) ;
986             }
987              
988             sub _write_str {
989 75     75   108 my ($self, $s) = @_;
990              
991 75         541 $self->[W_FH]->print($s) ;
992 75         35233 $self->[W_STATS]{bytes_out} += length($s);
993 75         128 $self->[W_STATS]{lines_out} += _count_newlines($s);
994 75         160 return length($s);
995             }
996              
997             sub _parent_kind {
998 169     169   218 my ($self) = @_ ;
999 169         192 my $stack = $self->[W_STACK] ;
1000 169 100       320 return unless @$stack ;
1001 151 50       339 return @$stack ? $stack->[-1][F_KIND] : JSON::JSONFold::Kind::KIND_NONE ;
1002             }
1003              
1004             sub _choose_limit {
1005 141     141   171 my ($kind, $list, $dict) = @_;
1006 141 50       450 return $kind == JSON::JSONFold::Kind::KIND_LIST() ? $list
    100          
1007             : $kind == JSON::JSONFold::Kind::KIND_DICT() ? $dict
1008             : 0;
1009             }
1010 47     47   98 sub _pack_limit { _choose_limit($_[1], $_[0][W_CFG][C_PACK_ARRAY_ITEMS], $_[0][W_CFG][C_PACK_OBJ_ITEMS]) }
1011 47     47   91 sub _fold_limit { _choose_limit($_[1], $_[0][W_CFG][C_FOLD_ARRAY_ITEMS], $_[0][W_CFG][C_FOLD_OBJ_ITEMS]) }
1012 47     47   89 sub _join_limit { _choose_limit($_[1], $_[0][W_CFG][C_JOIN_ARRAY_ITEMS], $_[0][W_CFG][C_JOIN_OBJ_ITEMS]) }
1013 76     76   124 sub _count_newlines { return ($_[0] =~ tr/\n//) }
1014              
1015             package JSON::JSONFold::CLI ;
1016              
1017 5     5   45 use Exporter 'import';
  5         12  
  5         267  
1018              
1019             our @EXPORT_OK = qw(
1020             demo_data
1021             run
1022             ) ;
1023              
1024 5     5   23 use strict;
  5         7  
  5         155  
1025 5     5   21 use warnings;
  5         8  
  5         5778  
1026              
1027             sub setup {
1028 0     0     require Carp ;
1029              
1030             $SIG{__DIE__} = sub {
1031 0 0   0     return if $^S;
1032 0           local $SIG{__DIE__};
1033 0           Carp::confess(@_);
1034 0           };
1035              
1036             $SIG{__WARN__} = sub {
1037 0     0     local $SIG{__WARN__};
1038 0           Carp::cluck(@_);
1039 0           };
1040              
1041 0           require Getopt::Long ;
1042              
1043             }
1044              
1045             sub demo_data {
1046             return {
1047             meta => { version => 1, ok => JSON::PP::true, name => "jsonfold demo" },
1048             ids => [ 1, 2, 3, 4, 5, 6 ],
1049             items => [ { id => 1, name => "alpha" }, { id => 2, name => "beta" }, ],
1050             matrix => [ [ 1, 2 ], [ 3, 4 ], [ 5, 6 ] ],
1051             long => [
1052             "this is a long message that may force the block to stay expanded",
1053             "second",
1054             "third",
1055             "fourth",
1056             ],
1057             "single-array" => [1],
1058             "single-obj" => { x => 2 },
1059 0           long_array => [ map { "a$_" } 1..50 ],
1060 0           wide_array => [ map { "abcdefghijklmnopqrstuvwxyz$_" } 1 .. 9 ],
1061 0     0     wide_object => { map { ; "abcdefghijk$_" => "lmnopqrstuvwxyz$_" } 1 .. 9 },
  0            
1062              
1063             };
1064             }
1065              
1066             sub parse_options {
1067 0     0     my %opt = (
1068             compact => 'default',
1069             indent => 2,
1070             sort_keys => 1,
1071             );
1072              
1073             Getopt::Long::GetOptions(
1074             'demo' => \$opt{demo},
1075             'verbose|v' => \$opt{verbose},
1076             'help|h' => \$opt{help},
1077             'input|i=s' => \$opt{input},
1078             'compact=s' => \$opt{compact},
1079             'indent=i' => \$opt{indent},
1080             'sort-keys!' => \$opt{sort_keys},
1081              
1082             'width=i' => \$opt{width},
1083 0 0         ) or die "Try --help\n";
1084              
1085 0           return \%opt;
1086             }
1087              
1088             sub usage {
1089 0     0     my $out = shift ;
1090 0           $out->print(<<___
1091             Usage: json-jsonfold [options] < input.json
1092              
1093             --demo
1094             --compact=default|none|low|med|high|max|pack|fold|join|off
1095             --width=N
1096             --indent=N
1097             --sort-keys
1098             --input=FILE
1099             ___
1100             ) ;
1101             }
1102              
1103             sub read_input {
1104 0     0     my ($input) = @_ ;
1105              
1106 0           my $json_text;
1107 0 0         if (defined $input) {
1108 0 0         open my $fh, '<', $input or die "$input: $!\n";
1109 0           local $/;
1110 0           $json_text = <$fh>;
1111 0 0         close $fh or die "$input: $!\n";
1112             } else {
1113 0           local $/;
1114 0           $json_text = ;
1115             }
1116              
1117 0           return JSON::PP->new->allow_nonref->decode($json_text);
1118             }
1119              
1120             sub show_verbose {
1121 0     0     require Data::Dumper ;
1122              
1123 0           my ($label) = shift ;
1124 0           my $dumper = new Data::Dumper([])->Terse(1)->Indent(1)->Sortkeys(1)->Pair('=')->Quotekeys(0) ;
1125              
1126 0           my $s = $dumper->Values( \@_)->Dump ;
1127 0           $s =~ s/\s+/ /gsm ;
1128              
1129 0           print STDERR "$label: $s\n" ;
1130              
1131             }
1132              
1133             sub run {
1134 0     0     setup() ;
1135 0           my $opt = parse_options();
1136              
1137 0 0         if ($opt->{help}) {
1138 0           usage(\*STDOUT);
1139 0           return 0;
1140             }
1141              
1142 0 0         my $data = $opt->{demo} ? demo_data() : read_input($opt->{input});
1143 0           my %cfg ;
1144 0           my $config = JSON::JSONFold::config($opt->{compact}, $opt->{width}, %cfg);
1145 0           my $verbose = $opt->{verbose} ;
1146              
1147 0 0         show_verbose("config", { $config->as_hash } ) if $verbose ;
1148            
1149 0           my $info = JSON::JSONFold::write_json($data, \*STDOUT, $opt->{width}, $config, sort_keys => $opt->{sort_keys});
1150              
1151 0 0         show_verbose("stats", { % $info }) if $verbose ;
1152 0           return 0;
1153             }
1154              
1155             run() unless caller() ;
1156              
1157             1;
1158              
1159             __END__