File Coverage

blib/lib/Data/CSel/WrapStruct.pm
Criterion Covered Total %
statement 73 86 84.8
branch 18 32 56.2
condition 1 3 33.3
subroutine 16 21 76.1
pod 2 2 100.0
total 110 144 76.3


line stmt bran cond sub pod time code
1             package Data::CSel::WrapStruct;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-04-15'; # DATE
5             our $DIST = 'Data-CSel-WrapStruct'; # DIST
6             our $VERSION = '0.008'; # VERSION
7              
8 1     1   111300 use 5.010001;
  1         14  
9 1     1   6 use strict;
  1         2  
  1         22  
10 1     1   5 use warnings;
  1         2  
  1         41  
11              
12 1     1   8 use Exporter qw(import);
  1         2  
  1         54  
13             our @EXPORT_OK = qw(
14             wrap_struct
15             unwrap_tree
16             );
17              
18             # convenience
19 1     1   8 use Data::CSel ();
  1         2  
  1         1152  
20             unshift @Data::CSel::CLASS_PREFIXES, __PACKAGE__
21             unless grep { $_ eq __PACKAGE__ } @Data::CSel::CLASS_PREFIXES;
22              
23             sub _wrap {
24 50     50   82 my ($data, $parent, $key_in_parent) = @_;
25 50         78 my $ref = ref($data);
26 50 100       99 if (!$ref) {
    100          
    50          
    0          
    0          
27 31         80 return Data::CSel::WrapStruct::Scalar->new($data, $parent, $key_in_parent);
28             #} elsif (blessed $data) {
29             } elsif ($ref eq 'ARRAY') {
30 15         49 my $node = Data::CSel::WrapStruct::Array->new($data, $parent);
31 15         26 $node->children([ map { _wrap($data->[$_], $node, $_) } 0..$#{$data}]);
  45         85  
  15         29  
32 15         41 return $node;
33             } elsif ($ref eq 'HASH') {
34 4         19 my $node = Data::CSel::WrapStruct::Hash->new($data, $parent);
35 4         16 my @keys = sort keys %$data;
36 4         15 $node->_keys(\@keys);
37 4         11 $node->children([ map { _wrap($data->{$_}, $node, $_) } @keys]);
  2         6  
38 4         10 return $node;
39             } elsif ($ref eq 'SCALAR') {
40 0         0 return Data::CSel::WrapStruct::ScalarRef->new($data, $parent, undef);
41             } elsif ($ref eq 'JSON::PP::Boolean') {
42 0         0 return Data::CSel::WrapStruct::Scalar->new($$data, $parent, undef);
43             } else {
44 0         0 die "Sorry, currently can't handle ref=$ref";
45             }
46             }
47              
48             sub wrap_struct {
49 3     3 1 8108 my $data = shift;
50 3         14 _wrap($data, undef, undef);
51             }
52              
53             sub unwrap_tree {
54 8     8 1 8645 my $tree = shift;
55              
56 8         15 state $cleaner = do {
57 1         615 require Data::Clean;
58 1         4828 Data::Clean->new(
59             '!recurse_obj' => 1,
60             'Data::CSel::WrapStruct::Scalar' => [call_method=>'value'],
61             'Data::CSel::WrapStruct::ScalarRef' => [call_method=>'value'],
62             'Data::CSel::WrapStruct::Array' => [call_method=>'value'],
63             'Data::CSel::WrapStruct::Hash' => [call_method=>'value'],
64             );
65             };
66              
67 8         2513 $cleaner->clean_in_place($tree);
68             }
69              
70             package
71             Data::CSel::WrapStruct::Base;
72              
73             sub new {
74 50     50   87 my ($class, $data_ref, $parent, $key_in_parent) = @_;
75 50         164 bless [$data_ref, $parent, $key_in_parent], $class;
76             }
77              
78             sub value {
79 44     44   1768 my $self = shift;
80 44 100       94 if (@_) {
81 2         7 my ($parent, $key_in_parent) = ($self->[1], $self->[2]);
82 2         5 my $new_value = shift;
83 2         3 my $orig_value = $self->[0];
84 2         5 $self->[0] = $new_value;
85 2 50       6 if (defined $key_in_parent) {
86 2         6 my $ref_parent = ref $parent->[0];
87 2 50       6 if ($ref_parent eq 'ARRAY') {
    0          
88 2         5 $parent->[0][$key_in_parent] = $new_value;
89             } elsif ($ref_parent eq 'HASH') {
90 0         0 $parent->[0]{$key_in_parent} = $new_value;
91             } else {
92 0         0 warn "Cannot replace value in parent: not array/hash";
93             }
94             }
95 2         5 return $new_value;
96             }
97 42         300 $self->[0];
98             }
99              
100             sub remove {
101 5     5   109 my $self = shift;
102 5         12 my ($parent, $key_in_parent) = ($self->[1], $self->[2]);
103 5 50 33     25 if (defined $parent && defined $key_in_parent) {
104 5         12 my $ref_parent = ref $parent->[0];
105 5 50       11 if ($ref_parent eq 'ARRAY') {
    0          
106 5         8 splice @{ $parent->[0] }, $key_in_parent, 1;
  5         12  
107             # shift larger indexes by 1
108 5         7 for my $chld (@{ $parent->children }) {
  5         12  
109 18 100       49 $chld->[2]-- if $chld->[2] >= $key_in_parent;
110             }
111             } elsif ($ref_parent eq 'HASH') {
112 0         0 delete $parent->[0]{$key_in_parent};
113             } else {
114 0         0 warn "Cannot remove node from parent: not array/hash";
115             }
116             }
117 5         12 undef;
118             }
119              
120             sub parent {
121 0     0   0 $_[0][1];
122             }
123              
124             package
125             Data::CSel::WrapStruct::Scalar;
126              
127             our @ISA = qw(Data::CSel::WrapStruct::Base);
128              
129             sub children {
130 70     70   1557 [];
131             }
132              
133             package
134             Data::CSel::WrapStruct::ScalarRef;
135              
136             our @ISA = qw(Data::CSel::WrapStruct::Base);
137              
138             sub children {
139 0     0   0 [];
140             }
141              
142             package
143             Data::CSel::WrapStruct::Array;
144              
145             our @ISA = qw(Data::CSel::WrapStruct::Base);
146              
147             sub children {
148 56 100   56   10986 if (@_ > 1) {
149 15         26 $_[0][2] = $_[1];
150             }
151 56         114 $_[0][2];
152             }
153              
154             sub length {
155 0     0   0 scalar @{ $_[0][0] };
  0         0  
156             }
157              
158             package
159             Data::CSel::WrapStruct::Hash;
160              
161             our @ISA = qw(Data::CSel::WrapStruct::Base);
162              
163             sub _keys {
164 4 50   4   13 if (@_ > 1) {
165 4         9 $_[0][2] = $_[1];
166             }
167 4         7 $_[0][2];
168             }
169              
170             sub children {
171 14 100   14   235 if (@_ > 1) {
172 4         9 $_[0][3] = $_[1];
173             }
174 14         28 $_[0][3];
175             }
176              
177             sub length {
178 4     4   460 scalar @{ $_[0][2] };
  4         13  
179             }
180              
181             sub has_key {
182 0     0     exists $_[0][0]{$_[1]};
183             }
184              
185             sub key {
186 0     0     $_[0][0]{$_[1]};
187             }
188              
189             1;
190             # ABSTRACT: Wrap data structure into a tree of objects suitable for use with Data::CSel
191              
192             __END__
193              
194             =pod
195              
196             =encoding UTF-8
197              
198             =head1 NAME
199              
200             Data::CSel::WrapStruct - Wrap data structure into a tree of objects suitable for use with Data::CSel
201              
202             =head1 VERSION
203              
204             This document describes version 0.008 of Data::CSel::WrapStruct (from Perl distribution Data-CSel-WrapStruct), released on 2020-04-15.
205              
206             =head1 SYNOPSIS
207              
208             use Data::CSel qw(csel);
209             use Data::CSel::WrapStruct qw(wrap_struct unwrap_tree);
210              
211             my $data = [
212             0,
213             1,
214             [2, ["two","dua"], {url=>"http://example.com/two.jpg"}, ["even","prime"]],
215             3,
216             [4, ["four","empat"], {}, ["even"]],
217             ];
218              
219             my $tree = wrap_struct($data);
220             my @nodes = csel(":root > * > *:nth-child(4) > *", $tree);
221             my @tags = map { $_->value } @nodes; # -> ("even", "prime", "even")
222              
223             Scalars are wrapped using C<Data::CSel::WrapStruct::Scalar> class, scalarrefs
224             are wrapped using C<Data::CSel::WrapStruct::ScalarRef> class, arrays are wrapped
225             using C<Data::CSel::WrapStruct::Array> class, and hashes are wrapped using
226             C<Data::CSel::WrapStruct::Hash> class. For convenience, when you load
227             C<Data::CSel::WrapStruct>, it adds C<Data::CSel::WrapStruct> to
228             C<@Data::CSel::CLASS_PREFIXES> so you don't have to specify C<<
229             {class_prefixes=>["Data::CSel::WrapStruct"]} >> C<csel()> option everytime.
230              
231             my @hashes = map {$_->value} csel("Hash", $tree);
232             # -> ({url=>"http://example.com/two.jpg"}, {})
233              
234             The wrapper objects provide some methods, e.g.:
235              
236             my @empty_hashes = map {$_->value} csel("Hash[length=0]", $tree);
237             # -> ({})
238              
239             my @hashes_that_have_url_key = map {$_->value} csel("Hash[has_key('url')]", $tree);
240             # -> ({url=>"http://example.com/two.jpg"})
241              
242             my @larger_scalars = [map {$_->value} csel("Scalar[value >= 3]", $tree)]
243             # -> (3, 4)
244              
245             See L</NODE METHODS>, L</SCALAR NODE METHODS>, L</SCALARREF NODE METHODS>,
246             L</ARRAY NODE METHODS>, L</HASH NODE METHODS> for more details on the provided
247             methods.
248              
249             You can replace the value of nodes using L</value>:
250              
251             my @posint_scalar_nodes = csel("Scalar[value > 0]", $tree);
252             for (@posint_scalar_nodes) { $_->value( $_->value * 10 ) }
253             use Data::Dump;
254             dd unwrap_tree($data);
255             # => [
256             # 0,
257             # 10,
258             # [20, ["two","dua"], {url=>"http://example.com/two.jpg"}, ["even","prime"]],
259             # 30,
260             # [40, ["four","empat"], {}, ["even"]],
261             # ];
262              
263             =head1 DESCRIPTION
264              
265             This module provides C<wrap_struct()> which creates a tree of objects from a
266             generic data structure. You can then perform node selection using
267             L<Data::CSel>'s C<csel()>.
268              
269             You can retrieve the original value of data items by calling C<value()> method
270             on the tree nodes.
271              
272             =for Pod::Coverage ^(.+)$
273              
274             =head1 NODE METHODS
275              
276             =head2 parent
277              
278             =head2 children
279              
280             =head2 value
281              
282             Usage:
283              
284             my $val = $node->value; # get node value
285             $node->value(1); # set node value
286              
287             Get or set node value.
288              
289             Note that when setting node value, the new node value is not automatically
290             wrapped for you. If you want to set new node value and expect to select it or
291             part of it again with C<csel()>, you will have to wrap the new value first with
292             L</wrap_struct>.
293              
294             =head2 remove
295              
296             Usage:
297              
298             $node->remove;
299              
300             Remove node from parent.
301              
302             =head1 SCALAR NODE METHODS
303              
304             In addition to methods listed in L</NODE METHODS>, Scalar nodes also have the
305             following methods.
306              
307             =head1 SCALARREF NODE METHODS
308              
309             In addition to methods listed in L</NODE METHODS>, ScalarRef nodes also have the
310             following methods.
311              
312             =head1 ARRAY NODE METHODS
313              
314             In addition to methods listed in L</NODE METHODS>, Array nodes also have the
315             following methods.
316              
317             =head2 length
318              
319             Get array length. Can be used to select an array based on its length, e.g.:
320              
321             @nodes = csel('Array[length > 0]');
322              
323             =head1 HASH NODE METHODS
324              
325             In addition to methods listed in L</NODE METHODS>, Hash nodes also have the
326             following methods.
327              
328             =head2 length
329              
330             Get the number of keys. Can be used to select a hash based on its number of
331             keys, e.g.:
332              
333             @nodes = csel('Hash[length > 0]');
334              
335             =head2 has_key
336              
337             Usage:
338              
339             my $bool = $node->has_key("foo");
340              
341             Check whether hash has a certain key. Can be used to select a hash, e.g.:
342              
343             @nodes = csel('Hash[has_key("foo")]');
344              
345             =head2 key
346              
347             Usage:
348              
349             my $key_val = $node->key("foo");
350              
351             Get a hash key's value. Can be used to select a hash based on the value of one
352             of its keys, e.g.:
353              
354             @nodes = csel('Hash[key("name") = "lisa"]');
355              
356             =head1 FUNCTIONS
357              
358             None exported by default, but exportable.
359              
360             =head2 wrap_struct
361              
362             Usage:
363              
364             my $tree = wrap_struct($data);
365              
366             Wrap a data structure into a tree of objects.
367              
368             Currently cannot handle recursive structure.
369              
370             =head2 unwrap_tree
371              
372             Usage:
373              
374             my $data = unwrap_tree($wrapped_data);
375              
376             Unwrap a tree produced by L</wrap_tree> back into unwrapped data structure.
377              
378             =head1 FAQ
379              
380             =head2 Changing the node value doesn't work!
381              
382             my $data = [0, 1, 2];
383             my @nodes = csel("Scalar[value > 0]", wrap_struct($data));
384             for (@nodes) { $_->[0] = "x" }
385             use Data::Dump;
386             dd $data;
387              
388             still prints C<< [0,1,2] >> instead of C<< [0,'x','x'] >>. Why?
389              
390             To set node value, you have to use the C<value()> node method with an argument:
391              
392             ...
393             for (@nodes) { $->value("x") }
394             ...
395              
396             will then print the expected C<< [0,'x','x'] >>.
397              
398             =head2 This module is slow!
399              
400             If you intend to select a data structure with thousands of nodes or more, you're
401             probably better off using other approach, for example L<Data::Walk::More>.
402             Data::Walk::More provides containers for the nodes you're traversing. For
403             example, the CSel expression C<< Hash[has_keys("foo")] > Array > Scalar[value >
404             0] >> can be written as:
405              
406             walk sub {
407             my $ref = ref $_;
408             return if ref $_; # ... Scalar
409             return if $_ <= 0; # ... [value > 0]
410             return unless ref $Data::Walk::More::containers[-1] eq 'ARRAY'; # ... Array
411             return unless ref $Data::Walk::More::containers[-2] eq 'HASH'; # ... Hash
412             return unless exists $Data::Walk::More::containers[-2]{foo}; # ... [has_keys("foo")]
413             push @matches, $_;
414             }, $data;
415              
416             =head1 HOMEPAGE
417              
418             Please visit the project's homepage at L<https://metacpan.org/release/Data-CSel-WrapStruct>.
419              
420             =head1 SOURCE
421              
422             Source repository is at L<https://github.com/perlancar/perl-Data-CSel-WrapStruct>.
423              
424             =head1 BUGS
425              
426             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Data-CSel-WrapStruct>
427              
428             When submitting a bug or request, please include a test-file or a
429             patch to an existing test-file that illustrates the bug or desired
430             feature.
431              
432             =head1 SEE ALSO
433              
434             L<Data::CSel>
435              
436             =head1 AUTHOR
437              
438             perlancar <perlancar@cpan.org>
439              
440             =head1 COPYRIGHT AND LICENSE
441              
442             This software is copyright (c) 2020, 2016 by perlancar@cpan.org.
443              
444             This is free software; you can redistribute it and/or modify it under
445             the same terms as the Perl 5 programming language system itself.
446              
447             =cut