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   18935 use strict;
  2         3  
  2         48  
6 2     2   5 use warnings;
  2         3  
  2         56  
7 2     2   6 no warnings 'redefine';
  2         5  
  2         85  
8              
9             our $VERSION = '0.22';
10              
11             #use Smart::Comments;
12 2     2   6 use Carp qw(carp confess);
  2         2  
  2         127  
13 2     2   1063 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_svg {
276             my $self = shift;
277             $self->_as_image('svg', @_);
278             }
279              
280             sub _as_image {
281             my ($self, $type, $fname) = @_;
282             my $dot = $self->as_dot;
283             #if ($fname eq 'fast00.png') {
284             #warn "==== $fname\n";
285             #warn $dot;
286             #use YAML::Syck;
287             #$self->_build_dom(1);
288             #warn Dump($self->as_dom);
289             #}
290             my @cmd = ($self->dot_prog(), '-T', $type);
291             #my @cmd = ('dot', '-T', $type);
292             if ($fname) {
293             push @cmd, '-o', $fname;
294             }
295             my ($img_data, $stderr);
296             my $success = run3 \@cmd, \$dot, \$img_data, \$stderr;
297             if ($stderr) {
298             if ($? == 0) {
299             carp $stderr;
300             } else {
301             Carp::croak $stderr;
302             }
303             }
304             if (!$fname) {
305             return $img_data;
306             }
307             }
308              
309             sub as_dom {
310             my $self = shift;
311             $self->_build_dom;
312             { classes => $self->{classes} };
313             }
314              
315             sub set_dom ($$) {
316             my $self = shift;
317             $self->{classes} = shift->{classes};
318             1;
319             }
320              
321             sub moose_roles ($) {
322             my $self = shift;
323             $self->{'moose_roles'} = shift;
324             }
325              
326             sub display_methods ($) {
327             my $self = shift;
328             $self->{'display_methods'} = shift;
329             }
330              
331             sub display_inheritance ($) {
332             my $self = shift;
333             $self->{'display_inheritance'} = shift;
334             }
335              
336             sub _build_dom {
337             my ($self, $force) = @_;
338             # avoid unnecessary evaluation:
339             return if $self->{classes} && !$force || !$self->{class_names};
340             #warn "HERE";
341             my @pkg = @{ $self->{class_names} };
342             my @classes;
343             $self->{classes} = \@classes;
344             my $public_only = $self->{public_only};
345             my %visited; # used to eliminate potential repetitions
346             for my $pkg (@pkg) {
347             #warn $pkg;
348             $pkg =~ s/::::/::/g;
349             if ($visited{$pkg}) { next; }
350             $visited{$pkg} = 1;
351              
352             if (!Class::Inspector->loaded($pkg)) {
353             #my $pmfile = Class::Inspector->filename($pkg);
354             #warn $pmfile;
355             #if ($pmfile) {
356             # if (! _load_file($pmfile)) {
357             # next;
358             # }
359             #} else { next }
360             next;
361             }
362             push @classes, {
363             name => $pkg, methods => [],
364             properties => [], subclasses => [],
365             };
366             my $from_class_accessor =
367             $pkg->isa('Class::Accessor') ||
368             $pkg->isa('Class::Accessor::Fast') ||
369             $pkg->isa('Class::Accessor::Grouped');
370             #accessor_name_for
371              
372             # If you want to gather only the functions defined in
373             # the current class only (w/o those inherited from ancestors),
374             # set inherited_methods property to false (default value is true).
375             my $methods = Class::Inspector->methods($pkg, 'expanded');
376             if ($methods and ref($methods) eq 'ARRAY') {
377             if ($from_class_accessor) {
378             my $i = 0;
379             my %functions = map { $_->[2] => $i++ } @$methods; # create hash from array
380             ### %functions
381             #my @accessors = map { /^_(.*)_accessor$/; $1 } keys %functions;
382             ### @accessors
383             my $use_best_practice = delete $functions{'accessor_name_for'} && delete $functions{'mutator_name_for'};
384             my %accessors;
385             foreach my $meth (keys %functions) {
386             next unless $meth;
387             if ($meth =~ /^_(.*)_accessor$/) {
388             my $accessor = $1;
389             if (exists $functions{$accessor}) {
390             if ($self->{inherited_methods} or
391             $methods->[$functions{$accessor}]->[1] eq $pkg) {
392             push @{ $classes[-1]->{properties} }, $accessor;
393             }
394             delete $functions{$accessor};
395             delete $functions{"_${accessor}_accessor"};
396             #push @{ $classes[-1]->{properties} }, $accessor;
397             }
398             next;
399             }
400             if ($use_best_practice) {
401             if ($meth =~ /^(?:get|set)_(.+)/) {
402             my $accessor = $1;
403             delete $functions{$meth};
404             if (!$accessors{$accessor}) {
405             #push @{ $classes[-1]->{properties} }, $accessor;
406             if ($self->{inherited_methods} or
407             $methods->[$functions{$accessor}]->[1] eq $pkg) {
408             push @{ $classes[-1]->{properties} }, $accessor;
409             }
410             $accessors{$accessor} = 1;
411             }
412             }
413             }
414             }
415             @$methods = grep { exists $functions{$_->[2]} } @$methods;
416             }
417             @{ $classes[-1]->{properties} } = sort @{ $classes[-1]->{properties} };
418              
419             foreach my $method (@$methods) {
420             next if $method->[1] ne $pkg;
421             if (! $self->{inherited_methods}) {
422             my $source_name = Devel::Peek::CvGV($method->[3]);
423             $source_name =~ s/^\*//;
424             next if $method->[0] ne $source_name;
425             }
426             $method = $method->[2];
427             next if $public_only && $method =~ /^_/o;
428             push @{$classes[-1]->{methods}}, $method;
429             }
430             }
431              
432              
433              
434             my $subclasses = Class::Inspector->subclasses($pkg);
435             if ($subclasses) {
436             no strict 'refs';
437             my @child = grep {
438             #warn "!!!! ", join ' ', @{"${_}::ISA"};
439             any { $_ eq $pkg } @{"${_}::ISA"};
440             } @$subclasses;
441              
442             if (@child) {
443             $classes[-1]->{subclasses} = \@child;
444             }
445             }
446              
447             if (Class::Inspector->function_exists($pkg, 'meta')) {
448             # at least Class::MOP
449             my $meta = $pkg->meta();
450             if ($meta->can('consumers')) {
451             # Something like Moose::Meta::Role
452             my @consumers = $meta->consumers();
453             if (@consumers) {
454             $classes[-1]->{'consumers'} = [ @consumers ];
455             }
456             }
457             }
458             }
459             #warn "@classes";
460             }
461              
462             sub _load_file ($) {
463             my $file = shift;
464             my $path = _normalize_path($file);
465             #warn "!!! >>>> $path\n";
466             if ( any {
467             #warn "<<<<< ", _normalize_path($_), "\n";
468             $path eq _normalize_path($_);
469             } values %INC ) {
470             #carp "!!! Caught duplicate module files: $file ($path)";
471             return 1;
472             }
473             #my @a = values %INC;
474             #warn "\n@a\n";
475             #warn "!!! Loading $path...\n";
476             eval {
477             require $path;
478             };
479             carp $@ if $@;
480             !$@;
481             }
482              
483             sub _xmi_get_new_id {
484             my $self = shift;
485             return 'xmi.' . $self->{_xmi}->{_id_counter}++;
486             }
487              
488             sub _xmi_create_inheritance {
489             my ($self, $class, $subclass_name) = @_;
490             my $child_id = $self->{_xmi}->{_name2id}->{$subclass_name};
491             my $id = $self->_xmi_get_new_id();
492              
493             my $element = XML::LibXML::Element->new('UML:Generalization');
494             $self->{_xmi}->{_classes_root}->appendChild($element);
495             $self->_xmi_set_default_attribute($element, 'isSpecification', 'false');
496             $element->setAttribute('xmi.id', $id);
497              
498             my $child = XML::LibXML::Element->new('UML:Generalization.child');
499             $element->appendChild($child);
500             my $child_xml_class = XML::LibXML::Element->new('UML:Class');
501             $child->appendChild($child_xml_class);
502             $child_xml_class->setAttribute('xmi.idref', $child_id);
503              
504             my $parent = XML::LibXML::Element->new('UML:Generalization.parent');
505             $element->appendChild($parent);
506             $child_xml_class = XML::LibXML::Element->new('UML:Class');
507             $parent->appendChild($child_xml_class);
508             $child_xml_class->setAttribute('xmi.idref', $class->{xmi_id});
509              
510             my $xml_class = $self->{_xmi}->{_classes_hash}->{$subclass_name};
511             return unless defined $xml_class;
512             my $generalization = XML::LibXML::Element->new('UML:Generalization');
513             $generalization->setAttribute('xmi.idref', $id);
514             my $generalizableElement = XML::LibXML::Element->new('UML:GeneralizableElement.generalization');
515             $generalizableElement->appendChild($generalization);
516             $xml_class->appendChild($generalizableElement);
517             }
518              
519             sub _xmi_write_method {
520             my ($self, $parent_node, $class, $method) = @_;
521              
522             my $id = $self->_xmi_get_new_id();
523             my $visibility = 'public';
524             $visibility = 'private' if substr($method, 0, 1) eq '_';
525             my $ownerScope = 'instance';
526             $ownerScope = 'classifier' if $method =~ /^[A-Z]/o;
527              
528             my $xml_method = $self->_xmi_add_element($parent_node, 'UML:Operation', $method);
529              
530             $xml_method->setAttribute('xmi.id', $id);
531             $xml_method->setAttribute('visibility', $visibility);
532             $xml_method->setAttribute('ownerScope', $ownerScope);
533             $self->_xmi_set_default_attribute($xml_method, 'concurrency', 'sequential');
534             $self->_xmi_set_default_attribute($xml_method, $_, 'false') foreach qw(isSpecification isQuery isRoot isLeaf isAbstract);
535             }
536              
537             sub _xmi_write_class {
538             my ($self, $class) = @_;
539              
540             my $xml_class = $self->_xmi_add_element($self->{_xmi}->{_classes_root}, 'UML:Class', $class->{name});
541             $self->{_xmi}->{_classes_hash}->{$class->{name}} = $xml_class;
542             $xml_class->setAttribute('xmi.id', $class->{xmi_id});
543             $xml_class->setAttribute('visibility', 'public');
544             $self->_xmi_set_default_attribute($xml_class, $_, 'false') foreach qw(isSpecification isRoot isLeaf isAbstract isActive);
545              
546             my $uml_classifier = XML::LibXML::Element->new('UML:Classifier.feature');
547             $xml_class->appendChild($uml_classifier);
548              
549             $self->_xmi_write_method($uml_classifier, $class, $_) foreach @{$class->{methods}};
550             $self->_xmi_create_inheritance($class, $_) foreach @{$class->{subclasses}};
551             }
552              
553             sub _xmi_set_id {
554             my ($self, $class) = @_;
555             $class->{xmi_id} = $self->_xmi_get_new_id();
556             $self->{_xmi}->{_name2id}->{$class->{name}} = $class->{xmi_id};
557             }
558              
559             sub _xmi_add_element {
560             my ($self, $parent, $class, $name) = @_;
561             my $node;
562             if (defined $name) {
563             foreach $node ($parent->getElementsByTagName($class)) {
564             if ($node->getAttribute('name') eq $name) {
565             return $node;
566             }
567             }
568             }
569             $node = $self->{_xmi}->{_document}->createElement($class);
570             $node->setAttribute('name', $name);
571             $parent->appendChild($node);
572             return $node;
573             }
574              
575             sub _xmi_set_default_attribute {
576             my ($self, $node, $name, $value) = @_;
577             return if defined $node->getAttribute($name);
578             $node->setAttribute($name, $value);
579             }
580              
581             sub _xmi_load_model {
582             my ($self, $fname) = @_;
583             $self->{_xmi}->{_document} = XML::LibXML->new()->parse_file($fname);
584             }
585              
586             sub _xmi_init_xml {
587             my ($self, $fname) = @_;
588             unless (defined $self->{_xmi}->{_document}) {
589             $self->{_xmi}->{_document} = XML::LibXML::Document->new('1.0', 'UTF-8');
590             }
591             my $doc = $self->{_xmi}->{_document};
592              
593             my $xmi_root = $doc->createElement('XMI');
594             $xmi_root->setAttribute('xmi.version', '1.2');
595             $xmi_root->setAttribute('xmlns:UML', 'org.omg.xmi.namespace.UML');
596             my $generate_time = POSIX::asctime(localtime(time()));
597             chomp($generate_time);
598             $xmi_root->setAttribute('timestamp', $generate_time);
599             $doc->setDocumentElement($xmi_root);
600              
601             my $xmi_content = $doc->createElement('XMI.content');
602             $xmi_root->appendChild($xmi_content);
603              
604             my $uml_model = $self->_xmi_add_element($xmi_content, 'UML:Model', $fname || '');
605             $uml_model->setAttribute('xmi.id', $self->_xmi_get_new_id());
606             $self->_xmi_set_default_attribute($uml_model, $_, 'false') foreach qw(isSpecification isRoot isLeaf isAbstract);
607              
608             $self->{_xmi}->{_classes_root} = $doc->createElement('UML:Namespace.ownedElement');
609             $uml_model->appendChild($self->{_xmi}->{_classes_root});
610              
611             return $doc;
612             }
613              
614             sub as_xmi {
615             my ($self, $fname) = @_;
616             $self->_build_dom;
617             $self->{_xmi} ||= {};
618             $self->{_xmi}->{_id_counter} = 1;
619             $self->{_xmi}->{_name2id} = {};
620             $self->_xmi_set_id($_) foreach @{$self->{classes}};
621             my $doc = $self->_xmi_init_xml($fname);
622             $self->_xmi_write_class($_) foreach @{$self->{classes}};
623             if ($fname) {
624             $doc->toFile($fname, 2);
625             } else {
626             return $doc;
627             }
628             }
629              
630             sub as_dot {
631             my ($self, $fname) = @_;
632             $self->_build_dom;
633             if ($fname) {
634             $tt->process(\$dot_template, $self, $fname)
635             || carp $tt->error();
636             } else {
637             my $dot;
638             $tt->process(\$dot_template, $self, \$dot)
639             || carp $tt->error();
640             $dot;
641             }
642             }
643              
644             sub set_dot ($$) {
645             my $self = shift;
646             $self->{dot} = shift;
647             }
648              
649             $dot_template = <<'_EOC_';
650             digraph uml_class_diagram {
651             [%- IF width && height %]
652             size="[% width %],[% height %]";
653             [%- END %]
654             node [shape=record, style="filled"];
655             edge [color=red, dir=none];
656              
657             [%- name2id = {} %]
658             [%- id = 1 %]
659             [%- FOREACH class = classes %]
660             [%- name = class.name %]
661             [%- name2id.$name = id %]
662             class_[% id %] [shape=plaintext, style="", label=<
663            
664            
[% name %]
665            
666            
667            
668            
669             [% IF class.properties.size > 0 %]
670             [%- FOREACH property = class.properties %]
671             [%- property.match("^_") ? "-" : "+" %]
672              
673             [%- END %][% END %]
674            
675             [%- FOREACH property = class.properties %]
676             [%- property %]
677              
678             [%- END %]
679            
680            
681            
682            
683            
684            
685             [%- IF display_methods %]
686            
687            
688             [% IF class.methods.size > 0 %]
689             [%- FOREACH method = class.methods %]
690             [%- method.match("^_") ? "-" : "+" %]
691              
692             [%- END %][% END %]
693            
694             [%- FOREACH method = class.methods %]
695             [%- method %]
696              
697             [%- END %]
698            
699            
700             [%- END %]
701            
702            
703            
>];
704             [%- id = id + 1 %]
705             [% END %]
706             [%- class_id = id %]
707              
708             [%- first = 1 %]
709             [%- id = 0 %]
710             [%- IF display_inheritance %]
711             [%- FOREACH class = classes %]
712             [%- id = id + 1 %]
713             [%- super = class.name %]
714             [%- NEXT IF !class.subclasses.size -%]
715              
716             [%- IF first -%]
717             node [shape="triangle", fillcolor=yellow, height=0.3, width=0.3];
718             [%- first = 0 %]
719             [%- END -%]
720              
721             angle_[% id %] [label=""];
722              
723             [%- super_id = name2id.$super %]
724             class_[% super_id %]:methods -> angle_[% id %]
725              
726             [%- FOREACH child = class.subclasses %]
727             [%- child_id = name2id.$child %]
728             [%- IF !child_id %]
729             class_[% class_id %] [shape=record, label="[% child %]" fillcolor="#f1e1f4", style="filled"];
730             angle_[% id %] -> class_[% class_id %]
731             [%- class_id = class_id + 1 %]
732             [%- ELSE %]
733             angle_[% id %] -> class_[% child_id %]:title
734             [%- END %]
735             [%- END %]
736             [%- END %]
737             [%- END %]
738              
739             [%- IF moose_roles %]
740             [%- first = 1 %]
741             edge [color=blue, dir=none];
742             [%- FOREACH class = classes %]
743             [%- id = id + 1 %]
744             [%- NEXT IF !class.consumers.size -%]
745             [%- role = class.name %]
746             [%- role_id = name2id.$role %]
747             [%- IF first %]
748             node [shape="triangle", fillcolor=orange, height=0.3, width=0.3];
749             [%- first = 0 %]
750             [%- END %]
751              
752             angle_[% id %] [label=""];
753             class_[% role_id %]:methods -> angle_[% id %]
754              
755             [%- FOREACH consumer = class.consumers %]
756             [%- consumer_id = name2id.$consumer %]
757             angle_[% id %] -> class_[% consumer_id %]:title
758             [%- END %]
759             [%- END %]
760             [%- END %]
761              
762             }
763             _EOC_
764              
765             1;
766             __END__