File Coverage

blib/lib/JSON/Transform.pm
Criterion Covered Total %
statement 175 182 96.1
branch 79 100 79.0
condition 20 30 66.6
subroutine 15 15 100.0
pod 1 1 100.0
total 290 328 88.4


line stmt bran cond sub pod time code
1             package JSON::Transform;
2              
3 1     1   89757 use strict;
  1         12  
  1         28  
4 1     1   6 use warnings;
  1         1  
  1         26  
5 1     1   5 use Exporter 'import';
  1         2  
  1         26  
6 1     1   656 use Storable qw(dclone);
  1         3158  
  1         61  
7 1     1   504 use JSON::Transform::Grammar;
  1         3  
  1         7  
8 1     1   465 use XML::Invisible qw(make_parser);
  1         13780  
  1         74  
9              
10 1     1   10 use constant DEBUG => $ENV{JSON_TRANSFORM_DEBUG};
  1         2  
  1         2157  
11              
12             our $VERSION = '0.03';
13             our @EXPORT_OK = qw(
14             parse_transform
15             );
16              
17             my %QUOTED2LITERAL = (
18             b => "\b",
19             f => "\f",
20             n => "\n",
21             r => "\r",
22             t => "\t",
23             '\\' => "\\",
24             '$' => "\$",
25             '`' => "`",
26             '"' => '"',
27             '/' => "/",
28             );
29             my %IS_BACKSLASH_ENTITY = map {$_=>1} qw(
30             jsonBackslashDouble
31             jsonBackslashDollar
32             jsonBackslashQuote
33             jsonBackslashGrave
34             );
35              
36             my $parser = make_parser(JSON::Transform::Grammar->new);
37             sub parse_transform {
38 21     21 1 15532 my ($input_text) = @_;
39 21         64 my $transforms = $parser->($input_text);
40             sub {
41 21     21   45 my ($data) = @_;
42 21         663 $data = dclone $data; # now can mutate away
43 21         59 my $uservals = {};
44 21         36 for (@{$transforms->{children}}) {
  21         59  
45 27         52 my $name = $_->{nodename};
46 27         42 my ($srcptr, $destptr, $mapping);
47 27 100       80 if ($name eq 'transformImpliedDest') {
    100          
    50          
48 8         10 ($srcptr, $mapping) = @{$_->{children}};
  8         15  
49 8         15 $destptr = $srcptr;
50             } elsif ($name eq 'transformCopy') {
51 17         22 ($destptr, $srcptr, $mapping) = @{$_->{children}};
  17         37  
52             } elsif ($name eq 'transformMove') {
53 2         5 ($destptr, $srcptr) = @{$_->{children}};
  2         4  
54 2         7 $srcptr = _eval_expr($data, $srcptr, _make_sysvals(), $uservals, 1);
55 2 50       11 die "invalid src pointer '$srcptr'" if !_pointer(1, $data, $srcptr);
56 2         3 my $srcdata = _pointer(0, $data, $srcptr, 1);
57 2         5 _apply_destination($data, $destptr, $srcdata, $uservals);
58 2         8 return $data;
59             } else {
60 0         0 die "Unknown transform type '$name'";
61             }
62 25         53 my $srcdata = _eval_expr($data, $srcptr, _make_sysvals(), $uservals);
63 25         54 my $newdata;
64 25 100       74 if ($mapping) {
65 11         27 my $opFrom = $mapping->{attributes}{opFrom};
66 11 50 66     39 die "Expected '$srcptr' to point to hash"
67             if $opFrom eq '<%' and ref $srcdata ne 'HASH';
68 11 50 66     34 die "Expected '$srcptr' to point to array"
69             if $opFrom eq '<@' and ref $srcdata ne 'ARRAY';
70 11         170 $newdata = _apply_mapping($data, $mapping->{children}[0], dclone $srcdata, $uservals);
71             } else {
72 14         21 $newdata = $srcdata;
73             }
74 25         54 _apply_destination($data, $destptr, $newdata, $uservals);
75             }
76 19         57 $data;
77 21         144205 };
78             }
79              
80             sub _apply_destination {
81 27     27   56 my ($topdata, $destptr, $newdata, $uservals) = @_;
82 27         44 my $name = $destptr->{nodename};
83 27 100       55 if ($name eq 'jsonPointer') {
    50          
84 24         39 $destptr = _eval_expr($topdata, $destptr, _make_sysvals(), $uservals, 1);
85 24         65 _pointer(0, $_[0], $destptr, 0, $newdata);
86             } elsif ($name eq 'variableUser') {
87 3         7 my $var = $destptr->{children}[0];
88 3         21 $uservals->{$var} = $newdata;
89             } else {
90 0         0 die "unknown destination type '$name'";
91             }
92             }
93              
94             sub _apply_mapping {
95 11     11   32 my ($topdata, $mapping, $thisdata, $uservals) = @_;
96 11         21 my $name = $mapping->{nodename};
97 11         18 my @pairs = _data2pairs($thisdata);
98 11 100       35 if ($name eq 'exprObjectMapping') {
    100          
    50          
99 2         5 my ($keyexpr, $valueexpr) = @{$mapping->{children}};
  2         6  
100 2         3 my %data;
101 2         6 for (@pairs) {
102 4         9 my $sysvals = _make_sysvals($_, \@pairs);
103 4         10 my $key = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
104 4         9 my $value = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
105 4         14 $data{$key} = $value;
106             }
107 2         8 return \%data;
108             } elsif ($name eq 'exprArrayMapping') {
109 6         9 my ($valueexpr) = @{$mapping->{children}};
  6         14  
110 6         8 my @data;
111 6         14 for (@pairs) {
112 14         26 my $sysvals = _make_sysvals($_, \@pairs);
113 14         29 my $value = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
114 14         38 push @data, $value;
115             }
116 6         18 return \@data;
117             } elsif ($name eq 'exprSingleValue') {
118 3         5 my ($valueexpr) = $mapping;
119 3         9 my $sysvals = _make_sysvals(undef, \@pairs);
120 3         14 return _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
121             } else {
122 0         0 die "Unknown mapping type '$name'";
123             }
124             }
125              
126             sub _make_sysvals {
127 72     72   124 my ($pair, $pairs) = @_;
128 72         160 my %vals = (E => \%ENV);
129 72 100       150 $vals{C} = scalar @$pairs if $pairs;
130 72 100       138 @vals{qw(K V)} = @$pair if $pair;
131 72         142 return \%vals;
132             }
133              
134             sub _eval_expr {
135 194     194   316 my ($topdata, $expr, $sysvals, $uservals, $as_location) = @_;
136 194         290 my $name = $expr->{nodename};
137 194 100 100     623 if ($name eq 'jsonPointer') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
138             my $text = join '', '', map _eval_expr($topdata, $_, $sysvals, $uservals),
139 48 100       79 @{$expr->{children} || []};
  48         191  
140 48 100       137 return $text if $as_location;
141 22 50       40 die "invalid src pointer '$text'" if !_pointer(1, $topdata, $text);
142 22         44 return _pointer(0, $topdata, $text);
143             } elsif ($name eq 'variableUser') {
144 3         13 my $var = $expr->{children}[0];
145 3 50       10 die "Unknown user variable '$var'" if !exists $uservals->{$var};
146 3         6 return $uservals->{$var};
147             } elsif ($name eq 'variableSystem') {
148 32         72 my $var = $expr->{children}[0];
149 32 50       66 die "Unknown system variable '$var'" if !exists $sysvals->{$var};
150 32         66 return $sysvals->{$var};
151             } elsif ($name eq 'jsonOtherNotDouble' or $name eq 'jsonOtherNotGrave') {
152 39         135 return $expr->{children}[0];
153             } elsif ($name eq 'exprStringQuoted') {
154             my $text = join '', '', map _eval_expr($topdata, $_, $sysvals, $uservals),
155 15 50       22 @{$expr->{children} || []};
  15         48  
156 15         38 return $text;
157             } elsif ($name eq 'exprSingleValue') {
158 49         66 my ($mainexpr, @other) = @{$expr->{children}};
  49         116  
159 49         97 my $value = _eval_expr($topdata, $mainexpr, $sysvals, $uservals);
160 49         96 for (@other) {
161 10         16 my $othername = $_->{nodename};
162 10 100       34 if ($othername eq 'exprKeyRemove') {
    100          
    50          
163 2         3 my ($keyexpr) = @{$_->{children}};
  2         5  
164 2         5 my $whichkey = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
165 2         6 delete $value->{$whichkey};
166             } elsif ($othername eq 'exprKeyAdd') {
167 4         7 my ($keyexpr, $valueexpr) = @{$_->{children}};
  4         8  
168 4         9 my $key = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
169 4         9 my $addvalue = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
170 4         10 $value->{$key} = $addvalue;
171             } elsif ($othername eq 'exprApplyJsonPointer') {
172 4         7 my ($ptrexpr) = @{$_->{children}};
  4         7  
173 4         10 return _eval_expr($value, $ptrexpr, $sysvals, $uservals);
174             } else {
175 0         0 die "Unknown expression modifier '$othername'";
176             }
177             }
178 45         101 return $value;
179             } elsif ($IS_BACKSLASH_ENTITY{$name}) {
180 1         3 my ($what) = @{$expr->{children}};
  1         2  
181 1         4 my $really = $QUOTED2LITERAL{$what};
182 1 50       4 die "Unknown $name '$what'" if !defined $really;
183 1         3 return $really;
184             } elsif ($name eq 'jsonUnicode') {
185 1         3 my ($what) = @{$expr->{children}};
  1         4  
186 1         6 return chr hex $what;
187             } elsif ($name eq 'exprArrayLiteral') {
188 3 100       6 my @contents = @{$expr->{children} || []};
  3         14  
189 3         5 my @data;
190 3         8 for (@contents) {
191 2         6 my $value = _eval_expr($topdata, $_, $sysvals, $uservals);
192 2         5 push @data, $value;
193             }
194 3         8 return \@data;
195             } elsif ($name eq 'exprObjectLiteral') {
196 3 50       4 my @colonPairs = @{$expr->{children} || []};
  3         12  
197 3         4 my %data;
198 3         7 for (@colonPairs) {
199 5         6 my ($keyexpr, $valueexpr) = @{$_->{children}};
  5         10  
200 5         10 my $key = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
201 5         11 my $value = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
202 5         14 $data{$key} = $value;
203             }
204 3         8 return \%data;
205             } else {
206 0         0 die "Unknown expr type '$name'";
207             }
208             }
209              
210             sub _data2pairs {
211 11     11   20 my ($data) = @_;
212 11 100       31 if (ref $data eq 'HASH') {
    50          
213 6         56 return map [ $_, $data->{$_} ], sort keys %$data;
214             } elsif (ref $data eq 'ARRAY') {
215 5         9 my $count = 0;
216 5         29 return map [ $count++, $_ ], @$data;
217             } else {
218 0         0 die "Given data '$data' neither array nor hash";
219             }
220             }
221              
222             # based on heart of Mojo::JSON::Pointer
223             # could be more memory-efficient by shallow-copy/replacing data at each level
224             sub _pointer {
225 72     72   140 my ($contains, $data, $pointer, $is_delete, $set_to) = @_;
226 72         100 my $is_set = @_ > 4; # if 5th arg supplied, even if false
227 72 100 100     208 return $_[1] = $set_to if $is_set and !length $pointer;
228 55 100       217 return $contains ? 1 : $data unless $pointer =~ s!^/!!;
    100          
229 37         55 my $lastptr;
230 37 50       99 my @parts = length $pointer ? (split '/', $pointer, -1) : ($pointer);
231 37         89 while (defined(my $p = shift @parts)) {
232 41         63 $p =~ s!~1!/!g;
233 41         51 $p =~ s/~0/~/g;
234 41 100       85 if (ref $data eq 'HASH') {
    50          
235 34 50 66     74 return undef if !exists $data->{$p} and !$is_set;
236 34         40 $data = ${ $lastptr = \(
237 34 100 66     184 @parts == 0 && $is_delete ? delete $data->{$p} : $data->{$p}
238             )};
239             }
240             elsif (ref $data eq 'ARRAY') {
241 7 0 33     29 return undef if !($p =~ /^\d+$/ || @$data > $p) and !$is_set;
      33        
242 7 50 66     9 $data = ${ $lastptr = \(
  7         36  
243             @parts == 0 && $is_delete ? delete $data->[$p] : $data->[$p]
244             )};
245             }
246 0         0 else { return undef }
247             }
248 37 100 66     113 $$lastptr = $set_to if defined $lastptr and $is_set;
249 37 100       103 return $contains ? 1 : $data;
250             }
251              
252             1;
253              
254             __END__