File Coverage

blib/lib/Text/JSON/Nibble.pm
Criterion Covered Total %
statement 11 77 14.2
branch 0 30 0.0
condition 0 9 0.0
subroutine 4 10 40.0
pod 6 6 100.0
total 21 132 15.9


line stmt bran cond sub pod time code
1             package Text::JSON::Nibble;
2              
3             =encoding utf8
4              
5             =cut
6              
7 1     1   13271 use 5.006;
  1         4  
8 1     1   6 use strict;
  1         2  
  1         20  
9 1     1   4 use warnings;
  1         5  
  1         22  
10              
11 1     1   469 use Data::Dumper;
  1         6691  
  1         493  
12              
13             =head1 NAME
14              
15             Text::JSON::Nibble - Nibble complete JSON objects from buffers
16              
17             =head1 VERSION
18              
19             Version 1.01
20              
21             =cut
22              
23             our $VERSION = '1.01';
24              
25             =head1 WARNING
26              
27             This module should be used with caution, it will not handle 'badly formed' json well, its entire purpose was because I was experiencing
28             segfaults with Cpanel::XS's decode_prefix when dealing with a streaming socket buffer.
29              
30             =head1 DESCRIPTION
31              
32             This module is a 'character' crawling JSON extractor for plain TEXT, usable in both a 'streaming' or 'block' method, for when you need something that is not XS.
33              
34             It is particularly handy for when you want to deal with JSON without decoding it.
35              
36             =head1 SYNOPSIS
37              
38             use Text::JSON::Nibble;
39              
40             my $json = '{"lol":{"a":[1,2,3],"b":"lol"}}';
41             my $item = Text::JSON::Nibble->new();
42              
43             my @results = @{ $item->digest($json) };
44              
45             =head1 EXAMPLES
46              
47             =head2 Example1 (Basic usage)
48              
49             use Text::JSON::Nibble;
50              
51             my $json = '{"lol":{"a":[1,2,3],"b":"lol"}}{"lol":{"a":[1,2,3],"b":"lol"}}';
52             my $item = Text::JSON::Nibble->new();
53              
54             foreach my $jsonBlock ( @{ $item->digest($json) } ) {
55             print "Found: $jsonBlock\n";
56             }
57              
58             # Will display the following:
59             # Found: {"lol":{"a":[1,2,3],"b":"lol"}}
60             # Found: {"lol":{"a":[1,2,3],"b":"lol"}}
61            
62              
63             =head2 Example2 (Basic usage - mangled JSON)
64              
65             use Text::JSON::Nibble;
66              
67             my $json = '\cxa4GL
68             my $item = Text::JSON::Nibble->new();
69              
70             foreach my $jsonBlock ( @{ $item->digest($json) } ) {
71             print "Found: $jsonBlock\n";
72             }
73              
74             # Will display the following:
75             # Found: {"lol":{"a":[1,2,3],"b":"lol"}}
76             # Found: {"lol":{"a":[1,2,3],"b":"lol"}}
77              
78             =head2 Example3 (Streaming usage for POE and others)
79              
80             use Text::JSON::Nibble;
81            
82             my @jsonStream = qw( {"test":1} {"moreTest":2} {"part ial":3} );
83             my $item = Text::JSON::Nibble->new();
84            
85             $item->process( shift @jsonStream );
86              
87             while( $item->stack ) {
88             my $jsonBlock = $item->pull;
89             print "Found $jsonBlock\n";
90              
91             while ( my $newJSON = shift @jsonStream ) {
92             $item->process($newJSON);
93             }
94             }
95              
96             =head1 Generic callers
97              
98             =head2 new
99              
100             Generate a new JSON Nibble object
101              
102             =cut
103              
104             sub new {
105 0     0 1   my $class = shift;
106              
107             # Some private stuff for ourself
108 0           my $self = {
109             jsonqueue => [],
110             buffer => "",
111             iChar => [],
112             };
113            
114             # We are interested in characters of this code
115 0           $self->{iChar}->[91] = 1;
116 0           $self->{iChar}->[93] = 1;
117 0           $self->{iChar}->[123] = 1;
118 0           $self->{iChar}->[125] = 1;
119              
120             # Go with god my son
121 0           bless $self, $class;
122 0           return $self;
123             }
124              
125             =head1 Block functions
126              
127             =head2 digest
128              
129             Digest the text that is fed in and attempt to return a complete an array of JSON object from it, returns either a blank array or an array of text-encoded-json.
130              
131             Note you can call and use this at any time, even if you are using streaming functionality.
132              
133             =cut
134              
135             sub digest {
136 0     0 1   my $self = shift;
137 0           my $data = shift;
138              
139             # A place for our return
140 0           my $return = [];
141            
142             # If we got passed a blank data scalar just return failure
143 0 0         return $return if (!$data);
144              
145             # Save the current state for if we are dealing with a stream elsewhere.
146 0 0         my $stateBackup = $self->{state} if ($self->{state});
147            
148             # Start with a fresh state
149 0           $self->reset;
150            
151             # Load the digest data into the processor
152 0           $self->process($data);
153            
154             # Generate our results
155 0           while ($self->stack) { push @{$return},$self->pull }
  0            
  0            
156              
157             # Restore the previous state
158 0 0         $self->{state} = $stateBackup if ($stateBackup);
159              
160             # Process the data and return the result
161 0           return $return;
162             }
163              
164             =head1 Streaming functions
165              
166             =head2 process
167              
168             Load data into the buffer for json extraction, can be called at any point.
169              
170             This function will return the buffer length remaining after extraction has been attempted.
171              
172             This function takes 1 optional argument, text to be added to the buffer.
173              
174             =cut
175              
176             sub process {
177 0     0 1   my $self = shift;
178 0           my $data = shift;
179              
180             # Add any data present to the buffer, elsewhere return the length of what we have.
181 0 0         if ($data) { $self->{buffer} .= $data }
  0            
182 0           else { return length($self->{buffer}) }
183            
184             # If we have no buffer return 0.
185 0 0         if (!$self->{buffer}) { return 0 }
  0            
186            
187             # Load our state or establish a new one
188 0           my $state;
189 0 0         if ( $self->{state} ) {
190 0           $state = $self->{state};
191             } else {
192 0           $state = {
193             'typeAOpen' => 0,
194             'typeBOpen' => 0,
195             'arrayPlace' => 0,
196             'prevChar' => 32
197             };
198             }
199            
200             # Extract the new information into an array split by char
201 0           my @jsonText = split(//,$data);
202            
203             # Where to shorten the buffer to if we make extractions
204 0           my $breakPoint;
205              
206             # Loop over the text looking for json objects
207 0           foreach my $chr ( @jsonText ) {
208             # Find the code for the current character
209 0           my $charCode = ord($chr);
210              
211             # Check if this character is an escape \, if not check if its [ { } or ]
212 0 0 0       if ( $state->{prevChar} != 92 && $self->{iChar}->[$charCode] ) {
213             # Handle { } type brackets
214 0 0         if ( $charCode == 123 ) { $state->{typeAOpen}++ }
  0 0          
    0          
    0          
215 0           elsif ( $charCode == 125 ) { $state->{typeAOpen}-- }
216              
217             # Handle [ ] type brackets
218 0           elsif ( $charCode == 91 ) { $state->{typeBOpen}++ }
219 0           elsif ( $charCode == 93 ) { $state->{typeBOpen}-- }
220            
221             # Mark we have found something to start with
222 0 0         if (!defined $state->{arrayStart}) { $state->{arrayStart} = $state->{arrayPlace} }
  0            
223             }
224              
225             # If we have a complete object then leave
226 0 0 0       if ( defined $state->{arrayStart} && !$state->{typeAOpen} && !$state->{typeBOpen} ) {
      0        
227             # Ok we had a JSON object fully open and closed.
228             # push it into a return
229 0           push @{$self->{jsonqueue}},substr($self->{buffer},$state->{arrayStart},$state->{arrayPlace}+1-$state->{arrayStart});
  0            
230 0           delete $state->{arrayStart};
231 0           $breakPoint = $state->{arrayPlace};
232             }
233            
234             # Increment our arrayplace
235 0           $state->{arrayPlace}++;
236            
237             # Remember the last char
238 0           $state->{prevChar} = $charCode;
239             }
240            
241             # Clean up the arrayPlace and save state
242 0 0         if ($breakPoint) {
243 0           $self->{buffer} = substr($self->{buffer},$breakPoint+1);
244 0           $state->{arrayPlace} -= $breakPoint+1;
245             }
246            
247             # Save our state
248 0           $self->{state} = $state;
249            
250             # Return the remaining buffer size
251 0           return length($self->{buffer});
252             }
253              
254             =head2 stack
255              
256             Return the amount of succesfully extracted JSON blocks ready to be pulled.
257              
258             If no JSON blocks are ready, returns 0.
259              
260             This function takes no arguments.
261              
262             =cut
263              
264             sub stack {
265 0     0 1   my $self = shift;
266 0           return scalar( @{ $self->{jsonqueue} } );
  0            
267             }
268              
269             =head2 pull
270              
271             Pull an item from the stack, shortening the stack by 1.
272              
273             This function will return "" if the stack is empty.
274              
275             This function takes no arguments.
276              
277             =cut
278              
279             sub pull {
280 0     0 1   my $self = shift;
281            
282 0 0         if ( $self->stack == 0 ) { return "" }
  0            
283 0           return shift @{ $self->{jsonqueue} };
  0            
284             }
285              
286             =head2 reset
287              
288             Effectively flushs the objects buffers, giving you a clean object, this can be handy when you want to start processing from another stream.
289              
290             This function returns nothing.
291              
292             This function takes no arguments.
293              
294             =cut
295              
296             sub reset {
297 0     0 1   my $self = shift;
298            
299 0           $self->{jsonqueue} = [];
300 0           $self->{buffer} => "";
301             }
302              
303             =head1 AUTHOR
304              
305             Paul G Webster, C<< >>
306              
307             =head1 BUGS
308              
309             Please report any bugs or feature requests to C, or through
310             the web interface at L. I will be notified, and then you'll
311             automatically be notified of progress on your bug as I make changes.
312              
313              
314              
315              
316             =head1 SUPPORT
317              
318             You can find documentation for this module with the perldoc command.
319              
320             perldoc Text::JSON::Nibble
321              
322              
323             You can also look for information at:
324              
325             =over 4
326              
327             =item * The author publishs this module to GitLab (Please report bugs here)
328              
329             L
330              
331             =item * RT: CPAN's request tracker
332              
333             L
334              
335             =item * AnnoCPAN: Annotated CPAN documentation
336              
337             L
338              
339             =item * CPAN Ratings
340              
341             L
342              
343             =item * Search CPAN
344              
345             L
346              
347             =back
348              
349              
350             =head1 ACKNOWLEDGEMENTS
351              
352              
353             =head1 LICENSE AND COPYRIGHT
354              
355             Copyright 2017 Paul G Webster.
356              
357             This program is released under the following license: BSD
358              
359              
360             =cut
361              
362             1; # End of Text::JSON::Nibble