File Coverage

blib/lib/Graphviz/DSL/Graph.pm
Criterion Covered Total %
statement 219 263 83.2
branch 64 86 74.4
condition 8 14 57.1
subroutine 27 30 90.0
pod 0 13 0.0
total 318 406 78.3


line stmt bran cond sub pod time code
1             package Graphviz::DSL::Graph;
2 6     6   34 use strict;
  6         13  
  6         200  
3 6     6   32 use warnings;
  6         12  
  6         192  
4              
5 6     6   5497 use parent qw/Graphviz::DSL::Component/;
  6         2016  
  6         83  
6              
7 6     6   448 use Carp ();
  6         12  
  6         116  
8 6     6   31 use Scalar::Util qw/blessed/;
  6         20  
  6         936  
9              
10 6     6   3272 use Graphviz::DSL::Util qw/parse_id/;
  6         15  
  6         486  
11              
12             use overload (
13 7     7   1836 '""' => sub { $_[0]->as_string },
14 6         65 fallback => 1,
15 6     6   23620 );
  6         7627  
16              
17             sub new {
18 36     36 0 913 my ($class, %args) = @_;
19              
20 36   100     237 my $id = delete $args{id} || 'G';
21 36   100     194 my $type = delete $args{type} || 'digraph';
22 36   100     393 my $is_subgraph = delete $args{subgraph} || 0;
23              
24 36         593 bless {
25             id => $id,
26             type => $type,
27             edges => [],
28             nodes => [],
29             gnode_attrs => [],
30             gedge_attrs => [],
31             graph_attrs => [],
32             subgraphs => [],
33             ranks => [],
34             objects => [],
35             is_subgraph => $is_subgraph,
36             delayed => 0,
37             }, $class;
38             }
39              
40             sub add {
41 47     47 0 117 my ($self, @nodes_or_routes) = @_;
42              
43 47 100       143 if (scalar @nodes_or_routes == 1) {
44 10         38 $self->_add_one_node($nodes_or_routes[0]);
45 10         28 return;
46             }
47              
48 37         217 while (my ($start, $end) = splice @nodes_or_routes, 0, 2) {
49 41 100       100 unless (defined $end) {
50 1         4 $self->_add_one_node($start);
51 1         5 return;
52             }
53              
54 80 100       298 ($start, $end) = map {
55 40         63 ref($_) eq 'ARRAY' ? $_ : [$_];
56             } ($start, $end);
57              
58 40         205 for my $edge ( _product($start, $end) ) {
59 56         236 $self->edge($edge);
60             }
61             }
62             }
63              
64             sub _find_same_id_node {
65 102     102   164 my ($self, $id) = @_;
66              
67 102         127 for my $node (@{$self->{nodes}}) {
  102         318  
68 240 100       679 return $node if $node->id eq $id;
69             }
70              
71 101         313 return;
72             }
73              
74             sub _add_one_node {
75 11     11   25 my ($self, $node) = @_;
76              
77 11 100       37 if (!ref($node)) {
    50          
78 5         18 $self->node($node);
79             } elsif (ref $node eq 'ARRAY') {
80 6         10 $self->node($_) for @{$node};
  6         18516  
81             } else {
82 0         0 Carp::croak("First parameter should be Scalar or ArrayRef");
83             }
84 11         21 return;
85             }
86              
87             sub multi_route {
88 2     2 0 5 my ($self, $stuff) = @_;
89              
90 2 100       17 unless (ref $stuff eq 'HASH') {
91 1         254 Carp::croak("multi_route should take 'HashRef'");
92             }
93              
94 1         6 my @edges = _apply(undef, $stuff);
95 1         5 $self->add(@{$_}[0, 1]) for @edges;
  8         33  
96             }
97              
98             sub _apply {
99 7     7   13 my ($parent, $data) = @_;
100              
101 7         10 my @edges;
102 7         13 my $ref = ref $data;
103 7 100       25 if ($ref eq 'ARRAY') {
    100          
104 1         184 for my $child (@{$data}) {
  1         5  
105 2         14 push @edges, [$parent, $child];
106             }
107             } elsif ($ref eq 'HASH') {
108 3         6 while (my ($key, $value) = each %{$data}) {
  9         47  
109 6 100       17 push @edges, [$parent, $key] if defined $parent;
110 6         31 push @edges, _apply($key, $value);
111             }
112             } else {
113 3         8 push @edges, [$parent, $data];
114             }
115              
116 7         26 return @edges;
117             }
118              
119             sub _product {
120 40     40   69 my ($array_ref1, $array_ref2) = @_;
121              
122 40         50 my @products;
123 40         45 for my $a (@{$array_ref1}) {
  40         91  
124 43         52 for my $b (@{$array_ref2}) {
  43         80  
125 56         258 push @products, [$a, $b];
126             }
127             }
128              
129 40         118 return @products;
130             }
131              
132             sub node {
133 103     103 0 283 my ($self, $node_id, @args) = @_;
134              
135 103         198 my @attrs = _to_key_value_pair(@args);
136              
137 103         111 my @nodes;
138 103 100       200 if (ref $node_id eq 'Regexp') {
139 1         1 for my $node (@{$self->{nodes}}) {
  1         3  
140 4 100       11 $node->update_attributes(\@attrs) if $node->id =~ m{$node_id};
141             }
142             } else {
143 102 100       249 if (my $node = $self->_find_same_id_node($node_id)) {
144 1         8 $node->update_attributes(\@attrs);
145 1         6 return $node;
146             } else {
147 101         338 my ($id, $port, $compass) = parse_id($node_id);
148 101         482 my $node = Graphviz::DSL::Node->new(
149             id => $id,
150             port => $port,
151             compass => $compass,
152             attributes => \@attrs,
153             );
154 101         143 push @{$self->{nodes}}, $node;
  101         908  
155 101         116 push @{$self->{objects}}, $node;
  101         364  
156              
157 101         335 return $node;
158             }
159             }
160             }
161              
162             sub _to_key_value_pair {
163 163     163   325 my @args = @_;
164              
165 163         271 my @pairs;
166 163         506 while (my ($k, $v) = splice @args, 0, 2) {
167 12         50 push @pairs, [$k, $v];
168             }
169              
170 163         309 return @pairs;
171             }
172              
173             sub _create_node {
174 307     307   439 my ($self, $node_id, $registered) = @_;
175              
176 307         340 my $node;
177 307 100       491 if ($registered) {
178 77         168 $node = $self->node($node_id);
179             } else {
180 230         591 my ($id, $port, $compass) = parse_id($node_id);
181 230         846 $node = Graphviz::DSL::Node->new(
182             id => $id, port => $port, compass => $compass,
183             );
184             }
185              
186 307         799 return $node;
187             }
188              
189             sub _find_object {
190 230     230   302 my ($self, $id) = @_;
191              
192 230         261 for my $subgraph (@{$self->{subgraphs}}) {
  230         719  
193 0 0       0 if ($subgraph->id eq $id) {
194 0         0 $subgraph->{delayed} = 1;
195 0         0 return $subgraph;
196             }
197             }
198              
199 230         560 my $node = $self->_create_node($id, 0);
200 230         279 for my $obj (@{$self->{nodes}}) {
  230         682  
201 667 100       2579 if ($obj->equal_to($node)) {
202 76         389 return $obj;
203             }
204             }
205              
206 154         1042 return;
207             }
208              
209             sub edge {
210 60     60 0 108 my ($self, $id, @args) = @_;
211              
212 60         203 my @attrs = _to_key_value_pair(@args);
213              
214 60 50       175 unless (ref $id eq 'ARRAY') {
215 0         0 Carp::croak("First parameter of 'edge' should be ArrayRef");
216             }
217              
218 60         323 my @start_objs = $self->_match_objects($id->[0]);
219 60         149 my @end_objs = $self->_match_objects($id->[1]);
220              
221 60         78 my @edge_objs;
222 60         101 for my $start_obj (@start_objs) {
223 32         80 for my $end_obj (@end_objs) {
224 15         54 push @edge_objs, [$start_obj, $end_obj];
225             }
226             }
227              
228 60         70 my @update_edges;
229 60         82 for my $edge (@{$self->{edges}}) {
  60         137  
230 150         253 for my $edge_obj (@edge_objs) {
231 68         229 my $test_edge = Graphviz::DSL::Edge->new(
232             start => $edge_obj->[0],
233             end => $edge_obj->[1],
234             );
235 68 100       264 push @update_edges, $edge if $edge->equal_to($test_edge);
236             }
237             }
238              
239 60 100       153 if (@update_edges) {
240 3         6 for my $edge (@update_edges) {
241 5         57 $edge->update_attributes(\@attrs);
242             }
243             } else {
244 114         244 my ($start, $end) = map {
245 57         118 my $_id = $_;
246 114 100       225 $self->_find_object($_id) || $self->_create_node($_id, 1);
247 57         74 } @{$id};
248              
249 57         277 my $edge = Graphviz::DSL::Edge->new(
250             start => $start,
251             end => $end,
252             attributes => \@attrs,
253             );
254              
255 57         73 push @{$self->{edges}}, $edge;
  57         127  
256 57         75 push @{$self->{objects}}, $edge;
  57         485  
257             }
258             }
259              
260             sub _match_objects {
261 120     120   177 my ($self, $pattern) = @_;
262              
263 120         185 my @objects;
264 120 100       236 if (ref $pattern eq 'Regexp') {
265 4         5 for my $obj (@{$self->{nodes}}, @{$self->{subgraphs}}) {
  4         8  
  4         8  
266 28 100       66 if ($obj->id =~ m{$pattern}) {
267 9         11 push @objects, $obj;
268             }
269              
270 28 50       98 if (blessed $obj eq 'Graphviz::DSL::Graph') {
271 0         0 $obj->{delayed} = 1;
272             }
273             }
274              
275 4 50       14 if (scalar @objects == 0) {
276 0         0 Carp::carp("No objects are matched\n");
277             }
278             } else {
279 116 100       253 if (my $obj = $self->_find_object($pattern)) {
280 39         70 push @objects, $obj;
281             }
282             }
283              
284 120         465 return @objects;
285             }
286              
287             sub name {
288 1     1 0 3 my ($self, $name) = @_;
289 1         2 $self->{id} = $name;
290 1         5 return $self->{id};
291             }
292              
293             sub type {
294 3     3 0 7 my ($self, $type) = @_;
295              
296 3 100 66     31 unless ($type eq 'digraph' || $type eq 'graph') {
297 1         268 Carp::croak("'type' should be 'digraph' or 'graph'");
298             }
299              
300 2         6 $self->{type} = $type;
301 2         7 return $self->{type};
302             }
303              
304             sub save {
305 0     0 0 0 my ($self, %args) = @_;
306              
307 0         0 my $path = delete $args{path};
308 0         0 my $type = delete $args{type};
309 0   0     0 my $encoding = delete $args{encoding} || 'utf-8';
310              
311 0         0 my $dotfile = "${path}.dot";
312 0 0       0 open my $fh, '>', $dotfile or Carp::croak("Can't open $dotfile: $!");
313 0         0 print {$fh} Encode::encode($encoding, $self->as_string);
  0         0  
314 0         0 close $fh;
315              
316 0 0       0 if ($type) {
317 0         0 my $dot = File::Which::which('dot');
318 0 0       0 unless (defined $dot) {
319 0         0 Carp::carp("Cannot generate image. Please install Graphviz(dot command).");
320 0         0 return;
321             }
322              
323 0         0 my $output = "${path}.${type}";
324 0         0 my $cmd_str = sprintf "%s -T%s %s -o %s", $dot, $type, $dotfile, $output;
325 0         0 my @cmd = split /\s/, $cmd_str;
326              
327 0 0       0 system(@cmd) == 0 or Carp::croak("Failed command: '@cmd'");
328             }
329             }
330              
331             sub rank {
332 3     3 0 10 my ($self, $type, @nodes) = @_;
333              
334 3 100       11 unless (@nodes) {
335 1         199 Carp::croak("not specified nodes");
336             }
337              
338 2         9 my @types = qw/same min max source sink/;
339 2 100       6 unless ( grep { $type eq $_} @types) {
  10         27  
340 1         717 Carp::croak("type must match any of '@types'");
341             }
342              
343 1         3 push @{$self->{ranks}}, [$type, \@nodes];
  1         8  
344             }
345              
346             sub _build_attrs {
347 42     42   70 my ($attrs, $is_join) = @_;
348              
349 42 100       149 return '' unless @{$attrs};
  42         274  
350              
351 1 50       5 unless (defined $is_join) {
352 0         0 $is_join = 1;
353             }
354              
355 1         1 my @strs;
356 1         2 for my $attr (@{$attrs}) {
  1         3  
357 1         2 my ($k, $v) = @{$attr};
  1         3  
358 1         5 my $str = qq{$k="$v"};
359 1         4 $str =~ s{\n}{\\n}g;
360 1         3 push @strs, $str;
361             }
362              
363 1 50       7 if ($is_join) {
364 0         0 my $joined = join q{,}, @strs;
365 0         0 return "[${joined}]";
366             } else {
367 1         5 return \@strs;
368             }
369             }
370              
371             sub update_attrs {
372 4     4 0 14 my ($self, $attr_key, @args) = @_;
373              
374             OUTER:
375 4         22 while (my ($key, $val) = splice @args, 0, 2) {
376 7         11 for my $old_attr (@{$self->{$attr_key}}) {
  7         22  
377 3         5 my ($old_key, $old_val) = @{$old_attr};
  3         7  
378              
379 3 50       17 if ($key eq $old_key) {
380 0         0 $old_attr->[1] = $val;
381 0         0 next OUTER;
382             }
383             }
384              
385 7         13 push @{$self->{$attr_key}}, [$key, $val];
  7         46  
386             }
387             }
388              
389             my %print_func = (
390             'Graphviz::DSL::Graph' => sub {
391             my $graph = shift;
392             return if $graph->{delayed};
393              
394             my @lines = split /\n/, $graph->as_string;
395              
396             my @results;
397             for my $line (@lines) {
398             chomp $line;
399             push @results, " ${line}";
400             }
401             return @results;
402             },
403             'Graphviz::DSL::Edge' => sub {
404             my ($edge, $is_directed) = @_;
405             sprintf " %s%s;", $edge->as_string($is_directed), _build_attrs($edge->attributes);
406             },
407             'Graphviz::DSL::Node' => sub {
408             my $node = shift;
409             sprintf " %s%s;", $node->as_string, _build_attrs($node->attributes);
410             },
411             );
412              
413             sub as_string {
414 9     9 0 34 my $self = shift;
415              
416 9         15 my @result;
417 9 100       33 my $is_directed = $self->{type} eq 'digraph' ? 1 : 0;
418 9         15 my $indent = ' ';
419              
420 9 100       29 my $graph_type = $self->{is_subgraph} ? 'subgraph' : $self->{type};
421 9         61 push @result, sprintf "%s \"%s\" {", $graph_type, $self->{id};
422              
423 9 100       19 if (@{$self->{graph_attrs}}) {
  9         70  
424 1         4 my $graph_attrs_str = join ";\n$indent", @{_build_attrs($self->{graph_attrs}, 0)};
  1         5  
425 1         4 push @result, sprintf "%s%s;", $indent, $graph_attrs_str;
426             }
427              
428 9 50       59 if (@{$self->{gnode_attrs}}) {
  9         35  
429 0         0 my $gnode_attr_str = _build_attrs($self->{gnode_attrs});
430 0         0 push @result, sprintf "%snode%s;", $indent, $gnode_attr_str;
431             }
432              
433 9 50       14 if (@{$self->{gedge_attrs}}) {
  9         27  
434 0         0 my $gedge_attr_str = _build_attrs($self->{gedge_attrs});
435 0         0 push @result, sprintf "%sedge%s;", $indent, $gedge_attr_str;
436             }
437              
438 9         16 for my $object (@{$self->{objects}}) {
  9         27  
439 41         128 my $class = blessed $object;
440 41 50       102 Carp::croak("Invalid object") unless defined $class;
441 41         107 push @result, $print_func{$class}->($object, $is_directed);
442             }
443              
444 9         15 for my $rank ( @{$self->{ranks}} ) {
  9         27  
445 0         0 my ($type, $nodes) = @{$rank};
  0         0  
446              
447 0         0 my $node_str = join '; ', @{$nodes};
  0         0  
448 0         0 push @result, sprintf "%s{ rank=%s; %s; }", $indent, $type, $node_str;
449             }
450              
451 9         18 push @result, "}\n";
452 9         66 return join "\n", @result;
453             }
454              
455             sub equal_to {
456 0     0 0   my ($self, $obj) = @_;
457              
458 0 0 0       if (blessed $obj && $obj->isa('Graphviz::DSL::Graph')) {
459 0           return 0;
460             }
461              
462 0           return $self->{id} eq $obj->{id};
463             }
464              
465             # accessor
466 0     0 0   sub id { $_[0]->{id}; }
467              
468             1;