File Coverage

blib/lib/JSON/Transform.pm
Criterion Covered Total %
statement 172 179 96.0
branch 79 100 79.0
condition 20 30 66.6
subroutine 14 14 100.0
pod 1 1 100.0
total 286 324 88.2


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