File Coverage

blib/lib/DBICx/AutoDoc.pm
Criterion Covered Total %
statement 42 265 15.8
branch 0 58 0.0
condition 1 30 3.3
subroutine 14 40 35.0
pod 23 23 100.0
total 80 416 19.2


line stmt bran cond sub pod time code
1             package DBICx::AutoDoc;
2 1     1   13782 use strict;
  1         2  
  1         23  
3 1     1   3 use warnings;
  1         1  
  1         37  
4             our $VERSION = '0.08';
5 1     1   3 use base qw( Class::Accessor::Grouped );
  1         5  
  1         531  
6 1     1   8609 use Carp qw( croak );
  1         1  
  1         41  
7 1     1   489 use Template;
  1         17442  
  1         28  
8 1     1   485 use FindBin qw( );
  1         716  
  1         20  
9 1     1   386 use Data::Dump qw( dump );
  1         3680  
  1         66  
10 1     1   378 use DBICx::AutoDoc::Magic;
  1         4  
  1         28  
11 1     1   602 use File::Temp qw( tempfile );
  1         14850  
  1         81  
12 1     1   658 use File::ShareDir qw( dist_dir );
  1         5442  
  1         101  
13 1     1   9 use File::Spec;
  1         1  
  1         17  
14 1     1   496 use Tie::IxHash;
  1         3336  
  1         502  
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   7 my @tmp = do { no strict 'refs'; @{ $class.'::ISA' } };
  1         1  
  1         1569  
  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 condition 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             }
234              
235 0         0 return values %relationships;
236             }
237              
238             sub relationship_map {
239 0     0 1 0 my ( $self ) = @_;
240              
241 0         0 my @relmap = ();
242 0         0 my $snames = $self->{ 'source_names' };
243              
244 0         0 for my $source ( @{ $self->sources } ) {
  0         0  
245 0         0 for my $rel ( @{ $source->{ 'relationships' } } ) {
  0         0  
246 0         0 my $type = $rel->{ 'type' };
247             my $map = {
248 0         0 name => $rel->{ 'name' },
249             type => $type,
250             };
251 0         0 push( @relmap, $map );
252 0 0       0 if ( $type eq 'many_to_many' ) {
253 0         0 for my $x (qw( link_rel_name foreign_rel_name )) {
254 0         0 $map->{ $x } = $rel->{ $x };
255             }
256 0         0 $map->{ 'accessor' } = 'many_to_many';
257             } else {
258 0         0 $map->{ 'accessor' } = $rel->{ 'attr' }->{ 'accessor' };
259 0         0 $map->{ 'self' } = $source->{ 'moniker' };
260 0         0 $map->{ 'foreign' } = $snames->{ $rel->{ 'foreign_class' } };
261              
262 0         0 my %cond = %{ $rel->{ 'cond' } };
  0         0  
263            
264 0         0 my @cond = ();
265 0         0 while ( my ( $l, $r ) = each %cond ) {
266 0         0 push( @cond, { split( '\.', $l, 2 ), split( '\.', $r ) } );
267             }
268 0         0 $map->{ 'cond' } = \@cond;
269             }
270             }
271             }
272 0         0 return \@relmap;
273             }
274              
275             sub get_simple_moniker_for {
276 0     0 1 0 my ( $self, $moniker ) = @_;
277              
278             #if ( $moniker->can( 'source_name' ) ) { $moniker = $moniker->source_name }
279              
280 0   0     0 $self->{ '_simple_moniker_cache' } ||= {};
281 0         0 my $cache = $self->{ '_simple_moniker_cache' };
282            
283 0 0       0 if ( $cache->{ $moniker } ) { return $cache->{ $moniker } }
  0         0  
284              
285 0         0 my $simple = $moniker;
286 0         0 $simple =~ s/\W+/_/g;
287              
288 0         0 my %inverse_cache = reverse %{ $cache };
  0         0  
289 0 0       0 if ( $inverse_cache{ $simple } ) {
290 0         0 my $i = 0;
291 0         0 while ( $inverse_cache{ $simple.$i } ) { $i++ }
  0         0  
292 0         0 $simple .= $i;
293             }
294              
295 0         0 $cache->{ $moniker } = $simple;
296             }
297              
298 0     0 1 0 sub byname($$) { return shift->{ 'name' } cmp shift->{ 'name' } }
299              
300             sub get_vars {
301 0     0 1 0 my ( $self ) = @_;
302              
303 0         0 my @vars = qw(
304             schema schema_class schema_version generated software_versions sources
305             relationship_map filename_base output connect dsn user
306             graphviz_command inheritance
307             );
308              
309             $self->{ '_vars' } ||= {
310             autodoc => $self,
311 0     0   0 dumper => sub { return dump( @_ ) },
312 0     0   0 simplify => sub { return $self->get_simple_moniker_for( @_ ) },
313 0     0   0 output_filename => sub { return $self->output_filename( @_ ) },
314             ENV => \%ENV,
315             varlist => [ @vars, 'ENV' ],
316 0   0     0 ( map { ( $_ => $self->$_() ) } @vars ),
  0         0  
317             };
318 0         0 return $self->{ '_vars' };
319             }
320              
321             sub find_template_file {
322 0     0 1 0 my ( $self, $template ) = @_;
323              
324 0         0 my $path = $self->include_path;
325 0 0       0 if ( ! ref $path ) { $path = [ $path ] }
  0         0  
326              
327 0         0 for my $x ( @{ $path } ) {
  0         0  
328 0         0 my $test = File::Spec->catfile( $x, $template );
329 0 0       0 if ( -f $test ) { return $test }
  0         0  
330             }
331              
332 0         0 return;
333             }
334              
335             sub fill_template {
336 0     0 1 0 my ( $self, $template ) = @_;
337              
338             my $first_line = sub {
339 0     0   0 open( my $fh, shift() ); chomp( my $start = <$fh> ); close( $fh );
  0         0  
  0         0  
340 0         0 return $start;
341 0         0 };
342              
343 0         0 my $tmpl = Template->new( { INCLUDE_PATH => $self->include_path } );
344 0         0 my $outfile = $self->output_filename( $template, 1 );
345 0         0 my $vars = $self->get_vars;
346              
347 0 0       0 if ( $first_line->( $self->find_template_file( $template ) ) =~ /^#!/ ) {
348 0         0 my ( undef, $file ) = tempfile();
349 0         0 my $script = $outfile.'.script';
350 0 0       0 $tmpl->process( $template, $vars, $script ) || croak $tmpl->error;
351              
352 0         0 my $cmd = $first_line->( $script );
353 0         0 $cmd =~ s/^#!//;
354              
355 0         0 open( my $outfh, '>', $outfile );
356 0         0 open( my $infh, '-|', $cmd, $script );
357 0         0 $outfh->print( <$infh> );
358 0         0 close( $infh );
359 0         0 close( $outfh );
360 0         0 unlink( $script );
361             } else {
362 0 0       0 $tmpl->process( $template, $vars, $outfile ) || croak $tmpl->error;
363             }
364             }
365              
366             sub filename_base {
367 0     0 1 0 my ( $self ) = @_;
368              
369 0   0     0 my $name = ref( $self->schema ) || $self->schema;
370 0 0       0 if ( ! $name ) { croak "Cannot call filename_base without a schema" }
  0         0  
371 0         0 $name =~ s/::/-/g;
372 0   0     0 return join( '-', $name, $self->schema->VERSION || 1 );
373             }
374              
375             sub output_filename {
376 0     0 1 0 my ( $self, $template, $full ) = @_;
377              
378 0         0 my $base = $self->filename_base;
379 0         0 $template =~ s/^AUTODOC/$base/;
380 0 0       0 if ( $full ) {
381 0         0 return File::Spec->catfile( $self->output, $template );
382             } else {
383 0         0 return $template;
384             }
385             }
386              
387             sub default_include_path {
388 1     1 1 2 my ( $self ) = @_;
389 1   33     12 (my $dist = ref( $self ) || $self) =~ s/::/-/g;
390 1         4 return [ dist_dir( $dist ), File::Spec->catdir( $FindBin::Bin, "templates" ) ];
391             }
392              
393             sub list_templates {
394 0     0 1   my ( $self ) = @_;
395              
396 0           my $inc = $self->include_path;
397 0 0         if ( ! ref $inc ) { $inc = [ $inc ] }
  0            
398 0           my %tmpls = ();
399 0           for my $dir ( @{ $inc } ) {
  0            
400 0           opendir( my $dirfh, $dir );
401 0           for ( readdir( $dirfh ) ) {
402 0 0         next unless /^AUTODOC/;
403 0           $tmpls{ $_ } = 1;
404             }
405 0           closedir( $dirfh );
406             }
407              
408 0 0         return sort { length( $a ) <=> length( $b ) || $a cmp $b } keys %tmpls;
  0            
409             }
410              
411             sub fill_all_templates {
412 0     0 1   my ( $self ) = @_;
413              
414 0           $self->fill_templates( $self->list_templates );
415             }
416              
417             sub fill_templates {
418 0     0 1   my ( $self, @templates ) = @_;
419              
420 0           $self->fill_template( $_ ) for @templates;
421             }
422              
423              
424             1;
425             __END__