File Coverage

blib/lib/IRC/Server/Tree.pm
Criterion Covered Total %
statement 136 140 97.1
branch 35 52 67.3
condition 15 28 53.5
subroutine 19 19 100.0
pod 13 13 100.0
total 218 252 86.5


line stmt bran cond sub pod time code
1             package IRC::Server::Tree;
2             our $VERSION = '0.05';
3              
4             ## Array-type object representing a network map.
5              
6 2     2   42853 use strictures 1;
  2         1338  
  2         60  
7 2     2   95 use Carp;
  2         4  
  2         173  
8              
9 2     2   10 use Scalar::Util 'blessed';
  2         7  
  2         189  
10 2     2   1939 use Storable 'dclone' ;
  2         12434  
  2         3119  
11              
12             sub new {
13 9     9 1 653 my $class = shift;
14              
15 9         12 my $self;
16              
17 9 100       30 BUILD: {
18 9         13 last BUILD unless @_;
19              
20 4 50       13 if (@_ > 1) {
21             ## Got a tree as a list
22             ## (or the user did something dumb and will regret it later)
23 0         0 $self = [ @_ ];
24             last BUILD
25 0         0 }
26              
27 4         6 my ($opt) = @_;
28              
29 4 100 66     28 if (blessed $opt && $opt->isa('IRC::Server::Tree') ) {
30             ## Got a Tree. Clone it to break refs.
31 1         142 $self = dclone($opt);
32             last BUILD
33 1         4 }
34              
35 3 50       11 if (ref $opt eq 'ARRAY') {
36             ## Got a Tree as a raw ARRAY.
37             ## No clone; keep refs to allow darker forms of magic
38 3         3 $self = $opt;
39             last BUILD
40 3         8 }
41              
42             }
43              
44 9 100       25 $self = [] unless $self;
45 9         53 bless $self, $class
46             }
47              
48             sub add_node_to_parent_ref {
49 18     18 1 32 my ($self, $parent_ref, $name, $arrayref) = @_;
50              
51 18 100       60 $arrayref = [@$arrayref] if blessed $arrayref;
52              
53 18   100     58 push @$parent_ref, $name, ($arrayref||=[]);
54              
55 18         81 $arrayref
56             }
57              
58             sub add_node_to_top {
59 5     5 1 638 my ($self, $name, $arrayref) = @_;
60              
61 5         16 $self->add_node_to_parent_ref( $self, $name, $arrayref )
62             }
63              
64             sub add_node_to_name {
65 13     13 1 484 my ($self, $parent_name, $name, $arrayref) = @_;
66              
67             ## Can be passed $self like add_node_to_parent_ref
68             ## Should just use add_node_to_top instead, though
69 13 50       45 if ($parent_name eq $self) {
70 0         0 return $self->add_node_to_top($name, $arrayref)
71             }
72              
73 13 50 0     32 my $index_route =
74             $self->trace_indexes($parent_name)
75             or carp "Cannot add node to nonexistant parent $parent_name"
76             and return;
77              
78 13         18 my $cur_ref = $self;
79              
80 13         34 while (my $idx = shift @$index_route) {
81 16         123 $cur_ref = $cur_ref->[$idx]
82             }
83              
84             ## Now in the ref belonging to our named parent.
85 13   100     54 $self->add_node_to_parent_ref($cur_ref, $name, $arrayref || [] )
86             }
87              
88             sub __t_add_to_hash {
89 13     13   49 my ($parent_hash, $name, $node_ref) = @_;
90              
91 13 50       37 $parent_hash->{$name} = {}
92             unless exists $parent_hash->{$name};
93              
94 13         22 my @list = @$node_ref;
95              
96 13         57 while (my ($nextname, $nextref) = splice @list, 0, 2 ) {
97 9         29 __t_add_to_hash( $parent_hash->{$name}, $nextname, $nextref )
98             }
99             }
100              
101             sub as_hash {
102 2     2 1 6 my ($self, $parent_ref) = @_;
103              
104 2 50       9 $parent_ref = $self unless defined $parent_ref;
105              
106 2         5 my $mapref = {};
107              
108 2         7 my @list = @$parent_ref;
109              
110 2         9 while (my ($name, $node_ref) = splice @list, 0, 2 ) {
111 4         9 __t_add_to_hash( $mapref, $name, $node_ref )
112             }
113              
114             $mapref
115 2         25 }
116              
117             sub as_list {
118 1     1 1 2 my ($self, $parent_ref) = @_;
119 1   33     9 $parent_ref ||= $self;
120 1         1 @{ $parent_ref }
  1         4  
121             }
122              
123             sub child_node_for {
124 3     3 1 5 my ($self, $server_name, $parent_ref) = @_;
125              
126 3 50       9 $parent_ref = $self unless defined $parent_ref;
127              
128 3 50       7 my $index_route =
129             $self->trace_indexes($server_name, $parent_ref)
130             or return;
131              
132             ## Recurse the list indexes.
133 3         4 my $cur_ref = $parent_ref;
134              
135 3         8 while (my $idx = shift @$index_route) {
136 6         14 $cur_ref = $cur_ref->[$idx]
137             }
138              
139             $cur_ref
140 3         10 }
141              
142             sub del_node_by_name {
143 4     4 1 8 my ($self, $name, $parent_ref) = @_;
144              
145             ## Returns deleted node.
146              
147 4 50 0     11 my $index_route =
148             $self->trace_indexes($name, $parent_ref)
149             or carp "Cannot del nonexistant node $name"
150             and return;
151              
152 4         9 my $idx_for_ref = pop @$index_route;
153 4         47 my $idx_for_name = $idx_for_ref - 1;
154              
155 4   33     25 my $cur_ref = $parent_ref || $self;
156 4         11 while (my $idx = shift @$index_route) {
157 2         6 $cur_ref = $cur_ref->[$idx]
158             }
159              
160             ## Should now be in top-level container and have index values
161             ## for the name/ref that we're deleting.
162 4         15 my ($del_name, $del_ref) = splice @$cur_ref, $idx_for_name, 2;
163              
164 4         19 $del_ref
165             }
166              
167             sub names_beneath {
168 48     48 1 58 my ($self, $ref_or_name) = @_;
169              
170             ## Given either a ref (such as from del_node_by_name)
171             ## or a name (ref is retrived), get the names of
172             ## all the nodes in the tree under us.
173              
174 48         48 my $ref;
175 48 50       106 if (!$ref_or_name) {
    100          
176 0         0 $ref = $self
177             } elsif (ref $ref_or_name) {
178 47   33     82 $ref = $ref_or_name || $self
179             } else {
180 1         3 $ref = $self->child_node_for($ref_or_name)
181             }
182              
183 48 50       90 return unless $ref;
184              
185 48         73 my @list = @$ref;
186 48         41 my @names;
187              
188             ## Recurse and accumulate names.
189 48         127 while (my ($node_name, $node_ref) = splice @list, 0, 2) {
190 36         44 push(@names, $node_name);
191 36 50       34 push(@names, @{ $self->names_beneath($node_ref) || [] });
  36         71  
192             }
193              
194             \@names
195 48         201 }
196              
197             sub trace {
198 4     4 1 9 my ($self, $server_name, $parent_ref) = @_;
199              
200             ## A list of named hops to the target.
201             ## The last hop is the target's name.
202              
203 4 100       12 $parent_ref = $self unless defined $parent_ref;
204              
205 4 50       11 my $index_route =
206             $self->trace_indexes($server_name, $parent_ref)
207             or return;
208              
209 4         12 $self->path_by_indexes($index_route, $parent_ref)
210             }
211              
212             sub path_by_indexes {
213 12     12 1 20 my ($self, $index_array, $parent_ref) = @_;
214             ## Walk a trace_indexes array and retrieve names.
215             ## Used by ->trace()
216              
217 12         24 my @indexes = @$index_array;
218              
219 12         22 my @names;
220 12   66     39 my $cur_ref = $parent_ref || $self;
221 12         31 while (my $idx = shift @indexes) {
222 31         59 push @names, $cur_ref->[ $idx - 1 ];
223 31         81 $cur_ref = $cur_ref->[$idx];
224             }
225              
226             \@names
227 12         56 }
228              
229             sub trace_indexes {
230 31     31 1 43 my ($self, $server_name, $parent_ref) = @_;
231              
232             ## An example of breadth-first search.
233             ##
234             ## We explore each path in the current node, and as we find new paths,
235             ## we queue them to be explored after the current iteration.
236             ## (This is in contrast to depth-first techniques, where you recursively
237             ## explore each deeper reference as you hit it, with the path-so-far
238             ## included in the call, until you have the path desired.)
239             ##
240             ## This is useful for cases like an IRC server tree, where there is
241             ## an essentially arbitrary structure to the tree; any node may have
242             ## any arbitrary number of child nodes (ad infinitum) and we have no
243             ## actual hints as to the possible path.
244             ##
245             ## (Hmm. Considering running networked maze-solver races...)
246             ##
247             ## Defaults to operating on $self
248             ## Return indexes into arrays describing the path
249             ## Return value is the full list of indexes to get to the array
250             ## belonging to the named server
251             ## i.e.:
252             ## 1, 3, 1
253             ## $parent_ref->[1] is a ref belonging to an intermediate hop
254             ## $parent_ref->[1]->[3] is a ref belonging to an intermediate hop
255             ## $parent_ref->[1]->[3]->[1] is the ref belonging to the target hop
256             ## Subtracting one from an index will get you the NAME value.
257              
258             ## A start-point.
259 31   66     132 my @queue = ( PARENT => ($parent_ref || $self) );
260              
261             ## Our seen routes.
262 31         38 my %route;
263              
264 31         46 my $parent_idx = 0;
265 31         92 PARENT: while (my ($parent_name, $parent_ref) = splice @queue, 0, 2) {
266              
267 74 50       132 return [ $parent_idx+1 ] if $parent_name eq $server_name;
268              
269 74         119 my @leaf_list = @$parent_ref;
270 74         77 my $child_idx = 0;
271              
272 74         168 CHILD: while (my ($child_name, $child_ref) = splice @leaf_list, 0, 2) {
273              
274 101 50       195 unless ( $route{$child_name} ) {
275 101 100       919 $route{$child_name} =
276 101         101 [ @{ $route{$parent_name}||[] }, $child_idx+1 ];
277              
278 101 100       212 return \@{$route{$child_name}} if $child_name eq $server_name;
  30         181  
279              
280 71         100 push @queue, $child_name, $child_ref;
281             }
282              
283 71         206 $child_idx += 2;
284             } ## CHILD
285              
286 44         116 $parent_idx += 2;
287             } ## PARENT
288              
289             return
290 1         4 }
291              
292             sub print_map {
293 1     1 1 1496 my ($self, $parent_ref) = @_;
294              
295 1 50       4 $parent_ref = $self unless defined $parent_ref;
296              
297 1         2 my $indent = 1;
298              
299 1         1 my $recurse_print;
300             $recurse_print = sub {
301 7     7   9 my ($name, $ref) = @_;
302 7         18 my @nodes = @$ref;
303              
304 7 100 100     23 if ($indent == 1 || scalar @nodes) {
305 3         5 $name = "* $name";
306             } else {
307 4         7 $name = "` $name";
308             }
309              
310 7         9 print {*STDOUT} ( (' ' x $indent) . "$name\n" );
  7         25  
311              
312 7         24 while (my ($next_name, $next_ref) = splice @nodes, 0, 2) {
313 5         6 $indent += 3;
314 5         14 $recurse_print->($next_name, $next_ref);
315 5         13 $indent -= 3;
316             }
317 1         7 };
318              
319 1         3 my @list = @$parent_ref;
320 1 50       3 warn "No refs found\n" unless @list;
321 1         5 while (my ($parent_name, $parent_ref) = splice @list, 0, 2) {
322 2         3 $recurse_print->($parent_name, $parent_ref);
323 2         6 $indent = 1;
324             }
325              
326 1         4 return 1
327             }
328              
329             1;
330              
331             =pod
332              
333             =head1 NAME
334              
335             IRC::Server::Tree - Manipulate an IRC "spanning tree"
336              
337             =head1 SYNOPSIS
338              
339             ## Basic path-tracing usage:
340             my $tree = IRC::Server::Tree->new;
341              
342             $tree->add_node_to_top($_) for qw/ peerA peerB /;
343              
344             $tree->add_node_to_name('peerA', 'leafA');
345             $tree->add_node_to_name('peerA', 'leafB');
346              
347             $tree->add_node_to_name('peerB', 'hubA');
348             $tree->add_node_to_name('hubA', 'peerB');
349              
350             ## ARRAY of hop names between root and peerB:
351             my $hop_names = $tree->trace( 'peerB' );
352              
353             See L for a simpler and more specialized
354             interface to the tree.
355              
356             See the DESCRIPTION for a complete method list.
357              
358             =head1 DESCRIPTION
359              
360             This piece was split out of a pending project because it may prove
361             otherwise useful. See L for higher-level
362             (and simpler) methods pertaining to manipulation of an IRC network
363             specifically; a Network instance also provides an optional
364             memory-for-speed tradeoff via memoization of traced paths.
365              
366             IRC servers are linked to form a network.
367             An IRC network is defined as a 'spanning tree' per RFC1459; this module
368             is an array-type object representing such a tree, with convenient path
369             resolution methods for determining route "hops" and extending or shrinking
370             the tree.
371              
372             An IRC network tree is essentially unordered; any node can have any
373             number of child nodes, with the only rules being that:
374              
375             =over
376              
377             =item *
378              
379             The tree remains a tree (it is acyclic; there is only one route between
380             any two nodes, and no node has more than one parent)
381              
382             =item *
383              
384             No two nodes can share the same name.
385              
386             =back
387              
388             Currently, this module doesn't enforce the listed rules for performance
389             reasons, but things will break if you add non-uniquely-named nodes. Be
390             warned. In fact, this module doesn't sanity
391             check very much of anything; an L does much
392             more to validate the tree and passed arguments.
393              
394             A new Tree can be created from an existing Tree:
395              
396             my $new_tree = IRC::Server::Tree->new( $old_tree );
397              
398             In principle, the general structure of the tree is your average deep
399             array-of-arrays:
400              
401             $self => [
402             hubA => [
403             leafA => [],
404             leafB => [],
405             ],
406              
407             hubB => [
408             leafC => [],
409             leafD => [],
410             ],
411             ],
412              
413             The methods provided below can be used to manipulate the tree and
414             determine hops in a path to an arbitrary node using a breadth-first
415             search.
416              
417             Currently routes are not memoized; that's left to a higher layer or
418             subclass.
419              
420             =head2 new
421              
422             Create a new network tree:
423              
424             my $tree = IRC::Server::Tree->new;
425              
426             Create a new network tree from an old one or part of one (see
427             L and L):
428              
429             my $tree = IRC::Server::Tree->new( $old_tree );
430              
431             (Note that this will clone the old Tree object.)
432              
433             Optionally create a tree from an ARRAY, if you really know what
434             you're doing:
435              
436             my $tree = IRC::Server::Tree->new(
437             [
438             hubA => [
439             hubB => [
440             hubBleaf1 => [],
441             ],
442             leaf1 => [],
443             leaf2 => [],
444             ],
445             ],
446             );
447              
448             =head2 add_node_to_parent_ref
449              
450             ## Add empty node to parent ref:
451             $tree->add_node_to_parent_ref( $parent_ref, $new_name );
452             ## Add existing node to parent ref:
453             $tree->add_node_to_parent_ref( $parent_ref, $new_name, $new_ref );
454              
455             Adds an empty or preexisting node to a specified parent reference.
456              
457             Also see L, L
458              
459             =head2 add_node_to_top
460              
461             $tree->add_node_to_top( $new_name );
462             $tree->add_node_to_top( $new_name, $new_ref );
463              
464             Also see L, L
465              
466             =head2 add_node_to_name
467              
468             $tree->add_node_to_name( $parent_name, $name );
469             $tree->add_node_to_name( $parent_name, $name, $new_ref );
470              
471             Adds an empty or specified node to the specified parent name.
472              
473             For example:
474              
475             $tree->add_node_to_top( 'MyHub1' );
476             $tree->add_node_to_name( 'MyHub1', 'MyLeafA' );
477              
478             ## Existing nodes under our new node
479             my $new_node = [ 'MyLeafB' => [] ];
480             $tree->add_node_to_name( 'MyHub1', 'MyHub2', $new_node );
481              
482             =head2 as_hash
483              
484             my $hash_ref = $tree->as_hash;
485             my $hash_ref = $tree->as_hash( $parent_ref );
486              
487             Get a (possibly deep) HASH describing the state of the tree underneath
488             the specified parent reference, or the entire tree if none is specified.
489              
490             For example:
491              
492             my $hash_ref = $tree->as_hash( $self->child_node_for('MyHub1') );
493              
494             Also see L
495              
496             =head2 as_list
497              
498             my @tree = $tree->as_list;
499             my @tree = $tree->as_list( $parent_ref );
500              
501             Returns the tree in list format.
502              
503             Not useful for most purposes and may be removed.
504              
505             =head2 child_node_for
506              
507             my $child_node = $tree->child_node_for( $parent_name );
508             my $child_node = $tree->child_node_for( $parent_name, $start_ref );
509              
510             Finds and returns the named child node from the tree.
511              
512             Starts at the root of the tree or the specified parent reference.
513              
514             =head2 del_node_by_name
515              
516             $tree->del_node_by_name( $parent_name );
517             $tree->del_node_by_name( $parent_name, $start_ref );
518              
519             Finds and deletes the named child from the tree.
520              
521             Returns the deleted node.
522              
523             =head2 names_beneath
524              
525             my $names = $tree->names_beneath( $parent_name );
526             my $names = $tree->names_beneath( $parent_ref );
527              
528             Return an arrayref of all names in the tree beneath the specified parent
529             node.
530              
531             Takes either the name of a node in the tree or a reference to a node.
532              
533             =head2 path_by_indexes
534              
535             my $names = $tree->path_by_indexes( $index_route );
536             my $names = $tree->path_by_indexes( $index_route, $parent_ref );
537              
538             Given an array of index hops as retrieved by L, retrieve
539             the name for each hop.
540              
541             This is mostly used internally by L.
542              
543             =head2 print_map
544              
545             $tree->print_map;
546             $tree->print_map( $start_ref );
547              
548             Prints a visualization of the network map to STDOUT.
549              
550             =head2 trace
551              
552             my $names = $tree->trace( $parent_name );
553             my $names = $tree->trace( $parent_name, $start_ref );
554              
555             Returns an arrayref of the names of every hop in the path to the
556             specified parent name.
557              
558             Starts tracing from the root of the tree unless a parent node reference
559             is also specified.
560              
561             The last hop returned is the target's name.
562              
563             =head2 trace_indexes
564              
565             Primarily intended for internal use. This is the breadth-first search
566             that other methods use to find a node. There is nothing very useful you
567             can do with this externally except count hops; it is documented here to
568             show how path resolution works.
569              
570             Returns an arrayref consisting of the index of every hop taken to get to
571             the node reference belonging to the specified node name starting from
572             the root of the tree or the specified parent node reference.
573              
574             Given a network:
575              
576             hubA
577             leafA
578             leafB
579             hubB
580             leafC
581             leafD
582              
583             C<< trace_indexes(B<'leafD'>) >> would return:
584              
585             [ 1, 5, 1 ]
586              
587             These are the indexes into the node references (arrays) owned by each
588             hop, including the last hop. Retrieving their names requires
589             subtracting one from each index; L handles this.
590              
591             =head1 AUTHOR
592              
593             Jon Portnoy
594              
595             =cut