File Coverage

blib/lib/DBIx/Tree/MaterializedPath/TreeRepresentation.pm
Criterion Covered Total %
statement 106 107 99.0
branch 38 40 95.0
condition 1 3 33.3
subroutine 12 12 100.0
pod 4 4 100.0
total 161 166 96.9


line stmt bran cond sub pod time code
1             package DBIx::Tree::MaterializedPath::TreeRepresentation;
2              
3 18     18   105 use warnings;
  18         37  
  18         618  
4 18     18   97 use strict;
  18         38  
  18         514  
5              
6 18     18   92 use Carp;
  18         41  
  18         1425  
7              
8 18     18   117 use Readonly;
  18         41  
  18         937  
9              
10 18     18   122 use DBIx::Tree::MaterializedPath::Node;
  18         54  
  18         611  
11              
12             =head1 NAME
13              
14             DBIx::Tree::MaterializedPath::TreeRepresentation - data structure for "materialized path" trees
15              
16             =head1 VERSION
17              
18             Version 0.06
19              
20             =cut
21              
22 18     18   94 use version 0.74; our $VERSION = qv('0.06');
  18         462  
  18         240  
23              
24             =head1 SYNOPSIS
25              
26             # Row data must be sorted by path:
27             my $column_names = ['id', 'path', 'name'];
28             my $subtree_data = [
29             [ 2, "1.1", "a"],
30             [ 3, "1.2", "b"],
31             [ 4, "1.3", "c"],
32             [ 5, "1.3.1", "d"],
33             [ 7, "1.3.1.1", "e"],
34             [ 6, "1.3.2", "f"],
35             ];
36              
37             my $subtree_representation =
38             DBIx::Tree::MaterializedPath::TreeRepresentation->new($node,
39             $column_names,
40             $subtree_data);
41              
42             $subtree_representation->traverse($coderef, $context);
43              
44             =head1 DESCRIPTION
45              
46             This module implements a data structure that represents a tree
47             (or subtree) as stored in the database.
48              
49             B Normally these objects would not be created independently
50             - call
51             L
52             on a
53             L
54             or a
55             L
56             to get its descendants as a
57             L
58             object, and then
59             L
60             those descendants.
61              
62             =head1 METHODS
63              
64             =head2 new
65              
66             $subtree_data =
67             DBIx::Tree::MaterializedPath::TreeRepresentation->new($node,
68             $cols_listref,
69             $rows_listref,
70             $options_hashref);
71              
72             C expects a
73             L
74             object (representing the node that this data belongs to), a listref
75             of database column names, and a listref of listrefs, each of which
76             represents a node row in the database.
77              
78             At minimum, each row must contain entries for the
79             L
80             and the
81             L
82             as specified in the
83             L
84             constructor. The rows should be sorted by path in ascending order.
85              
86             Additionally, the row may contain entries for
87             any metadata columns which are stored with the nodes.
88              
89             One L object will be created in
90             the data structure for each input row. If the optional parameters
91             hashref contains a true value for "B", and if no
92             metadata entries exist in the input row, then the node object's
93             metadata will not be populated, and will only be retrieved
94             from the database when the L method is called on a
95             given node.
96              
97             =cut
98              
99             sub new
100             {
101 43     43 1 5878 my ($class, $node, $column_names, $rows, @args) = @_;
102              
103 43 100       343 croak 'Missing node' unless $node;
104 42 50       979 eval { ref($node) && $node->isa('DBIx::Tree::MaterializedPath::Node') }
105             or
106 42 100       105 do { croak 'Invalid node: not a "DBIx::Tree::MaterializedPath::Node"' };
  2         254  
107              
108 40 100       523 croak 'Missing column names' unless $column_names;
109 39 100       250 croak 'Invalid column names' unless ref($column_names) eq 'ARRAY';
110              
111 38 100       235 croak 'Missing rows' unless $rows;
112 37 100       232 croak 'Invalid rows' unless ref($rows) eq 'ARRAY';
113              
114 36 100       143 my $options = ref $args[0] eq 'HASH' ? $args[0] : {@args};
115              
116 36 100       174 my $ignore_empty_hash = $options->{ignore_empty_hash} ? 1 : 0;
117              
118 36   33     326 my $self = bless {}, ref($class) || $class;
119              
120 36         153 $self->{_node} = $node;
121              
122             # E.g. calling C on node "E" below:
123             #
124             # A
125             # ___|_____
126             # | |
127             # B E
128             # _|_ ___|___
129             # | | | | |
130             # C D F I J
131             # _|_
132             # | |
133             # G H
134             #
135             # might produce column names that look like this:
136             #
137             # ['id', 'path', 'name']
138             #
139             # and database rows that look like this:
140             #
141             # [
142             # [ 6, "1.2.1", "F"],
143             # [ 7, "1.2.1.1", "G"],
144             # [ 8, "1.2.1.2", "H"],
145             # [ 9, "1.2.2", "I"],
146             # [ 10, "1.2.3", "J"],
147             # ]
148             #
149             # which results in the following data structure:
150             #
151             # [
152             # {
153             # node => DBIx::Tree::MaterializedPath::Node "F",
154             # children => [
155             # {
156             # node => DBIx::Tree::MaterializedPath::Node "G",
157             # children => [],
158             # },
159             # {
160             # node => DBIx::Tree::MaterializedPath::Node "H",
161             # children => [],
162             # },
163             # ],
164             # },
165             # {
166             # node => DBIx::Tree::MaterializedPath::Node "I",
167             # children => [],
168             # },
169             # {
170             # node => DBIx::Tree::MaterializedPath::Node "J",
171             # children => [],
172             # },
173             # ]
174              
175 36         496 my $root = $node->get_root;
176              
177 36         73 my $num_nodes = 0;
178 36         89 my @nodes = ();
179              
180 36 100       60 if (@{$rows})
  36         135  
181             {
182 28         68 my $path_col = $root->{_path_column_name};
183              
184 28         171 my $ix_path_col = 0;
185 28         51 my $found = 0;
186 28         51 foreach my $column_name (@{$column_names})
  28         88  
187             {
188 56 100       161 if ($column_name eq $path_col)
189             {
190 27         40 $found++;
191 27         64 last;
192             }
193 29         72 $ix_path_col++;
194             }
195 28 100       343 croak 'Path column name not found' unless $found;
196              
197 27         82 my $path = $rows->[0]->[$ix_path_col];
198 27         56 my $length = length $path;
199              
200 27         372 _add_descendant_nodes(
201             {
202             prev_path => q{},
203             prev_length => $length,
204             nodes => \@nodes,
205             },
206             {
207             root => $root,
208             ix_path_col => $ix_path_col,
209             column_names => $column_names,
210             num_nodes_ref => \$num_nodes,
211             rows => $rows,
212             ignore_empty_hash => $ignore_empty_hash
213             },
214             );
215             }
216              
217 35         171 $self->{_descendants} = \@nodes;
218 35         88 $self->{_num_nodes} = $num_nodes;
219 35 100       208 $self->{_has_nodes} = $self->{_num_nodes} ? 1 : 0;
220              
221 35         151 return $self;
222             }
223              
224             sub _add_descendant_nodes
225             {
226 69     69   130 my ($args, $invariant_args) = @_;
227              
228 69         139 my $prev_path = $args->{prev_path};
229 69         124 my $prev_length = $args->{prev_length};
230 69         120 my $nodes = $args->{nodes};
231              
232 69         106 my $root = $invariant_args->{root};
233 69         195 my $ix_path_col = $invariant_args->{ix_path_col};
234 69         116 my $column_names = $invariant_args->{column_names};
235 69         112 my $num_nodes_ref = $invariant_args->{num_nodes_ref};
236 69         213 my $rows = $invariant_args->{rows};
237 69         105 my $ignore_empty_hash = $invariant_args->{ignore_empty_hash};
238              
239 69         108 my $node_children = undef;
240              
241 69         95 while (@{$rows})
  254         671  
242             {
243 207         384 my $path = $rows->[0]->[$ix_path_col];
244 207         641 my $length = length $path;
245              
246             # If path length is less, we've gone back up
247             # a level in the tree:
248 207 100       732 if ($length < $prev_length)
    100          
249             {
250 22         96 return;
251             }
252              
253             # If path length is greater, we've gone down
254             # a level in the tree:
255             elsif ($length > $prev_length)
256             {
257 42         273 _add_descendant_nodes(
258             {
259             prev_path => $prev_path,
260             prev_length => $length,
261             nodes => $node_children,
262             },
263             $invariant_args,
264             );
265             }
266              
267             # If path length is the same, we're adding
268             # siblings at the same level:
269             else
270             {
271 143         171 my $row = shift @{$rows};
  143         246  
272              
273 143 50       321 if ($path eq $prev_path)
274             {
275 0         0 carp "Danger! Found multiple rows with path <$path>";
276             }
277             else
278             {
279 143         226 $prev_path = $path;
280             }
281              
282 143         174 my %data = map { $_ => shift @{$row} } @{$column_names};
  423         513  
  423         1719  
  143         287  
283 143         1062 my $child = DBIx::Tree::MaterializedPath::Node->new($root,
284             {data => \%data, ignore_empty_hash => $ignore_empty_hash});
285              
286 143         549 $node_children = [];
287 143         346 push @{$nodes}, {node => $child, children => $node_children};
  143         492  
288 143         198 ${$num_nodes_ref}++;
  143         467  
289             }
290             }
291              
292 47         252 return;
293             }
294              
295             =head2 has_nodes
296              
297             $subtree_data->has_nodes()
298              
299             Return true if the data structure contains any nodes.
300              
301             =cut
302              
303             sub has_nodes
304             {
305 7     7 1 1115 my ($self) = @_;
306 7         39 return $self->{_has_nodes};
307             }
308              
309             =head2 num_nodes
310              
311             $subtree_data->num_nodes()
312              
313             Return the number of nodes in the data structure.
314              
315             =cut
316              
317             sub num_nodes
318             {
319 3     3 1 7 my ($self) = @_;
320 3         16 return $self->{_num_nodes};
321             }
322              
323             =head2 traverse
324              
325             $subtree_data->traverse( $coderef, $optional_context )
326              
327             Given a coderef, traverse down the data structure in leftmost
328             depth-first order and apply the coderef at each node.
329              
330             The first argument to the I<$coderef> will be the node being
331             traversed. The second argument to the I<$coderef> will be that
332             node's parent.
333              
334             If supplied, I<$context> will be the third argument to the
335             coderef. I<$context> can be a reference to a data structure that
336             can allow information to be carried along from node to node while
337             traversing the tree.
338              
339             E.g. to count the number of descendants:
340              
341             my $context = {count => 0};
342             my $coderef = sub {
343             my ($node, $parent, $context) = @_;
344             $context->{count}++;
345             };
346              
347             my $descendants = $node->get_descendants();
348             $descendants->traverse($coderef, $context);
349              
350             print "The node has $context->{count} descendants.\n";
351              
352             Note that you may be able to use closure variables instead of
353             passing them along in I<$context>:
354              
355             my $count = 0;
356             my $coderef = sub {
357             my ($node, $parent) = @_;
358             $count++;
359             };
360              
361             my $descendants = $node->get_descendants();
362             $descendants->traverse($coderef, $context);
363              
364             print "The node has $count descendants.\n";
365              
366             =cut
367              
368             sub traverse
369             {
370 33     33 1 2564 my ($self, $coderef, $context) = @_;
371              
372 33 100       280 croak 'Missing coderef' unless $coderef;
373 32 100       222 croak 'Invalid coderef' unless ref($coderef) eq 'CODE';
374              
375 31 100       273 return unless $self->{_has_nodes};
376 26         117 $self->_traverse($self->{_node}, $self->{_descendants}, $coderef, $context);
377              
378 26         107 return;
379             }
380              
381             sub _traverse
382             {
383 67     67   134 my ($self, $parent, $descendants, $coderef, $context) = @_;
384              
385 67         93 foreach my $child (@{$descendants})
  67         189  
386             {
387 140         244 my $node = $child->{node};
388 140         420 $coderef->($node, $parent, $context);
389              
390 140         8191 my $children = $child->{children};
391 140 100       170 if (@{$children})
  140         490  
392             {
393 41         146 $self->_traverse($node, $children, $coderef, $context);
394             }
395             }
396              
397 67         186 return;
398             }
399              
400             ###################################################################
401              
402             1;
403              
404             __END__