File Coverage

blib/lib/DBIx/Class/Visualizer.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


};
line stmt bran cond sub pod time code
1 1     1   14885 use 5.10.1;
  1         3  
2 1     1   3 use strict;
  1         1  
  1         18  
3 1     1   2 use warnings;
  1         1  
  1         51  
4              
5             package DBIx::Class::Visualizer;
6              
7             # ABSTRACT: Visualize a DBIx::Class schema
8             our $AUTHORITY = 'cpan:CSSON'; # AUTHORITY
9             our $VERSION = '0.0100';
10              
11 1     1   904 use GraphViz2;
  0            
  0            
12             use List::Util qw/any/;
13             use DateTime::Tiny;
14             use Moo;
15              
16             #has logger => (
17             # is => 'ro',
18             # default => sub {
19             # my $logger = Log::Handler->new;
20             # $logger->add(screen => {
21             # maxlevel => 'debug',
22             # minlevel => 'error',
23             # message_layout => '%m',
24             #
25             # });
26             # return $logger;
27             # },
28             #);
29             has graphviz_config => (
30             is => 'ro',
31             lazy => 1,
32             default => sub {
33             my $self = shift;
34              
35             return +{
36             global => {
37             directed => 1,
38             smoothing => 'none',
39             overlap => 'false',
40             },
41             graph => {
42             rankdir => 'LR',
43             splines => 'true',
44             label => sprintf ('%s (version %s) rendered by DBIx::Class::Visualizer %s.', ref $self->schema, $self->schema->schema_version, DateTime::Tiny->now->as_string),
45             fontname => 'helvetica',
46             fontsize => 10,
47             labeljust => 'l',
48             nodesep => 0.28,
49             ranksep => 0.36,
50             },
51             node => {
52             fontname => 'helvetica',
53             shape => 'none',
54             },
55             };
56             },
57             );
58             has graph => (
59             is => 'ro',
60             lazy => 1,
61             init_arg => undef,
62             builder => '_build_graph',
63             );
64             sub _build_graph {
65             return GraphViz2->new(shift->graphviz_config);
66             }
67             has schema => (
68             is => 'ro',
69             required => 1,
70             );
71             has added_relationships => (
72             is => 'ro',
73             default => sub { +{} },
74             );
75             has ordered_relationships => (
76             is => 'ro',
77             default => sub { [] },
78             );
79              
80             sub BUILD {
81             my $self = shift;
82             my @sources = grep { !/^View::/ } $self->schema->sources;
83              
84             foreach my $source_name (sort @sources) {
85             $self->add_node($source_name);
86             }
87             foreach my $source_name (sort @sources) {
88             $self->add_edges($source_name);
89             }
90             }
91              
92             sub svg {
93             my $self = shift;
94              
95             my $output;
96             $self->graph->run(output_file => \$output, format => 'svg');
97             return $output;
98             }
99              
100             sub add_node {
101             my $self = shift;
102             my $source_name = shift;
103              
104             my $node_name = $self->node_name($source_name);
105             my $rs = $self->schema->resultset($source_name)->result_source;
106              
107             my @primary_columns = $rs->primary_columns;
108             my @foreign_columns = map { keys %{ $_->{'attrs'}{'fk_columns'} } } map { $rs->relationship_info($_) } $rs->relationships;
109              
110             my $label_data = {
111             source_name => $source_name,
112             columns => [],
113             };
114             for my $column ($rs->columns) {
115             my $is_primary = any { $column eq $_ } @primary_columns;
116             my $is_foreign = any { $column eq $_ } @foreign_columns;
117             push @{ $label_data->{'columns'} } => {
118             is_primary => $is_primary,
119             is_foreign => $is_foreign,
120             name => $column,
121             };
122             }
123             $self->graph->add_node(
124             name => $node_name,
125             label => $self->create_label_html($node_name, $label_data),
126             margin => 0.01,
127             );
128             }
129              
130             sub add_edges {
131             my $self = shift;
132             my $source_name = shift;
133              
134             my $node_name = $self->node_name($source_name);
135             my $rs = $self->schema->resultset($source_name)->result_source;
136              
137             RELATION:
138             for my $relation_name (sort $rs->relationships) {
139             my $relation = $rs->relationship_info($relation_name);
140             (my $other_source_name = $relation->{'class'}) =~ s{^.*?::Result::}{};
141             my $other_node_name = $self->node_name($other_source_name);
142              
143             # Have we already added the edge from the reversed direction?
144             next RELATION if exists $self->added_relationships->{"$other_node_name-->$node_name"};
145              
146             my $other_rs = $self->schema->resultset($other_source_name)->result_source;
147             my $other_relation;
148              
149             OTHER_RELATION:
150             for my $other_relation_name ($other_rs->relationships) {
151             my $relation_to_attempt = $other_rs->relationship_info($other_relation_name);
152              
153             my $possibly_original_class = $relation_to_attempt->{'class'} =~ s{^.*?::Result::}{}rg;
154             next OTHER_RELATION if $possibly_original_class ne $source_name;
155             $other_relation = $relation_to_attempt;
156             $other_relation->{'_name'} = $other_relation_name;
157             }
158              
159             if(!defined $other_relation) {
160             warn "! No reverse relationship $source_name <-> $other_source_name";
161             next RELATION;
162             }
163              
164             my $arrowhead = $self->get_arrow_type($relation);
165             my $arrowtail = $self->get_arrow_type($other_relation);
166              
167             my $headport = ref $relation->{'cond'} eq 'HASH' && scalar keys %{ $relation->{'cond'} } == 1
168             ? (keys %{ $relation->{'cond'} })[0] =~ s{^foreign\.}{}rx
169             : $node_name
170             ;
171             my $tailport = ref $relation->{'cond'} eq 'HASH' && scalar keys %{ $relation->{'cond'} } == 1
172             ? (values %{ $relation->{'cond'} })[0] =~ s{^self\.}{}rx
173             : $node_name
174             ;
175              
176             $self->graph->add_edge(
177             from => "$node_name:$tailport",
178             to => "$other_node_name:$headport",
179             arrowhead => $arrowhead,
180             arrowtail => $arrowtail,
181             dir => 'both',
182             minlen => 2,
183             );
184              
185             $self->added_relationships->{ "$node_name-->$other_node_name" } = 1;
186             $self->added_relationships->{ "$other_node_name-->$node_name" } = 1;
187              
188             push @{ $self->ordered_relationships } => (
189             "$node_name-->$other_node_name",
190             "$other_node_name-->$node_name"
191             );
192             }
193             }
194              
195             sub get_arrow_type {
196             my $self = shift;
197             my $relation = shift;
198              
199             my $accessor = $relation->{'attrs'}{'accessor'};
200             my $is_depends_on = $relation->{'attrs'}{'is_depends_on'};
201             my $join_type = exists $relation->{'attrs'}{'join_type'} ? lc $relation->{'attrs'}{'join_type'} : '';
202              
203             my $has_many = $accessor eq 'multi' && !$is_depends_on && $join_type eq 'left' ? 1 : 0;
204             my $belongs_to = $accessor eq 'single' && $is_depends_on && $join_type eq '' ? 1 : 0;
205             my $might_have = $accessor eq 'single' && !$is_depends_on && $join_type eq 'left' ? 1 : 0;
206              
207             return $has_many ? join ('' => qw/crow none odot/)
208             : $belongs_to ? join ('' => qw/none tee/)
209             : $might_have ? join ('' => qw/none tee none odot/)
210             : join ('' => qw/dot dot dot/)
211             ;
212              
213             }
214              
215             sub node_name {
216             my $self = shift;
217             my $node_name = shift;
218             $node_name =~ s{::}{__}g;
219             return $node_name;
220             }
221             sub port_name {
222             my $self = shift;
223             my $source_name = shift;
224             my $column_name = shift;
225              
226             my $node_name = $self->node_name($source_name);
227             return "$node_name--$column_name";
228             }
229              
230             sub create_label_html {
231             my $self = shift;
232             my $node_name = shift;
233             my $data = shift;
234              
235             my $column_html = [];
236              
237             for my $column (@{ $data->{'columns'} }) {
238             my $clean_column_name = my $column_name = $column->{'name'};
239              
240             my $port_name = $self->port_name($node_name, $column_name);
241              
242             $column_name = $column->{'is_primary'} ? "$column_name" : $column_name;
243             $column_name = $column->{'is_foreign'} ? "$column_name" : $column_name;
244             push @{ $column_html } => qq{
245            
$column_name __
246             }
247             my $html = qq{
248             <
249            
250            
$data->{'source_name'}
251            
252             } . join ('', @{ $column_html }) . qq{
253            
>
254             };
255              
256             return $html;
257             }
258              
259             1;
260              
261             __END__