File Coverage

blib/lib/DBICx/AutoDoc.pm
Criterion Covered Total %
statement 42 266 15.7
branch 0 62 0.0
condition 1 30 3.3
subroutine 14 40 35.0
pod 23 23 100.0
total 80 421 19.0


line stmt bran cond sub pod time code
1             package DBICx::AutoDoc;
2 1     1   47202 use strict;
  1         3  
  1         24  
3 1     1   4 use warnings;
  1         1  
  1         33  
4             our $VERSION = '0.09';
5 1     1   5 use base qw( Class::Accessor::Grouped );
  1         2  
  1         393  
6 1     1   7335 use Carp qw( croak );
  1         2  
  1         37  
7 1     1   309 use Template;
  1         15645  
  1         27  
8 1     1   294 use FindBin qw( );
  1         737  
  1         21  
9 1     1   309 use Data::Dump qw( dump );
  1         3879  
  1         52  
10 1     1   290 use DBICx::AutoDoc::Magic;
  1         3  
  1         30  
11 1     1   434 use File::Temp qw( tempfile );
  1         12893  
  1         57  
12 1     1   279 use File::ShareDir qw( dist_dir );
  1         4427  
  1         49  
13 1     1   6 use File::Spec;
  1         2  
  1         15  
14 1     1   300 use Tie::IxHash;
  1         2789  
  1         509  
15              
16             __PACKAGE__->mk_group_accessors( simple => qw(
17             output connect dsn user pass
18             ) );
19             __PACKAGE__->mk_group_accessors( inherited => qw(
20             include_path graphviz_command
21             ) );
22             __PACKAGE__->include_path( __PACKAGE__->default_include_path() );
23             __PACKAGE__->graphviz_command( [ "fdp" ] );
24              
25             sub new {
26 0     0 1 0 my $self = bless( {
27             output => '.',
28             connect => 0,
29             }, shift() );
30 0         0 my %args = @_;
31              
32 0         0 for my $key ( keys %args ) { $self->$key( $args{ $key } ) }
  0         0  
33              
34 0         0 return $self;
35             }
36              
37             sub schema {
38 0     0 1 0 my ( $self, $val ) = @_;
39              
40 0 0       0 if ( $val ) {
    0          
41 0         0 $self->{ 'schema' } = $val;
42 0         0 eval "require $val";
43 0 0       0 if ( $@ ) { croak "Could not load $val: $@" }
  0         0  
44             } elsif( my $schema = $self->{ 'schema' } ) {
45 0 0 0     0 if ( ref( $schema ) || ! $self->connect ) { return $schema }
  0         0  
46 0         0 print "Connecting to database\n";
47 0         0 $self->{ 'schema' } = $schema->connect(
48             $self->dsn, $self->user, $self->pass,
49             );
50 0         0 return $self->{ 'schema' };
51             } else {
52 0         0 croak "No schema provided";
53             }
54             }
55              
56             sub schema_class {
57 0     0 1 0 my ( $self ) = @_;
58              
59 0         0 my $schema = $self->schema;
60 0   0     0 return ref( $schema ) || $schema;
61             }
62              
63 0 0   0 1 0 sub schema_version { shift->schema->VERSION || 1 }
64              
65             sub generated {
66 0     0 1 0 my ( $self ) = @_;
67              
68 0   0     0 $self->{ 'generated' } ||= localtime;
69 0         0 return $self->{ 'generated' };
70             }
71              
72             sub software_versions {
73 0     0 1 0 my ( $self ) = @_;
74              
75             return {
76 0         0 map { ( $_ => $_->VERSION ) } qw(
  0         0  
77             DBICx::AutoDoc DBICx::AutoDoc::Magic
78             DBIx::Class Template
79             )
80             };
81             }
82              
83             sub sources {
84 0     0 1 0 my ( $self ) = @_;
85              
86 0 0       0 if ( $self->{ 'sources' } ) { return $self->{ 'sources' } }
  0         0  
87              
88 0         0 my $schema = $self->schema;
89              
90 0         0 my @sources = ();
91 0         0 $self->{ 'sources' } = \@sources;
92              
93 0         0 my %source_names = ();
94 0         0 $self->{ 'source_names' } = \%source_names;
95            
96             # mst: map { $_->source_name }
97             # grep { $_->result_class eq $class }
98             # map { $schema->source($_) } $schema->sources
99             # mst: it's all you can have safely :)
100 0         0 for my $moniker ( sort $schema->sources ) {
101 0         0 my $source = $schema->source( $moniker );
102 0         0 my $rs = $schema->resultset( $moniker );
103 0         0 my $cl = $rs->result_class;
104              
105 0         0 $source_names{ $cl } = $source->source_name;
106              
107             # COLLECTING DATA
108 0         0 push( @sources, {
109             moniker => $moniker,
110             simple_moniker => $self->get_simple_moniker_for( $moniker ),
111             class => $cl,
112             primary_columns => [ $cl->primary_columns ],
113             table => $cl->table,
114             result_class => $cl,
115             resultset_class => $cl->resultset_class,
116             columns => [ $self->get_columns_for( $cl ) ],
117             unique_constraints => [ $self->get_unique_constraints_for( $cl ) ],
118             relationships => [ $self->get_relationships_for( $cl ) ],
119             } );
120             }
121              
122 0         0 return $self->{ 'sources' };
123             }
124              
125             sub inheritance {
126 0     0 1 0 my ( $self, @classes ) = @_;
127              
128 0 0       0 if ( ! @classes ) {
129 0         0 @classes = ( map { $_->{ 'class' } } @{ $self->sources } );
  0         0  
  0         0  
130             }
131 0         0 my %parents = ();
132 0         0 while ( @classes ) {
133 0         0 my $class = shift( @classes );
134 0 0       0 next if $parents{ $class };
135 1     1   8 my @tmp = do { no strict 'refs'; @{ $class.'::ISA' } };
  1         2  
  1         1583  
  0         0  
  0         0  
  0         0  
136 0         0 push( @classes, @tmp );
137 0         0 $parents{ $class } = \@tmp;
138             }
139 0         0 return \%parents;
140             }
141              
142             sub get_columns_for {
143 0     0 1 0 my ( $self, $class ) = @_;
144              
145 0         0 my %cols = ();
146 0         0 tie( %cols, 'Tie::IxHash' );
147              
148             # COLUMNS
149 0         0 for ( $class->columns ) {
150 0         0 my $col = $class->column_info( $_ );
151             $col->{ 'default_value' } =
152 0         0 ref($col->{ 'default_value' }) eq "SCALAR" ? ${$col->{ 'default_value' }}
153             : defined($col->{ 'default_value' }) ? "'$col->{ 'default_value' }'"
154             : 'NULL'
155 0 0       0 if exists $col->{ 'default_value' };
    0          
    0          
156 0         0 $col->{ 'name' } = $_;
157 0 0       0 $col->{ 'is_inflated' } = delete $col->{ '_inflate_info' } ? 1 : 0;
158 0         0 $cols{ $_ } = $col;
159             }
160              
161             # PRIMARY COLUMNS
162 0         0 for my $c ( $class->primary_columns ) {
163 0         0 $cols{ $c }->{ 'is_primary' } = 1;
164             }
165              
166             # UNIQUE CONSTRAINTS
167 0         0 my %tmp = $class->unique_constraints;
168 0         0 while ( my ( $key, $val ) = each %tmp ) {
169 0         0 for my $x ( @{ $val } ) {
  0         0  
170 0         0 push( @{ $cols{ $x }->{ 'unique_constraints' } }, $key );
  0         0  
171             }
172             }
173              
174 0         0 return values %cols;
175             }
176              
177             sub get_unique_constraints_for {
178 0     0 1 0 my ( $self, $class ) = @_;
179              
180             # UNIQUE CONSTRAINTS
181 0         0 my %unique = ();
182              
183 0         0 my %tmp = $class->unique_constraints;
184 0         0 for my $key ( sort keys %tmp ) {
185 0         0 $unique{ $key }->{ 'name' } = $key;
186 0         0 $unique{ $key }->{ 'columns' } = $tmp{ $key }
187             }
188              
189 0         0 return values %unique;
190             }
191              
192             sub get_relationships_for {
193 0     0 1 0 my ( $self, $class ) = @_;
194              
195 0         0 my %relationships = ();
196              
197             # RELATIONSHIPS (from DBICx::AutoDoc::Magic)
198 0 0       0 unless ( $class->can( '_autodoc' ) ) {
199 0         0 croak "$class cannot _autodoc, something must have gone wrong";
200             }
201              
202 0   0     0 my $ad = $class->_autodoc || {};
203 0 0       0 for ( @{ $ad->{ 'relationships' } || [] } ) {
  0         0  
204 0         0 my ( $type, $relname, @parts ) = @{ $_ };
  0         0  
205 0   0     0 my $rel = ( $relationships{ $relname } ||= {} );
206 0         0 @{ $rel }{qw( name type )} = ( $relname, $type );
  0         0  
207              
208 0 0       0 if ( $type eq 'many_to_many' ) {
209 0         0 @{ $rel }{qw( link_rel_name foreign_rel_name attributes )} = @parts;
  0         0  
210             } else {
211 0         0 @{ $rel }{qw( foreign_class cond attributes )} = @parts;
  0         0  
212             }
213             }
214              
215             # RELATIONSHIPS (from DBIx::Class::Relationship)
216 0         0 for my $name ( $class->relationships ) {
217 0   0     0 my $rel = ( $relationships{ $name } ||= {} );
218 0         0 my $info = $class->relationship_info( $name );
219 0   0     0 $rel->{ 'name' } ||= $name;
220 0         0 for my $key ( keys %{ $info } ) {
  0         0  
221 0         0 $rel->{ $key } = $info->{ $key };
222             }
223             }
224              
225             # GENERAL RELATIONSHIP MUNGING
226 0         0 for my $name ( keys %relationships ) {
227 0         0 my $rel = $relationships{ $name };
228 0         0 for my $x ( '', 'foreign_' ) {
229 0 0       0 if ( $rel->{ $x.'class' } ) {
230 0         0 $rel->{ $x.'moniker' } = $rel->{ $x.'class' }->source_name;
231             }
232             }
233             # Can't handle the comples conds returned by code refs yet
234             # $rel->{ 'cond' } = ($rel->{ 'cond' }->({ self_alias => 'self', foreign_alias => 'foreign' }))[0]
235             delete( $rel->{ 'cond' } )
236 0 0       0 if ref( $rel->{ 'cond' } ) eq 'CODE';
237             }
238              
239 0         0 return values %relationships;
240             }
241              
242             sub relationship_map {
243 0     0 1 0 my ( $self ) = @_;
244              
245 0         0 my @relmap = ();
246 0         0 my $snames = $self->{ 'source_names' };
247              
248 0         0 for my $source ( @{ $self->sources } ) {
  0         0  
249 0         0 for my $rel ( @{ $source->{ 'relationships' } } ) {
  0         0  
250 0         0 my $type = $rel->{ 'type' };
251             my $map = {
252 0         0 name => $rel->{ 'name' },
253             type => $type,
254             };
255 0         0 push( @relmap, $map );
256 0 0       0 if ( $type eq 'many_to_many' ) {
257 0         0 for my $x (qw( link_rel_name foreign_rel_name )) {
258 0         0 $map->{ $x } = $rel->{ $x };
259             }
260 0         0 $map->{ 'accessor' } = 'many_to_many';
261             } else {
262 0         0 $map->{ 'accessor' } = $rel->{ 'attr' }->{ 'accessor' };
263 0         0 $map->{ 'self' } = $source->{ 'moniker' };
264 0         0 $map->{ 'foreign' } = $snames->{ $rel->{ 'foreign_class' } };
265              
266 0 0       0 my %cond = %{ $rel->{ 'cond' } || {} };
  0         0  
267            
268 0         0 my @cond = ();
269 0         0 while ( my ( $l, $r ) = each %cond ) {
270 0         0 push( @cond, { split( '\.', $l, 2 ), split( '\.', $r ) } );
271             }
272 0         0 $map->{ 'cond' } = \@cond;
273             }
274             }
275             }
276 0         0 return \@relmap;
277             }
278              
279             sub get_simple_moniker_for {
280 0     0 1 0 my ( $self, $moniker ) = @_;
281              
282             #if ( $moniker->can( 'source_name' ) ) { $moniker = $moniker->source_name }
283              
284 0   0     0 $self->{ '_simple_moniker_cache' } ||= {};
285 0         0 my $cache = $self->{ '_simple_moniker_cache' };
286            
287 0 0       0 if ( $cache->{ $moniker } ) { return $cache->{ $moniker } }
  0         0  
288              
289 0         0 my $simple = $moniker;
290 0         0 $simple =~ s/\W+/_/g;
291              
292 0         0 my %inverse_cache = reverse %{ $cache };
  0         0  
293 0 0       0 if ( $inverse_cache{ $simple } ) {
294 0         0 my $i = 0;
295 0         0 while ( $inverse_cache{ $simple.$i } ) { $i++ }
  0         0  
296 0         0 $simple .= $i;
297             }
298              
299 0         0 $cache->{ $moniker } = $simple;
300             }
301              
302 0     0 1 0 sub byname($$) { return shift->{ 'name' } cmp shift->{ 'name' } }
303              
304             sub get_vars {
305 0     0 1 0 my ( $self ) = @_;
306              
307 0         0 my @vars = qw(
308             schema schema_class schema_version generated software_versions sources
309             relationship_map filename_base output connect dsn user
310             graphviz_command inheritance
311             );
312              
313             $self->{ '_vars' } ||= {
314             autodoc => $self,
315 0     0   0 dumper => sub { return dump( @_ ) },
316 0     0   0 simplify => sub { return $self->get_simple_moniker_for( @_ ) },
317 0     0   0 output_filename => sub { return $self->output_filename( @_ ) },
318             ENV => \%ENV,
319             varlist => [ @vars, 'ENV' ],
320 0   0     0 ( map { ( $_ => $self->$_() ) } @vars ),
  0         0  
321             };
322 0         0 return $self->{ '_vars' };
323             }
324              
325             sub find_template_file {
326 0     0 1 0 my ( $self, $template ) = @_;
327              
328 0         0 my $path = $self->include_path;
329 0 0       0 if ( ! ref $path ) { $path = [ $path ] }
  0         0  
330              
331 0         0 for my $x ( @{ $path } ) {
  0         0  
332 0         0 my $test = File::Spec->catfile( $x, $template );
333 0 0       0 if ( -f $test ) { return $test }
  0         0  
334             }
335              
336 0         0 return;
337             }
338              
339             sub fill_template {
340 0     0 1 0 my ( $self, $template ) = @_;
341              
342             my $first_line = sub {
343 0     0   0 open( my $fh, shift() ); chomp( my $start = <$fh> ); close( $fh );
  0         0  
  0         0  
344 0         0 return $start;
345 0         0 };
346              
347 0         0 my $tmpl = Template->new( { INCLUDE_PATH => $self->include_path } );
348 0         0 my $outfile = $self->output_filename( $template, 1 );
349 0         0 my $vars = $self->get_vars;
350              
351 0 0       0 if ( $first_line->( $self->find_template_file( $template ) ) =~ /^#!/ ) {
352 0         0 my ( undef, $file ) = tempfile();
353 0         0 my $script = $outfile.'.script';
354 0 0       0 $tmpl->process( $template, $vars, $script ) || croak $tmpl->error;
355              
356 0         0 my $cmd = $first_line->( $script );
357 0         0 $cmd =~ s/^#!//;
358              
359 0         0 open( my $outfh, '>', $outfile );
360 0         0 open( my $infh, '-|', $cmd, $script );
361 0         0 $outfh->print( <$infh> );
362 0         0 close( $infh );
363 0         0 close( $outfh );
364 0         0 unlink( $script );
365             } else {
366 0 0       0 $tmpl->process( $template, $vars, $outfile ) || croak $tmpl->error;
367             }
368             }
369              
370             sub filename_base {
371 0     0 1 0 my ( $self ) = @_;
372              
373 0   0     0 my $name = ref( $self->schema ) || $self->schema;
374 0 0       0 if ( ! $name ) { croak "Cannot call filename_base without a schema" }
  0         0  
375 0         0 $name =~ s/::/-/g;
376 0   0     0 return join( '-', $name, $self->schema->VERSION || 1 );
377             }
378              
379             sub output_filename {
380 0     0 1 0 my ( $self, $template, $full ) = @_;
381              
382 0         0 my $base = $self->filename_base;
383 0         0 $template =~ s/^AUTODOC/$base/;
384 0 0       0 if ( $full ) {
385 0         0 return File::Spec->catfile( $self->output, $template );
386             } else {
387 0         0 return $template;
388             }
389             }
390              
391             sub default_include_path {
392 1     1 1 2 my ( $self ) = @_;
393 1   33     8 (my $dist = ref( $self ) || $self) =~ s/::/-/g;
394 1         6 return [ dist_dir( $dist ), File::Spec->catdir( $FindBin::Bin, "templates" ) ];
395             }
396              
397             sub list_templates {
398 0     0 1   my ( $self ) = @_;
399              
400 0           my $inc = $self->include_path;
401 0 0         if ( ! ref $inc ) { $inc = [ $inc ] }
  0            
402 0           my %tmpls = ();
403 0           for my $dir ( @{ $inc } ) {
  0            
404 0           opendir( my $dirfh, $dir );
405 0           for ( readdir( $dirfh ) ) {
406 0 0         next unless /^AUTODOC/;
407 0           $tmpls{ $_ } = 1;
408             }
409 0           closedir( $dirfh );
410             }
411              
412 0 0         return sort { length( $a ) <=> length( $b ) || $a cmp $b } keys %tmpls;
  0            
413             }
414              
415             sub fill_all_templates {
416 0     0 1   my ( $self ) = @_;
417              
418 0           $self->fill_templates( $self->list_templates );
419             }
420              
421             sub fill_templates {
422 0     0 1   my ( $self, @templates ) = @_;
423              
424 0           $self->fill_template( $_ ) for @templates;
425             }
426              
427              
428             1;
429             __END__