File Coverage

lib/UML/Class/Simple.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             # vim:set ft=perl ts=4 sw=4 et fdm=marker:
2              
3             package UML::Class::Simple;
4              
5 2     2   27777 use strict;
  2         7  
  2         84  
6 2     2   10 use warnings;
  2         3  
  2         75  
7 2     2   8 no warnings 'redefine';
  2         8  
  2         128  
8              
9             our $VERSION = '0.20';
10              
11             #use Smart::Comments;
12 2     2   11 use Carp qw(carp confess);
  2         4  
  2         190  
13 2     2   1213 use Class::Inspector;
  0            
  0            
14             use Devel::Peek ();
15             use File::Spec;
16             use IPC::Run3;
17             use List::MoreUtils 'any';
18             use Template;
19             use XML::LibXML ();
20              
21             require Exporter;
22             our @ISA = qw(Exporter);
23             our @EXPORT = qw(
24             classes_from_runtime classes_from_files
25             exclude_by_paths grep_by_paths
26             );
27              
28             my $tt = Template->new;
29             my $dot_template;
30              
31             sub classes_from_runtime {
32             my ($modules, $pattern) = @_;
33             $modules = [$modules] if $modules and !ref $modules;
34             $pattern = '' if !defined $pattern;
35             for (@$modules) {
36             eval "use $_;";
37             if ($@) { carp $@; return (); }
38             }
39             grep { /$pattern/ } _runtime_packages();
40             }
41              
42             sub _normalize_path ($) {
43             my $path = shift;
44             $path = File::Spec->rel2abs($path);
45             if (File::Spec->case_tolerant()) {
46             $path = lc($path);
47             } else {
48             $path;
49             }
50             }
51              
52             sub exclude_by_paths ($@) {
53             my $rclasses = shift;
54             my @paths = map { _normalize_path($_) } @_;
55             my @res;
56             #_extend_INC();
57             for my $class (@$rclasses) {
58             #warn $class;
59             my $filename = Class::Inspector->resolved_filename($class);
60             #warn "[0] ", $filename, "\n";
61             if (!$filename && $INC{$class}) {
62             $filename = Class::Inspector->loaded_filename($class);
63             }
64             if (!$filename) { next; }
65             #warn "[1] ", $filename, "\n";
66             $filename = _normalize_path($filename);
67             #warn "[2] ", $filename, "\n";
68             #my $value = $INC{$key};
69             if (any { substr($filename, 0, length) eq $_ } @paths) {
70             #warn "!!! ignoring $filename\n";
71             next;
72             }
73             #warn "adding $filename <=> @paths\n";
74             push @res, $class;
75             }
76             @res;
77             }
78              
79             sub grep_by_paths ($@) {
80             my $rclasses = shift;
81             my @paths = map { _normalize_path($_) } @_;
82             my @res;
83             #_extend_INC();
84             for my $class (@$rclasses) {
85             my $filename = Class::Inspector->resolved_filename($class);
86             if (!$filename && $INC{$class}) {
87             $filename = Class::Inspector->loaded_filename($class);
88             }
89             if (!$filename) { next; }
90             $filename = _normalize_path($filename);
91             #my $value = $INC{$key};
92             if (any { substr($filename, 0, length) eq $_ } @paths) {
93             #warn "adding $filename <=> @paths\n";
94             push @res, $class;
95             next;
96             }
97             #warn "!!! ignoring $filename\n";
98             }
99             @res;
100             }
101              
102             sub _runtime_packages {
103             no strict 'refs';
104             my $pkg_name = shift || '::';
105             my $cache = shift || {};
106             return if $cache->{$pkg_name};
107             $cache->{$pkg_name} = 1;
108             for my $entry (keys %$pkg_name) {
109             next if $entry !~ /\:\:$/ or $entry eq 'main::';
110             my $subpkg_name = $pkg_name.$entry;
111             #warn $subpkg_name;
112             _runtime_packages($subpkg_name, $cache);
113             $cache->{$subpkg_name} = 1;
114             }
115             map { s/^::|::$//g; $_ } keys %$cache;
116             }
117              
118             sub classes_from_files {
119             require PPI;
120             my ($list, $pattern, $read_only) = @_;
121             $list = [$list] if $list and !ref $list;
122             $pattern = '' if !defined $pattern;
123             my @classes;
124             my $cache = {};
125             for my $file (@$list) {
126             _gen_paths($file, $cache);
127             my $doc = PPI::Document->new( $file );
128             if (!$doc) {
129             carp "warning: Can't parse $file: ", PPI::Document->errstr;
130             next;
131             }
132             my $res = $doc->find('PPI::Statement::Package');
133             next if !$res;
134             push @classes, map { $_->namespace } @$res;
135             _load_file($file) if !$read_only;
136             }
137             @classes = grep { /$pattern/ } @classes;
138             #@classes = sort @classes;
139             wantarray ? @classes : \@classes;
140             }
141              
142             sub _gen_paths {
143             my ($file, $cache) = @_;
144             $file =~ s{\\+}{/}g;
145             my $dir;
146             while ($file =~ m{(?x) \G .+? /+ }gc) {
147             $dir .= $&;
148             next if $cache->{$dir};
149             $cache->{$dir} = 1;
150             #warn "pushing ~~~ $dir\n";
151             unshift @INC, $dir;
152             }
153             }
154              
155             sub new {
156             my $class = ref $_[0] ? ref shift : shift;
157             my $rclasses = shift || [];
158             my $self = bless {
159             class_names => $rclasses,
160             node_color => '#f1e1f4',
161             display_inheritance => 1,
162             display_methods => 1,
163             }, $class;
164             $self->{inherited_methods} = 1;
165             my $options = shift;
166             if (ref($options) eq 'HASH') {
167             $self->{inherited_methods} = $options->{inherited_methods};
168             if (defined $options->{xmi_model}) {
169             $self->_xmi_load_model($options->{xmi_model});
170             }
171             }
172             #$self->_build_dom;
173             $self;
174             }
175              
176             sub size {
177             my $self = shift;
178             if (@_) {
179             my ($width, $height) = @_;
180             if (!$width || !$height || ($width . $height) !~ /^[\.\d]+$/) {
181             carp "invalid width and height";
182             return undef;
183             } else {
184             $self->{width} = $width;
185             $self->{height} = $height;
186             return 1;
187             }
188             } else {
189             return ($self->{width}, $self->{height});
190             }
191             }
192              
193             sub node_color {
194             my $self = shift;
195             if (@_) {
196             $self->{node_color} = shift;
197             } else {
198             $self->{node_color};
199             }
200             }
201              
202             sub dot_prog {
203             my $self = shift;
204             if (@_) {
205             my $cmd = shift;
206             can_run($cmd) or die "ERROR: The dot program ($cmd) cannot be found or be run.\n";
207             $self->{dot_prog} = $cmd;
208             } else {
209             $self->{dot_prog} || 'dot';
210             }
211             }
212              
213             # copied from IPC::Cmd. Copyright by IPC::Cmd's author.
214             sub can_run {
215             my $command = shift;
216              
217             # a lot of VMS executables have a symbol defined
218             # check those first
219             if ( $^O eq 'VMS' ) {
220             require VMS::DCLsym;
221             my $syms = VMS::DCLsym->new;
222             return $command if scalar $syms->getsym( uc $command );
223             }
224              
225             require Config;
226             require File::Spec;
227             require ExtUtils::MakeMaker;
228              
229             if( File::Spec->file_name_is_absolute($command) ) {
230             return MM->maybe_command($command);
231              
232             } else {
233             for my $dir (
234             (split /\Q$Config::Config{path_sep}\E/, $ENV{PATH}),
235             File::Spec->curdir
236             ) {
237             my $abs = File::Spec->catfile($dir, $command);
238             return $abs if $abs = MM->maybe_command($abs);
239             }
240             }
241             }
242              
243             sub _property {
244             my $self = shift;
245             my $property_name = shift;
246             if (@_) {
247             $self->{$property_name} = shift;
248             $self->_build_dom(1);
249             } else {
250             $self->{$property_name};
251              
252             }
253             }
254              
255             sub public_only {
256             my $self = shift;
257             $self->_property('public_only', @_);
258             }
259              
260             sub inherited_methods {
261             my $self = shift;
262             $self->_property('inherited_methods', @_);
263             }
264              
265             sub as_png {
266             my $self = shift;
267             $self->_as_image('png', @_);
268             }
269              
270             sub as_gif {
271             my $self = shift;
272             $self->_as_image('gif', @_);
273             }
274              
275             sub _as_image {
276             my ($self, $type, $fname) = @_;
277             my $dot = $self->as_dot;
278             #if ($fname eq 'fast00.png') {
279             #warn "==== $fname\n";
280             #warn $dot;
281             #use YAML::Syck;
282             #$self->_build_dom(1);
283             #warn Dump($self->as_dom);
284             #}
285             my @cmd = ($self->dot_prog(), '-T', $type);
286             #my @cmd = ('dot', '-T', $type);
287             if ($fname) {
288             push @cmd, '-o', $fname;
289             }
290             my ($img_data, $stderr);
291             my $success = run3 \@cmd, \$dot, \$img_data, \$stderr;
292             if ($stderr) {
293             if ($? == 0) {
294             carp $stderr;
295             } else {
296             Carp::croak $stderr;
297             }
298             }
299             if (!$fname) {
300             return $img_data;
301             }
302             }
303              
304             sub as_dom {
305             my $self = shift;
306             $self->_build_dom;
307             { classes => $self->{classes} };
308             }
309              
310             sub set_dom ($$) {
311             my $self = shift;
312             $self->{classes} = shift->{classes};
313             1;
314             }
315              
316             sub moose_roles ($) {
317             my $self = shift;
318             $self->{'moose_roles'} = shift;
319             }
320              
321             sub display_methods ($) {
322             my $self = shift;
323             $self->{'display_methods'} = shift;
324             }
325              
326             sub display_inheritance ($) {
327             my $self = shift;
328             $self->{'display_inheritance'} = shift;
329             }
330              
331             sub _build_dom {
332             my ($self, $force) = @_;
333             # avoid unnecessary evaluation:
334             return if $self->{classes} && !$force || !$self->{class_names};
335             #warn "HERE";
336             my @pkg = @{ $self->{class_names} };
337             my @classes;
338             $self->{classes} = \@classes;
339             my $public_only = $self->{public_only};
340             my %visited; # used to eliminate potential repetitions
341             for my $pkg (@pkg) {
342             #warn $pkg;
343             $pkg =~ s/::::/::/g;
344             if ($visited{$pkg}) { next; }
345             $visited{$pkg} = 1;
346              
347             if (!Class::Inspector->loaded($pkg)) {
348             #my $pmfile = Class::Inspector->filename($pkg);
349             #warn $pmfile;
350             #if ($pmfile) {
351             # if (! _load_file($pmfile)) {
352             # next;
353             # }
354             #} else { next }
355             next;
356             }
357             push @classes, {
358             name => $pkg, methods => [],
359             properties => [], subclasses => [],
360             };
361             my $from_class_accessor =
362             $pkg->isa('Class::Accessor') ||
363             $pkg->isa('Class::Accessor::Fast') ||
364             $pkg->isa('Class::Accessor::Grouped');
365             #accessor_name_for
366              
367             # If you want to gather only the functions defined in
368             # the current class only (w/o those inherited from ancestors),
369             # set inherited_methods property to false (default value is true).
370             my $methods = Class::Inspector->methods($pkg, 'expanded');
371             if ($methods and ref($methods) eq 'ARRAY') {
372             if ($from_class_accessor) {
373             my $i = 0;
374             my %functions = map { $_->[2] => $i++ } @$methods; # create hash from array
375             ### %functions
376             #my @accessors = map { /^_(.*)_accessor$/; $1 } keys %functions;
377             ### @accessors
378             my $use_best_practice = delete $functions{'accessor_name_for'} && delete $functions{'mutator_name_for'};
379             my %accessors;
380             foreach my $meth (keys %functions) {
381             next unless $meth;
382             if ($meth =~ /^_(.*)_accessor$/) {
383             my $accessor = $1;
384             if (exists $functions{$accessor}) {
385             if ($self->{inherited_methods} or
386             $methods->[$functions{$accessor}]->[1] eq $pkg) {
387             push @{ $classes[-1]->{properties} }, $accessor;
388             }
389             delete $functions{$accessor};
390             delete $functions{"_${accessor}_accessor"};
391             #push @{ $classes[-1]->{properties} }, $accessor;
392             }
393             next;
394             }
395             if ($use_best_practice) {
396             if ($meth =~ /^(?:get|set)_(.+)/) {
397             my $accessor = $1;
398             delete $functions{$meth};
399             if (!$accessors{$accessor}) {
400             #push @{ $classes[-1]->{properties} }, $accessor;
401             if ($self->{inherited_methods} or
402             $methods->[$functions{$accessor}]->[1] eq $pkg) {
403             push @{ $classes[-1]->{properties} }, $accessor;
404             }
405             $accessors{$accessor} = 1;
406             }
407             }
408             }
409             }
410             @$methods = grep { exists $functions{$_->[2]} } @$methods;
411             }
412             @{ $classes[-1]->{properties} } = sort @{ $classes[-1]->{properties} };
413              
414             foreach my $method (@$methods) {
415             next if $method->[1] ne $pkg;
416             if (! $self->{inherited_methods}) {
417             my $source_name = Devel::Peek::CvGV($method->[3]);
418             $source_name =~ s/^\*//;
419             next if $method->[0] ne $source_name;
420             }
421             $method = $method->[2];
422             next if $public_only && $method =~ /^_/o;
423             push @{$classes[-1]->{methods}}, $method;
424             }
425             }
426              
427              
428              
429             my $subclasses = Class::Inspector->subclasses($pkg);
430             if ($subclasses) {
431             no strict 'refs';
432             my @child = grep {
433             #warn "!!!! ", join ' ', @{"${_}::ISA"};
434             any { $_ eq $pkg } @{"${_}::ISA"};
435             } @$subclasses;
436              
437             if (@child) {
438             $classes[-1]->{subclasses} = \@child;
439             }
440             }
441              
442             if (Class::Inspector->function_exists($pkg, 'meta')) {
443             # at least Class::MOP
444             my $meta = $pkg->meta();
445             if ($meta->can('consumers')) {
446             # Something like Moose::Meta::Role
447             my @consumers = $meta->consumers();
448             if (@consumers) {
449             $classes[-1]->{'consumers'} = [ @consumers ];
450             }
451             }
452             }
453             }
454             #warn "@classes";
455             }
456              
457             sub _load_file ($) {
458             my $file = shift;
459             my $path = _normalize_path($file);
460             #warn "!!! >>>> $path\n";
461             if ( any {
462             #warn "<<<<< ", _normalize_path($_), "\n";
463             $path eq _normalize_path($_);
464             } values %INC ) {
465             #carp "!!! Caught duplicate module files: $file ($path)";
466             return 1;
467             }
468             #my @a = values %INC;
469             #warn "\n@a\n";
470             #warn "!!! Loading $path...\n";
471             eval {
472             require $path;
473             };
474             carp $@ if $@;
475             !$@;
476             }
477              
478             sub _xmi_get_new_id {
479             my $self = shift;
480             return 'xmi.' . $self->{_xmi}->{_id_counter}++;
481             }
482              
483             sub _xmi_create_inheritance {
484             my ($self, $class, $subclass_name) = @_;
485             my $child_id = $self->{_xmi}->{_name2id}->{$subclass_name};
486             my $id = $self->_xmi_get_new_id();
487              
488             my $element = XML::LibXML::Element->new('UML:Generalization');
489             $self->{_xmi}->{_classes_root}->appendChild($element);
490             $self->_xmi_set_default_attribute($element, 'isSpecification', 'false');
491             $element->setAttribute('xmi.id', $id);
492              
493             my $child = XML::LibXML::Element->new('UML:Generalization.child');
494             $element->appendChild($child);
495             my $child_xml_class = XML::LibXML::Element->new('UML:Class');
496             $child->appendChild($child_xml_class);
497             $child_xml_class->setAttribute('xmi.idref', $child_id);
498              
499             my $parent = XML::LibXML::Element->new('UML:Generalization.parent');
500             $element->appendChild($parent);
501             $child_xml_class = XML::LibXML::Element->new('UML:Class');
502             $parent->appendChild($child_xml_class);
503             $child_xml_class->setAttribute('xmi.idref', $class->{xmi_id});
504              
505             my $xml_class = $self->{_xmi}->{_classes_hash}->{$subclass_name};
506             return unless defined $xml_class;
507             my $generalization = XML::LibXML::Element->new('UML:Generalization');
508             $generalization->setAttribute('xmi.idref', $id);
509             my $generalizableElement = XML::LibXML::Element->new('UML:GeneralizableElement.generalization');
510             $generalizableElement->appendChild($generalization);
511             $xml_class->appendChild($generalizableElement);
512             }
513              
514             sub _xmi_write_method {
515             my ($self, $parent_node, $class, $method) = @_;
516              
517             my $id = $self->_xmi_get_new_id();
518             my $visibility = 'public';
519             $visibility = 'private' if substr($method, 0, 1) eq '_';
520             my $ownerScope = 'instance';
521             $ownerScope = 'classifier' if $method =~ /^[A-Z]/o;
522              
523             my $xml_method = $self->_xmi_add_element($parent_node, 'UML:Operation', $method);
524              
525             $xml_method->setAttribute('xmi.id', $id);
526             $xml_method->setAttribute('visibility', $visibility);
527             $xml_method->setAttribute('ownerScope', $ownerScope);
528             $self->_xmi_set_default_attribute($xml_method, 'concurrency', 'sequential');
529             $self->_xmi_set_default_attribute($xml_method, $_, 'false') foreach qw(isSpecification isQuery isRoot isLeaf isAbstract);
530             }
531              
532             sub _xmi_write_class {
533             my ($self, $class) = @_;
534              
535             my $xml_class = $self->_xmi_add_element($self->{_xmi}->{_classes_root}, 'UML:Class', $class->{name});
536             $self->{_xmi}->{_classes_hash}->{$class->{name}} = $xml_class;
537             $xml_class->setAttribute('xmi.id', $class->{xmi_id});
538             $xml_class->setAttribute('visibility', 'public');
539             $self->_xmi_set_default_attribute($xml_class, $_, 'false') foreach qw(isSpecification isRoot isLeaf isAbstract isActive);
540              
541             my $uml_classifier = XML::LibXML::Element->new('UML:Classifier.feature');
542             $xml_class->appendChild($uml_classifier);
543              
544             $self->_xmi_write_method($uml_classifier, $class, $_) foreach @{$class->{methods}};
545             $self->_xmi_create_inheritance($class, $_) foreach @{$class->{subclasses}};
546             }
547              
548             sub _xmi_set_id {
549             my ($self, $class) = @_;
550             $class->{xmi_id} = $self->_xmi_get_new_id();
551             $self->{_xmi}->{_name2id}->{$class->{name}} = $class->{xmi_id};
552             }
553              
554             sub _xmi_add_element {
555             my ($self, $parent, $class, $name) = @_;
556             my $node;
557             if (defined $name) {
558             foreach $node ($parent->getElementsByTagName($class)) {
559             if ($node->getAttribute('name') eq $name) {
560             return $node;
561             }
562             }
563             }
564             $node = $self->{_xmi}->{_document}->createElement($class);
565             $node->setAttribute('name', $name);
566             $parent->appendChild($node);
567             return $node;
568             }
569              
570             sub _xmi_set_default_attribute {
571             my ($self, $node, $name, $value) = @_;
572             return if defined $node->getAttribute($name);
573             $node->setAttribute($name, $value);
574             }
575              
576             sub _xmi_load_model {
577             my ($self, $fname) = @_;
578             $self->{_xmi}->{_document} = XML::LibXML->new()->parse_file($fname);
579             }
580              
581             sub _xmi_init_xml {
582             my ($self, $fname) = @_;
583             unless (defined $self->{_xmi}->{_document}) {
584             $self->{_xmi}->{_document} = XML::LibXML::Document->new('1.0', 'UTF-8');
585             }
586             my $doc = $self->{_xmi}->{_document};
587              
588             my $xmi_root = $doc->createElement('XMI');
589             $xmi_root->setAttribute('xmi.version', '1.2');
590             $xmi_root->setAttribute('xmlns:UML', 'org.omg.xmi.namespace.UML');
591             my $generate_time = POSIX::asctime(localtime(time()));
592             chomp($generate_time);
593             $xmi_root->setAttribute('timestamp', $generate_time);
594             $doc->setDocumentElement($xmi_root);
595              
596             my $xmi_content = $doc->createElement('XMI.content');
597             $xmi_root->appendChild($xmi_content);
598              
599             my $uml_model = $self->_xmi_add_element($xmi_content, 'UML:Model', $fname || '');
600             $uml_model->setAttribute('xmi.id', $self->_xmi_get_new_id());
601             $self->_xmi_set_default_attribute($uml_model, $_, 'false') foreach qw(isSpecification isRoot isLeaf isAbstract);
602              
603             $self->{_xmi}->{_classes_root} = $doc->createElement('UML:Namespace.ownedElement');
604             $uml_model->appendChild($self->{_xmi}->{_classes_root});
605              
606             return $doc;
607             }
608              
609             sub as_xmi {
610             my ($self, $fname) = @_;
611             $self->_build_dom;
612             $self->{_xmi} ||= {};
613             $self->{_xmi}->{_id_counter} = 1;
614             $self->{_xmi}->{_name2id} = {};
615             $self->_xmi_set_id($_) foreach @{$self->{classes}};
616             my $doc = $self->_xmi_init_xml($fname);
617             $self->_xmi_write_class($_) foreach @{$self->{classes}};
618             if ($fname) {
619             $doc->toFile($fname, 2);
620             } else {
621             return $doc;
622             }
623             }
624              
625             sub as_dot {
626             my ($self, $fname) = @_;
627             $self->_build_dom;
628             if ($fname) {
629             $tt->process(\$dot_template, $self, $fname)
630             || carp $tt->error();
631             } else {
632             my $dot;
633             $tt->process(\$dot_template, $self, \$dot)
634             || carp $tt->error();
635             $dot;
636             }
637             }
638              
639             sub set_dot ($$) {
640             my $self = shift;
641             $self->{dot} = shift;
642             }
643              
644             $dot_template = <<'_EOC_';
645             digraph uml_class_diagram {
646             [%- IF width && height %]
647             size="[% width %],[% height %]";
648             [%- END %]
649             node [shape=record, style="filled"];
650             edge [color=red, dir=none];
651              
652             [%- name2id = {} %]
653             [%- id = 1 %]
654             [%- FOREACH class = classes %]
655             [%- name = class.name %]
656             [%- name2id.$name = id %]
657             class_[% id %] [shape=plaintext, style="", label=<
658            
659            
[% name %]
660            
661            
662            
663            
664             [% IF class.properties.size > 0 %]
665             [%- FOREACH property = class.properties %]
666             [%- property.match("^_") ? "-" : "+" %]
667              
668             [%- END %][% END %]
669            
670             [%- FOREACH property = class.properties %]
671             [%- property %]
672              
673             [%- END %]
674            
675            
676            
677            
678            
679            
680             [%- IF display_methods %]
681            
682            
683             [% IF class.methods.size > 0 %]
684             [%- FOREACH method = class.methods %]
685             [%- method.match("^_") ? "-" : "+" %]
686              
687             [%- END %][% END %]
688            
689             [%- FOREACH method = class.methods %]
690             [%- method %]
691              
692             [%- END %]
693            
694            
695             [%- END %]
696            
697            
698            
>];
699             [%- id = id + 1 %]
700             [% END %]
701             [%- class_id = id %]
702              
703             [%- first = 1 %]
704             [%- id = 0 %]
705             [%- IF display_inheritance %]
706             [%- FOREACH class = classes %]
707             [%- id = id + 1 %]
708             [%- super = class.name %]
709             [%- NEXT IF !class.subclasses.size -%]
710              
711             [%- IF first -%]
712             node [shape="triangle", fillcolor=yellow, height=0.3, width=0.3];
713             [%- first = 0 %]
714             [%- END -%]
715              
716             angle_[% id %] [label=""];
717              
718             [%- super_id = name2id.$super %]
719             class_[% super_id %]:methods -> angle_[% id %]
720              
721             [%- FOREACH child = class.subclasses %]
722             [%- child_id = name2id.$child %]
723             [%- IF !child_id %]
724             class_[% class_id %] [shape=record, label="[% child %]" fillcolor="#f1e1f4", style="filled"];
725             angle_[% id %] -> class_[% class_id %]
726             [%- class_id = class_id + 1 %]
727             [%- ELSE %]
728             angle_[% id %] -> class_[% child_id %]:title
729             [%- END %]
730             [%- END %]
731             [%- END %]
732             [%- END %]
733              
734             [%- IF moose_roles %]
735             [%- first = 1 %]
736             edge [color=blue, dir=none];
737             [%- FOREACH class = classes %]
738             [%- id = id + 1 %]
739             [%- NEXT IF !class.consumers.size -%]
740             [%- role = class.name %]
741             [%- role_id = name2id.$role %]
742             [%- IF first %]
743             node [shape="triangle", fillcolor=orange, height=0.3, width=0.3];
744             [%- first = 0 %]
745             [%- END %]
746              
747             angle_[% id %] [label=""];
748             class_[% role_id %]:methods -> angle_[% id %]
749              
750             [%- FOREACH consumer = class.consumers %]
751             [%- consumer_id = name2id.$consumer %]
752             angle_[% id %] -> class_[% consumer_id %]:title
753             [%- END %]
754             [%- END %]
755             [%- END %]
756              
757             }
758             _EOC_
759              
760             1;
761             __END__