File Coverage

blib/lib/JSON/Transform.pm
Criterion Covered Total %
statement 144 151 95.3
branch 65 84 77.3
condition 20 30 66.6
subroutine 14 14 100.0
pod 1 1 100.0
total 244 280 87.1


line stmt bran cond sub pod time code
1             package JSON::Transform;
2              
3 1     1   88347 use strict;
  1         12  
  1         28  
4 1     1   5 use warnings;
  1         2  
  1         26  
5 1     1   5 use Exporter 'import';
  1         2  
  1         28  
6 1     1   415 use JSON::Transform::Parser qw(parse);
  1         3  
  1         63  
7 1     1   636 use Storable qw(dclone);
  1         3205  
  1         70  
8              
9 1     1   8 use constant DEBUG => $ENV{JSON_TRANSFORM_DEBUG};
  1         4  
  1         1769  
10              
11             our $VERSION = '0.01';
12             our @EXPORT_OK = qw(
13             parse_transform
14             );
15              
16             sub parse_transform {
17 15     15 1 11212 my ($input_text) = @_;
18 15         47 my $transforms = parse $input_text;
19             sub {
20 15     15   36 my ($data) = @_;
21 15         452 $data = dclone $data; # now can mutate away
22 15         43 my $uservals = {};
23 15         20 for (@{$transforms->{children}}) {
  15         43  
24 21         40 my $name = $_->{nodename};
25 21         33 my ($srcptr, $destptr, $mapping);
26 21 100       60 if ($name eq 'transformImpliedDest') {
    100          
    50          
27 8         13 ($srcptr, $mapping) = @{$_->{children}};
  8         13  
28 8         14 $destptr = $srcptr;
29             } elsif ($name eq 'transformCopy') {
30 11         18 ($destptr, $srcptr, $mapping) = @{$_->{children}};
  11         20  
31             } elsif ($name eq 'transformMove') {
32 2         16 ($destptr, $srcptr) = @{$_->{children}};
  2         7  
33 2         6 $srcptr = _eval_expr($data, $srcptr, _make_sysvals(), $uservals, 1);
34 2 50       8 die "invalid src pointer '$srcptr'" if !_pointer(1, $data, $srcptr);
35 2         6 my $srcdata = _pointer(0, $data, $srcptr, 1);
36 2         7 _apply_destination($data, $destptr, $srcdata, $uservals);
37 2         8 return $data;
38             } else {
39 0         0 die "Unknown transform type '$name'";
40             }
41 19         38 my $srcdata = _eval_expr($data, $srcptr, _make_sysvals(), $uservals);
42 19         44 my $newdata;
43 19 100       33 if ($mapping) {
44 10         22 my $opFrom = $mapping->{attributes}{opFrom};
45 10 50 66     35 die "Expected '$srcptr' to point to hash"
46             if $opFrom eq '<%' and ref $srcdata ne 'HASH';
47 10 50 66     33 die "Expected '$srcptr' to point to array"
48             if $opFrom eq '<@' and ref $srcdata ne 'ARRAY';
49 10         163 $newdata = _apply_mapping($data, $mapping->{children}[0], dclone $srcdata, $uservals);
50             } else {
51 9         13 $newdata = $srcdata;
52             }
53 19         39 _apply_destination($data, $destptr, $newdata, $uservals);
54             }
55 13         41 $data;
56 15         82199 };
57             }
58              
59             sub _apply_destination {
60 21     21   44 my ($topdata, $destptr, $newdata, $uservals) = @_;
61 21         35 my $name = $destptr->{nodename};
62 21 100       39 if ($name eq 'jsonPointer') {
    50          
63 18         36 $destptr = _eval_expr($topdata, $destptr, _make_sysvals(), $uservals, 1);
64 18         49 _pointer(0, $_[0], $destptr, 0, $newdata);
65             } elsif ($name eq 'variableUser') {
66 3         6 my $var = $destptr->{children}[0];
67 3         12 $uservals->{$var} = $newdata;
68             } else {
69 0         0 die "unknown destination type '$name'";
70             }
71             }
72              
73             sub _apply_mapping {
74 10     10   26 my ($topdata, $mapping, $thisdata, $uservals) = @_;
75 10         17 my $name = $mapping->{nodename};
76 10         19 my @pairs = _data2pairs($thisdata);
77 10 100       33 if ($name eq 'exprObjectMapping') {
    100          
    50          
78 2         3 my ($keyexpr, $valueexpr) = @{$mapping->{children}};
  2         5  
79 2         5 my %data;
80 2         5 for (@pairs) {
81 4         10 my $sysvals = _make_sysvals($_, \@pairs);
82 4         9 my $key = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
83 4         8 my $value = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
84 4         14 $data{$key} = $value;
85             }
86 2         8 return \%data;
87             } elsif ($name eq 'exprArrayMapping') {
88 5         7 my ($valueexpr) = @{$mapping->{children}};
  5         11  
89 5         8 my @data;
90 5         11 for (@pairs) {
91 12         23 my $sysvals = _make_sysvals($_, \@pairs);
92 12         21 my $value = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
93 12         36 push @data, $value;
94             }
95 5         17 return \@data;
96             } elsif ($name eq 'exprSingleValue') {
97 3         7 my ($valueexpr) = $mapping;
98 3         7 my $sysvals = _make_sysvals(undef, \@pairs);
99 3         10 return _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
100             } else {
101 0         0 die "Unknown mapping type '$name'";
102             }
103             }
104              
105             sub _make_sysvals {
106 58     58   101 my ($pair, $pairs) = @_;
107 58         145 my %vals = (EO => {}, EA => []);
108 58 100       131 $vals{C} = scalar @$pairs if $pairs;
109 58 100       112 @vals{qw(K V)} = @$pair if $pair;
110 58         127 return \%vals;
111             }
112              
113             sub _eval_expr {
114 121     121   211 my ($topdata, $expr, $sysvals, $uservals, $as_location) = @_;
115 121         167 my $name = $expr->{nodename};
116 121 100 100     342 if ($name eq 'jsonPointer') {
    100          
    100          
    100          
    100          
    50          
117             my $text = join '', '', map _eval_expr($topdata, $_, $sysvals, $uservals),
118 36 100       47 @{$expr->{children} || []};
  36         145  
119 36 100       101 return $text if $as_location;
120 16 50       35 die "invalid src pointer '$text'" if !_pointer(1, $topdata, $text);
121 16         35 return _pointer(0, $topdata, $text);
122             } elsif ($name eq 'variableUser') {
123 3         7 my $var = $expr->{children}[0];
124 3 50       7 die "Unknown user variable '$var'" if !exists $uservals->{$var};
125 3         7 return $uservals->{$var};
126             } elsif ($name eq 'variableSystem') {
127 29         48 my $var = $expr->{children}[0];
128 29 50       56 die "Unknown system variable '$var'" if !exists $sysvals->{$var};
129 29         61 return $sysvals->{$var};
130             } elsif ($name eq 'jsonOtherNotDouble' or $name eq 'jsonOtherNotGrave') {
131 24         84 return $expr->{children}[0];
132             } elsif ($name eq 'exprStringQuoted') {
133             my $text = join '', '', map _eval_expr($topdata, $_, $sysvals, $uservals),
134 6 50       8 @{$expr->{children} || []};
  6         23  
135 6         16 return $text;
136             } elsif ($name eq 'exprSingleValue') {
137 23         34 my ($mainexpr, @other) = @{$expr->{children}};
  23         43  
138 23         43 my $value = _eval_expr($topdata, $mainexpr, $sysvals, $uservals);
139 23         46 for (@other) {
140 6         9 my $othername = $_->{nodename};
141 6 100       16 if ($othername eq 'exprKeyRemove') {
    50          
142 2         3 my ($keyexpr) = @{$_->{children}};
  2         4  
143 2         5 my $whichkey = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
144 2         7 delete $value->{$whichkey};
145             } elsif ($othername eq 'exprKeyAdd') {
146 4         6 my ($keyexpr, $valueexpr) = @{$_->{children}};
  4         36  
147 4         10 my $key = _eval_expr($topdata, $keyexpr, $sysvals, $uservals);
148 4         7 my $addvalue = _eval_expr($topdata, $valueexpr, $sysvals, $uservals);
149 4         11 $value->{$key} = $addvalue;
150             } else {
151 0         0 die "Unknown expression modifier '$othername'";
152             }
153             }
154 23         54 return $value;
155             } else {
156 0         0 die "Unknown expr type '$name'";
157             }
158             }
159              
160             sub _data2pairs {
161 10     10   22 my ($data) = @_;
162 10 100       29 if (ref $data eq 'HASH') {
    50          
163 5         46 return map [ $_, $data->{$_} ], sort keys %$data;
164             } elsif (ref $data eq 'ARRAY') {
165 5         8 my $count = 0;
166 5         25 return map [ $count++, $_ ], @$data;
167             } else {
168 0         0 die "Given data '$data' neither array nor hash";
169             }
170             }
171              
172             # based on heart of Mojo::JSON::Pointer
173             # could be more memory-efficient by shallow-copy/replacing data at each level
174             sub _pointer {
175 54     54   106 my ($contains, $data, $pointer, $is_delete, $set_to) = @_;
176 54         77 my $is_set = @_ > 4; # if 5th arg supplied, even if false
177 54 100 100     151 return $_[1] = $set_to if $is_set and !length $pointer;
178 43 100       168 return $contains ? 1 : $data unless $pointer =~ s!^/!!;
    100          
179 25         38 my $lastptr;
180 25 50       71 my @parts = length $pointer ? (split '/', $pointer, -1) : ($pointer);
181 25         61 while (defined(my $p = shift @parts)) {
182 29         42 $p =~ s!~1!/!g;
183 29         38 $p =~ s/~0/~/g;
184 29 100       59 if (ref $data eq 'HASH') {
    50          
185 22 50 66     52 return undef if !exists $data->{$p} and !$is_set;
186 22         26 $data = ${ $lastptr = \(
187 22 100 66     116 @parts == 0 && $is_delete ? delete $data->{$p} : $data->{$p}
188             )};
189             }
190             elsif (ref $data eq 'ARRAY') {
191 7 0 33     32 return undef if !($p =~ /^\d+$/ || @$data > $p) and !$is_set;
      33        
192 7 50 66     10 $data = ${ $lastptr = \(
  7         39  
193             @parts == 0 && $is_delete ? delete $data->[$p] : $data->[$p]
194             )};
195             }
196 0         0 else { return undef }
197             }
198 25 100 66     82 $$lastptr = $set_to if defined $lastptr and $is_set;
199 25 100       65 return $contains ? 1 : $data;
200             }
201              
202             =head1 NAME
203              
204             JSON::Transform - arbitrary transformation of JSON-able data
205              
206             =begin markdown
207              
208             # PROJECT STATUS
209              
210             | OS | Build status |
211             |:-------:|--------------:|
212             | Linux | [![Build Status](https://travis-ci.org/mohawk2/json-transform.svg?branch=master)](https://travis-ci.org/mohawk2/json-transform) |
213              
214             [![CPAN version](https://badge.fury.io/pl/JSON-Transform.svg)](https://metacpan.org/pod/JSON::Transform) [![Coverage Status](https://coveralls.io/repos/github/mohawk2/json-transform/badge.svg?branch=master)](https://coveralls.io/github/mohawk2/json-transform?branch=master)
215              
216             =end markdown
217              
218             =head1 SYNOPSIS
219              
220             use JSON::Transform qw(parse_transform);
221             use JSON::MaybeXS;
222             my $transformer = parse_transform(from_file($transformfile));
223             to_file($outputfile, encode_json $transformer->(decode_json $json_input));
224              
225             =head1 DESCRIPTION
226              
227             Implements a language concisely describing a set of
228             transformations from an arbitrary JSON-able piece of data, to
229             another one. The description language uses L
230             6901)|https://tools.ietf.org/html/rfc6901> for addressing. JSON-able
231             means only strings, booleans, nulls (Perl C), numbers, array-refs,
232             hash-refs, with no circular references.
233              
234             A transformation is made up of an output expression, which can be composed
235             of sub-expressions.
236              
237             For instance, to transform an array of hashes that each have an C
238             key, to a hash mapping each C to its hash:
239              
240             # [ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ]
241             # ->
242             "" <@ { "/$K/id":$V#`id` }
243             # ->
244             # { "1": { "name": "Alice" }, "2": { "name": "Bob" } }
245              
246             While to do the reverse transformation:
247              
248             "" <% [ $V@`id`:$K ]
249              
250             The identity for an array:
251              
252             "" <@ [ $V ]
253              
254             The identity for an object/hash:
255              
256             "" <% { $K:$V }
257              
258             To get the keys of a hash:
259              
260             "" <% [ $K ]
261              
262             To get how many keys in a hash:
263              
264             "" <% $C
265              
266             To get how many items in an array:
267              
268             "" <@ $C
269              
270             To move from one part of a structure to another:
271              
272             "/destination" << "/source"
273              
274             To copy from one part of a structure to another:
275              
276             "/destination" <- "/source"
277              
278             To do the same with a transformation (assumes C is an array
279             of hashes):
280              
281             "/destination" <- "/source" <@ [ $V@`order`:$K ]
282              
283             To bind a variable, then replace the whole data structure:
284              
285             $defs <- "/definitions"
286             "" <- $defs
287              
288             =head2 Expression types
289              
290             =over
291              
292             =item Object/hash
293              
294             These terms are used here interchangeably.
295              
296             =item Array
297              
298             =item String
299              
300             =item Integer
301              
302             =item Float
303              
304             =item Boolean
305              
306             =item Null
307              
308             =back
309              
310             =head2 JSON pointers
311              
312             JSON pointers are surrounded by C<"">. JSON pointer syntax gives special
313             meaning to the C<~> character, as well as to C. To quote a C<~>,
314             say C<~0>. To quote a C, say C<~1>. Since a C<$> has special meaning,
315             to use a literal one, quote it with a preceding C<\>.
316              
317             The output type of a JSON pointer is whatever the pointed-at value is.
318              
319             =head2 Transformations
320              
321             A transformation has a destination, a transformation type operator, and
322             a source-value expression. The destination can be a variable to bind to,
323             or a JSON pointer.
324              
325             If the source-value expression has a JSON-pointer source, then the
326             destination can be omitted and the JSON-pointer source will be used.
327              
328             The output type of the source-value expression can be anything.
329              
330             =head3 Transformation operators
331              
332             =over
333              
334             =item C<<< <- >>>
335              
336             Copying (including assignment for variable bindings)
337              
338             =item C<<< << >>>
339              
340             Moving - error if the source-value is other than a bare JSON pointer
341              
342             =back
343              
344             =head2 Destination value expressions
345              
346             These can be either a variable, or a JSON pointer.
347              
348             =head3 Variables
349              
350             These are expressed as C<$> followed by a lower-case letter, followed
351             by zero or more letters.
352              
353             =head2 Source value expressions
354              
355             These can be either a single value including variables, of any type,
356             or a mapping expression.
357              
358             =head2 String value expressions
359              
360             String value expressions can be surrounded by C<``>. They have the same
361             quoting rules as in JSON's C<">-surrounded strings, including quoting
362             of C<`> using C<\>. Any value inside, including variables, will be
363             concatenated in the obvious way, and numbers will be coerced into strings
364             (be careful of locale). Booleans and nulls will be stringified into
365             C<[true]>, C<[false]>, C<[null]>.
366              
367             =head2 Mapping expressions
368              
369             A mapping expression has a source-value, a mapping operator, and a
370             mapping description.
371              
372             The mapping operator is either C<<< <@ >>>, requiring the source-value
373             to be of type array, or C<<< <% >>>, requiring type object/hash. If the
374             input data pointed at by the source value expression is not the right
375             type, this is an error.
376              
377             The mapping description must be surrounded by either C<[]> meaning return
378             type array, or C<{}> for object/hash.
379              
380             The description will be evaluated once for each input value.
381             Within the brackets, C<$K> and C<$V> will have special meaning.
382              
383             For an array input, each input will be each single array value, and C<$K>
384             will be the zero-based array index.
385              
386             For an object/hash input, each input will be each pair. C<$K> will be
387             the object key being evaluated, of type string.
388              
389             In either case, C<$V> will be the relevant value, of whatever type from
390             the input. C<$C> will be of type integer, being the number of inputs.
391              
392             =head3 Mapping to an object/hash
393              
394             The return value will be of type object/hash, composed of a set of pairs,
395             expressed within C<{}> as:
396              
397             =over
398              
399             =item a expression of type string
400              
401             =item C<:>
402              
403             =item an expression of any type
404              
405             =back
406              
407             =head3 Mapping to an array
408              
409             Within C<[]>, the value expression will be an arbitrary value expression.
410              
411             =head2 Single-value modifiers
412              
413             A single value can have a modifier, followed by arguments.
414              
415             =head3 C<@>
416              
417             The operand value must be of type object/hash.
418             The argument must be a pair of string-value, C<:>, any-value.
419             The return value will be the object/hash with that additional key/value pair.
420              
421             =head3 C<#>
422              
423             The operand value must be of type object/hash.
424             The argument must be a string-value.
425             The return value will be the object/hash without that key.
426              
427             =head2 Available system variables
428              
429             =head3 C<$K>
430              
431             Available in mapping expressions. For each data pair, set to either the
432             zero-based index in an array, or the string key of an object/hash.
433              
434             =head3 C<$V>
435              
436             Available in mapping expressions. For each data pair, set to the value.
437              
438             =head3 C<$C>
439              
440             Available in mapping expressions. Set to the integer number of values.
441              
442             =head3 C<$EO>
443              
444             An empty object/hash.
445              
446             =head3 C<$EA>
447              
448             An empty array.
449              
450             =head2 Comments
451              
452             Any C<--> sequence up to the end of that line will be a comment,
453             and ignored.
454              
455             =head1 DEBUGGING
456              
457             To debug, set environment variable C to a true value.
458              
459             =head1 EXPORT
460              
461             =head2 parse_transform
462              
463             On error, throws an exception. On success, returns a function that can
464             be called with JSON-able data, that will either throw an exception or
465             return the transformed data.
466              
467             Takes arguments:
468              
469             =over
470              
471             =item $input_text
472              
473             The text describing the transformation.
474              
475             =back
476              
477             =head1 SEE ALSO
478              
479             L
480              
481             L - intended
482             to change an existing structure, leaving it (largely) the same shape
483              
484             =head1 AUTHOR
485              
486             Ed J, C<< >>
487              
488             =head1 BUGS
489              
490             Please report any bugs or feature requests on
491             L.
492              
493             Or, if you prefer email and/or RT: to C
494             at rt.cpan.org>, or through the web interface at
495             L. I will be
496             notified, and then you'll automatically be notified of progress on your
497             bug as I make changes.
498              
499             =head1 LICENSE AND COPYRIGHT
500              
501             Copyright 2018 Ed J.
502              
503             This program is free software; you can redistribute it and/or modify it
504             under the terms of the the Artistic License (2.0). You may obtain a
505             copy of the full license at:
506              
507             L
508              
509             =cut
510              
511             1;