File Coverage

blib/lib/JSON/String.pm
Criterion Covered Total %
statement 62 63 98.4
branch 19 22 86.3
condition 4 6 66.6
subroutine 12 12 100.0
pod 1 2 50.0
total 98 105 93.3


line stmt bran cond sub pod time code
1 5     5   117587 use strict;
  5         11  
  5         175  
2 5     5   20 use warnings;
  5         8  
  5         178  
3              
4             package JSON::String;
5              
6 5     5   20 use Carp qw(croak);
  5         9  
  5         346  
7             our @CARP_NOT = qw(JSON::String::BaseHandler JSON::String::HASH JSON::String::ARRAY);
8 5     5   3162 use JSON;
  5         51333  
  5         20  
9              
10 5     5   2301 use JSON::String::ARRAY;
  5         14  
  5         154  
11 5     5   2125 use JSON::String::HASH;
  5         10  
  5         2526  
12              
13             our $VERSION = '0.2.0'; # VERSION
14              
15             sub tie {
16 25     25 0 28879 my($class, $string) = @_;
17 25         38 my $ref = \$_[1];
18              
19 25         63 my $data = _validate_string_ref($ref);
20 21         47 return _construct_object($data, $ref);
21             }
22              
23             sub _construct_object {
24 175     175   169 my($data, $str_ref, $encoder) = @_;
25              
26 175 50 66     361 croak('Either string ref or encoder sub expected, not both') if ($str_ref and $encoder);
27              
28 175 100       430 return $data unless ref $data;
29              
30 54 100       99 $encoder = _create_encoder($data, $str_ref) unless $encoder;
31              
32 54         53 my $self;
33 54 100       125 if (ref($data) eq 'ARRAY') {
    50          
34 22         39 foreach my $elt ( @$data ) {
35 53         79 $elt = _construct_object($elt, undef, $encoder);
36             }
37 22         31 $self = [];
38 22         99 CORE::tie @$self, 'JSON::String::ARRAY', data => $data, encoder => $encoder;
39             } elsif (ref($data) eq 'HASH') {
40 32         81 foreach my $key ( keys %$data ) {
41 50         85 $data->{$key} = _construct_object($data->{$key}, undef, $encoder);
42             }
43 32         47 $self = {};
44 32         134 CORE::tie %$self, 'JSON::String::HASH', data => $data, encoder => $encoder;
45             }
46              
47 54         122 return $self;
48             }
49              
50             {
51             my $codec = JSON->new->canonical;
52             sub codec {
53 44     44 1 95 shift;
54 44 100       82 if (@_) {
55 1         2 $codec = shift;
56             }
57 44         193 return $codec;
58             }
59             }
60              
61             sub _create_encoder {
62 21     21   23 my($data, $str_ref) = @_;
63              
64 21         28 my $codec = codec;
65             return sub {
66 59     59   56 my $val;
67 59         42 my $error = do {
68 59         48 local $@;
69 59         94 $val = eval { $$str_ref = $codec->encode($data) };
  59         302  
70 59         550 $@;
71             };
72 59 100       136 croak("Error encoding data structure: $error") if $error;
73 58         98 return $val;
74 21         88 };
75             }
76              
77             sub _validate_string_ref {
78 25     25   32 my $ref = shift;
79              
80 25 100       88 unless (ref $ref eq 'SCALAR') {
81 1         11 croak q(Expected plain string, but got reference);
82             }
83 24 100       58 unless (defined $$ref) {
84 1         24 croak('Expected string, but got ');
85             }
86 23 100       92 unless (length $$ref) {
87 1         11 croak('Expected non-empty string');
88             }
89              
90 22         43 my $data = codec()->decode($$ref);
91              
92 21 50 66     336 unless (ref($data) eq 'ARRAY' or ref($data) eq 'HASH') {
93 0         0 croak('Cannot handle '.ref($data).' reference');
94             }
95 21         28 return $data;
96             }
97              
98             1;
99              
100             =pod
101              
102             =head1 NAME
103              
104             JSON::String - Automatically change a JSON string when a data structure changes
105              
106             =head1 SYNOPSIS
107              
108             # Basic use
109             my $json_string = q({ a: 1, b: 2, c: [ 4, 5, 6 ] });
110             my $data = JSON::String->tie($json_string);
111             @{$data->{c}} = qw(this data changed);
112             # $json_string now contains '{ a: 1, b: 2, c: ["this", "data", "changed"] }'
113              
114             # Useful when the JSON gets saved somewhere more permanent
115             my $object = load_object_from_database();
116             my $decoded_struct = JSON::String->tie($object->{json_attribute});
117             possibly_change_data($decoded_struct); # json_attribute will change if the struct changes
118             save_object_to_database($object) if ($object->has_changes);
119              
120             =head1 DESCRIPTION
121              
122             This module constructs a data structure from a JSON string that, when changed,
123             automatically changes the original string's contents to match the new data.
124             Hashrefs and arrayrefs are supported, and their values can be scalars,
125             hashrefs or arrayrefs. This is useful in cases where the JSON string is
126             persisted in a database, and needs to be updated if the underlying data
127             ever changes.
128              
129             The JSON format does not handle recursive data, and an exception will be
130             thrown if the data structure is changed such that it has a loop.
131              
132             =head1 CONSTRUCTOR
133              
134             my $data = JSON::String->tie($json_string);
135              
136             Returns either a hashref or arrayref, depending on the input JSON string.
137             The string passed in must by valid JSON encoding either an arrayref or
138             hashref, otherwise it will throw an exception.
139              
140             The returned data structure is tied to the string such that when the data
141             changes, the JSON string stored in the variable will be changed to reflect
142             the new data. If the string changes, the data structure will _not_ change.
143              
144             =head2 Methods
145              
146             =over 4
147              
148             =item JSON::String->codec(); # returns a JSON instance
149              
150             =item JSON::String->codec($obj);
151              
152             Get or change the JSON codec object. The initial codec is created with
153             JSON->new->canonical()
154              
155             Any object can be used as the codec as long as it has C and
156             C methods. A data structure's codec does not change after it
157             is created. If the class's codec changes after creation, the data structure
158             will continue to use whatever codec was active when it was created.
159              
160             =back
161              
162             =head2 Mechanism
163              
164             This module uses Perl's C mechanism to perform its magic. The hash-
165             and arrayrefs that make up the returned data structure are references to tied
166             hashes and arrays. When their data changes, the top-level data structure is
167             re-encoded and stored back in the original variable.
168              
169             =head2 Diagnostics
170              
171             Error conditions are signalled with exceptions.
172              
173             =over 4
174              
175             =item Error encoding data structure: %s
176              
177             The codec's encode() method threw an exception when encoding the data structure.
178              
179             =item Cannot handle %s reference
180              
181             JSON::String->tie() was passed a string that did not decode to either a
182             hashref or arrayref.
183              
184             =item Expected plain string, but got reference
185              
186             JSON::String->tie() was passed a reference to something instead of a string.
187              
188             =item Expected string, but got
189              
190             JSON::String->tie() was passed undef instead of a string.
191              
192             =item Expected non-empty string
193              
194             JSON::String->tie() was passed an empty string.
195              
196             =back
197              
198             =head1 SEE ALSO
199              
200             L, L, L
201              
202             =head1 AUTHOR
203              
204             Anthony Brummett
205              
206             =head1 COPYRIGHT
207              
208             Copyright 2015, Anthony Brummett. This module is free software. It may
209             be used, redistributed and/or modified under the same terms as Perl itself.
210              
211             =cut