File Coverage

blib/lib/Alzabo/MethodMaker.pm
Criterion Covered Total %
statement 39 460 8.4
branch 0 192 0.0
condition 0 34 0.0
subroutine 13 70 18.5
pod 0 19 0.0
total 52 775 6.7


line stmt bran cond sub pod time code
1             package Alzabo::MethodMaker;
2              
3 1     1   1956 use strict;
  1         4  
  1         56  
4 1     1   5 use vars qw($VERSION);
  1         4  
  1         129  
5              
6 1     1   8 use Alzabo::Exceptions;
  1         2  
  1         13  
7 1     1   5 use Alzabo::Runtime;
  1         3  
  1         11  
8 1     1   5 use Alzabo::Utils;
  1         2  
  1         27  
9              
10 1     1   5 use Params::Validate qw( :all );
  1         2  
  1         1285  
11             Params::Validate::validation_options( on_fail => sub { Alzabo::Exception::Params->throw( error => join '', @_ ) } );
12              
13             $VERSION = 2.0;
14              
15             # types of methods that can be made - only ones that haven't been
16             # deprecated
17             my @options = qw( foreign_keys
18             linking_tables
19             lookup_columns
20             row_columns
21             self_relations
22              
23             tables
24             table_columns
25              
26             insert_hooks
27             update_hooks
28             select_hooks
29             delete_hooks
30             );
31              
32             sub import
33             {
34 0     0     my $class = shift;
35              
36 0           validate( @_, { schema => { type => SCALAR },
37             class_root => { type => SCALAR,
38             optional => 1 },
39             name_maker => { type => CODEREF,
40             optional => 1 },
41 0           ( map { $_ => { optional => 1 } } 'all', @options ) } );
42 0           my %p = @_;
43              
44 0 0         return unless exists $p{schema};
45 0 0         return unless grep { exists $p{$_} && $p{$_} } 'all', @options;
  0 0          
46              
47 0           my $maker = $class->new(%p);
48              
49 0           $maker->make;
50             }
51              
52             sub new
53             {
54 0     0 0   my $class = shift;
55 0           my %p = @_;
56              
57 0 0         if ( delete $p{all} )
58             {
59 0           foreach (@options)
60             {
61 0 0 0       $p{$_} = 1 unless exists $p{$_} && ! $p{$_};
62             }
63             }
64              
65 0           my $s = Alzabo::Runtime::Schema->load_from_file( name => delete $p{schema} );
66              
67 0           my $class_root;
68 0 0         if ( $p{class_root} )
69             {
70 0           $class_root = $p{class_root};
71             }
72             else
73             {
74 0           my $x = 0;
75             do
76 0           {
77 0           $class_root = caller($x++);
78 0 0         die "No base class could be determined\n" unless $class_root;
79             } while ( $class_root->isa(__PACKAGE__) );
80             }
81              
82 0           my $self;
83              
84 0 0   0     $p{name_maker} = sub { $self->name(@_) } unless ref $p{name_maker};
  0            
85              
86 0           $self = bless { opts => \%p,
87             class_root => $class_root,
88             schema => $s,
89             }, $class;
90              
91 0           return $self;
92             }
93              
94             sub make
95             {
96 0     0 0   my $self = shift;
97              
98 0           $self->{schema_class} = join '::', $self->{class_root}, 'Schema';
99 0           bless $self->{schema}, $self->{schema_class};
100              
101 0           $self->eval_schema_class;
102 0           $self->load_class( $self->{schema_class} );
103              
104             {
105             # Users can add methods to these superclasses
106 1     1   8 no strict 'refs';
  1         2  
  1         1454  
  0            
107 0           foreach my $thing ( qw( Table Row ) )
108             {
109 0           @{ "$self->{class_root}::${thing}::ISA" }
  0            
110             = ( "Alzabo::Runtime::$thing", "Alzabo::DocumentationContainer" );
111             }
112             }
113              
114 0           foreach my $t ( sort { $a->name cmp $b->name } $self->{schema}->tables )
  0            
115             {
116 0           $self->{table_class} = join '::', $self->{class_root}, 'Table', $t->name;
117 0           $self->{row_class} = join '::', $self->{class_root}, 'Row', $t->name;
118              
119 0           bless $t, $self->{table_class};
120 0           $self->eval_table_class;
121 0           $self->{schema}->add_contained_class( table => $self->{table_class} );
122              
123 0           $self->eval_row_class;
124 0           $t->add_contained_class( row => $self->{row_class} );
125              
126 0 0         if ( $self->{opts}{tables} )
127             {
128 0           $self->make_table_method($t);
129             }
130              
131 0           $self->load_class( $self->{table_class} );
132 0           $self->load_class( $self->{row_class} );
133              
134 0 0         if ( $self->{opts}{table_columns} )
135             {
136 0           $self->make_table_column_methods($t);
137             }
138              
139 0 0         if ( $self->{opts}{row_columns} )
140             {
141 0           $self->make_row_column_methods($t);
142             }
143 0 0         if ( grep { $self->{opts}{$_} } qw( foreign_keys linking_tables lookup_columns ) )
  0            
144             {
145 0           $self->make_foreign_key_methods($t);
146             }
147              
148 0           foreach ( qw( insert update select delete ) )
149             {
150 0 0         if ( $self->{opts}{"$_\_hooks"} )
151             {
152 0           $self->make_hooks($t, $_);
153             }
154             }
155             }
156             }
157              
158             sub eval_schema_class
159             {
160 0     0 0   my $self = shift;
161              
162 0           eval <<"EOF";
163             package $self->{schema_class};
164              
165             use base qw( Alzabo::Runtime::Schema Alzabo::DocumentationContainer );
166             EOF
167              
168 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
169             }
170              
171             sub eval_table_class
172             {
173 0     0 0   my $self = shift;
174              
175 0           eval <<"EOF";
176             package $self->{table_class};
177              
178             use base qw( $self->{class_root}::Table );
179              
180             sub _row_class { '$self->{row_class}' }
181              
182             EOF
183              
184 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
185             }
186              
187             sub eval_row_class
188             {
189 0     0 0   my $self = shift;
190              
191             # Need to load this so that ->can checks can see them
192 0           require Alzabo::Runtime;
193              
194 0           eval <<"EOF";
195             package $self->{row_class};
196              
197             use base qw( $self->{class_root}::Row Alzabo::DocumentationContainer );
198              
199             EOF
200              
201 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
202             }
203              
204             sub make_table_method
205             {
206 0     0 0   my $self = shift;
207 0           my $t = shift;
208              
209             my $name = $self->_make_method
210             ( type => 'table',
211             class => $self->{schema_class},
212             returns => 'table object',
213 0     0     code => sub { return $t; },
214 0 0         table => $t,
215             ) or return;
216              
217 0           $self->{schema_class}->add_method_docs
218             ( Alzabo::MethodDocs->new
219             ( name => $name,
220             group => 'Methods that return table objects',
221             description => "returns the " . $t->name . " table object",
222             ) );
223             }
224              
225             sub load_class
226             {
227 0     0 0   my $self = shift;
228 0           my $class = shift;
229              
230 0           eval "use $class;";
231              
232 0 0 0       die $@ if $@ && $@ !~ /^Can\'t locate .* in \@INC/;
233             }
234              
235             sub make_table_column_methods
236             {
237 0     0 0   my $self = shift;
238 0           my $t = shift;
239              
240 0           foreach my $c ( sort { $a->name cmp $b->name } $t->columns )
  0            
241             {
242 0           my $col_name = $c->name;
243              
244             my $name = $self->_make_method
245             ( type => 'table_column',
246             class => $self->{table_class},
247             returns => 'column_object',
248              
249             # We can't just return $c because we may need to go
250             # through the alias bits. And we need to use $_[0] for
251             # the same reason.
252 0     0     code => sub { return $_[0]->column($col_name) },
253 0 0         column => $c,
254             ) or next;
255              
256 0           $self->{table_class}->add_method_docs
257             ( Alzabo::MethodDocs->new
258             ( name => $name,
259             group => 'Methods that return column objects',
260             description => "returns the " . $c->name . " column object",
261             ) );
262             }
263             }
264              
265             sub make_row_column_methods
266             {
267 0     0 0   my $self = shift;
268 0           my $t = shift;
269              
270 0           foreach my $c ( sort { $a->name cmp $b->name } $t->columns )
  0            
271             {
272 0           my $col_name = $c->name;
273              
274             my $name = $self->_make_method
275             ( type => 'row_column',
276             class => $self->{row_class},
277             returns => 'scalar value/takes new value',
278 0     0     code => sub { my $self = shift;
279 0 0         if (@_)
280             {
281 0           $self->update( $col_name => $_[0] );
282             }
283 0           return $self->select($col_name); },
284 0 0         column => $c,
285             ) or next;
286              
287 0           $self->{row_class}->add_method_docs
288             ( Alzabo::MethodDocs->new
289             ( name => $name,
290             group => 'Methods that update/return a column value',
291             spec => [ { type => SCALAR } ],
292             description =>
293             "returns the value of the " . $c->name . " column for a row. Given a value, it will also update the row first.",
294             ) );
295             }
296             }
297              
298             sub make_foreign_key_methods
299             {
300 0     0 0   my $self = shift;
301 0           my $t = shift;
302              
303 0           foreach my $other_t ( sort { $a->name cmp $b->name } $t->schema->tables )
  0            
304             {
305 0 0         my @fk = $t->foreign_keys_by_table($other_t)
306             or next;
307              
308 0 0 0       if ( @fk == 2 && $fk[0]->table_from eq $fk[0]->table_to &&
      0        
309             $fk[1]->table_from eq $fk[1]->table_to )
310             {
311 0 0         unless ($fk[0]->is_one_to_one)
312             {
313 0 0         $self->make_self_relation($fk[0]) if $self->{opts}{self_relations};
314             }
315 0           next;
316             }
317              
318 0           foreach my $fk (@fk)
319             {
320 0           $self->_make_fk_method($fk);
321             }
322             }
323             }
324              
325             sub _make_method
326             {
327 0     0     my $self = shift;
328 0           my %p = validate @_, { type => { type => SCALAR },
329             class => { type => SCALAR },
330             returns => { type => SCALAR, optional => 1 },
331             code => { type => CODEREF },
332              
333             # Stuff we can pass through to name_maker
334             foreign_key => { optional => 1 },
335             foreign_key_2 => { optional => 1 },
336             column => { optional => 1 },
337             table => { optional => 1 },
338             parent => { optional => 1 },
339             plural => { optional => 1 },
340             };
341              
342 0 0         my $name = $self->{opts}{name_maker}->( %p )
343             or return;
344              
345 0           my ($code_name, $debug_name) = ("$p{class}::$name",
346             "$p{class}\->$name");
347              
348 0 0         if ( $p{class}->can($name) )
349             {
350 0           warn "MethodMaker: Creating $p{type} method $debug_name will override"
351             . " the method of the same name in the parent class\n";
352             }
353              
354 1     1   7 no strict 'refs'; # We use symbolic references here
  1         2  
  1         2038  
355 0 0         if ( defined &$code_name )
356             {
357             # This should probably always be shown to the user, not just
358             # when debugging mode is turned on, because name clashes can
359             # cause confusion - whichever subroutine happens first will
360             # arbitrarily win.
361              
362 0           warn "MethodMaker: skipping $p{type} method $debug_name, subroutine already exists\n";
363 0           return;
364             }
365              
366 0           if (Alzabo::Debug::METHODMAKER)
367             {
368             my $message = "Making $p{type} method $debug_name";
369             $message .= ": returns $p{returns}" if $p{returns};
370             print STDERR "$message\n";
371             }
372              
373 0           *$code_name = $p{code};
374 0           return $name;
375             }
376              
377             sub _make_fk_method
378             {
379 0     0     my $self = shift;
380 0           my $fk = shift;
381 0           my $table_to = $fk->table_to->name;
382              
383             # The table may be a linking or lookup table. If we are
384             # supposed to make that kind of method we will and then we'll
385             # skip to the next foreign table.
386 0 0         $self->make_linking_table_method($fk)
387             if $self->{opts}{linking_tables};
388              
389 0 0         $self->make_lookup_columns_methods($fk)
390             if $self->{opts}{lookup_columns};
391              
392 0 0         return unless $self->{opts}{foreign_keys};
393              
394 0 0         if ($fk->is_one_to_many)
395             {
396             my $name = $self->_make_method
397             ( type => 'foreign_key',
398             class => $self->{row_class},
399             returns => 'row cursor',
400 0     0     code => sub { my $self = shift;
401 0           return $self->rows_by_foreign_key( foreign_key => $fk, @_ ); },
402 0 0         foreign_key => $fk,
403             plural => 1,
404             ) or return;
405              
406 0           $self->{row_class}->add_method_docs
407             ( Alzabo::MethodDocs->new
408             ( name => $name,
409             group => 'Methods that return cursors for foreign keys',
410             description =>
411             "returns a cursor containing related rows from the " . $fk->table_to->name . " table",
412             spec => 'same as Alzabo::Runtime::Table->rows_where',
413             ) );
414             }
415             # Singular method name
416             else
417             {
418             my $name = $self->_make_method
419             ( type => 'foreign_key',
420             class => $self->{row_class},
421             returns => 'single row',
422 0     0     code => sub { my $self = shift;
423 0           return $self->rows_by_foreign_key( foreign_key => $fk, @_ ); },
424 0 0         foreign_key => $fk,
425             plural => 0,
426             ) or return;
427              
428 0           $self->{row_class}->add_method_docs
429             ( Alzabo::MethodDocs->new
430             ( name => $name,
431             group => 'Methods that return a single row for foreign keys',
432             description =>
433             "returns a single related row from the " . $fk->table_to->name . " table",
434             spec => 'same as Alzabo::Runtime::Table->one_row',
435             ) );
436             }
437             }
438              
439             sub make_self_relation
440             {
441 0     0 0   my $self = shift;
442 0           my $fk = shift;
443              
444 0           my (@pairs, @reverse_pairs);
445 0 0         if ($fk->is_one_to_many)
446             {
447 0           @pairs = map { [ $_->[0], $_->[1]->name ] } $fk->column_pairs;
  0            
448 0           @reverse_pairs = map { [ $_->[1], $_->[0]->name ] } $fk->column_pairs;
  0            
449             }
450             else
451             {
452 0           @pairs = map { [ $_->[1], $_->[0]->name ] } $fk->column_pairs;
  0            
453 0           @reverse_pairs = map { [ $_->[0], $_->[1]->name ] } $fk->column_pairs;
  0            
454             }
455              
456 0           my $table = $fk->table_from;
457              
458             my $name = $self->_make_method
459             ( type => 'self_relation',
460             class => $self->{row_class},
461             returns => 'single row',
462 0     0     code => sub { my $self = shift;
463 0           my @where = map { [ $_->[0], '=', $self->select( $_->[1] ) ] } @pairs;
  0            
464 0           return $table->one_row( where => \@where, @_ ); },
465 0 0         foreign_key => $fk,
466             parent => 1,
467             ) or last;
468              
469 0 0         if ($name)
470             {
471 0           $self->{row_class}->add_method_docs
472             ( Alzabo::MethodDocs->new
473             ( name => $name,
474             group => 'Methods that return a parent row',
475             description =>
476             "a single parent row from the same table",
477             spec => 'same as Alzabo::Runtime::Table->one_row',
478             ) );
479             }
480              
481             $name = $self->_make_method
482             ( type => 'self_relation',
483             class => $self->{row_class},
484             returns => 'row cursor',
485             code =>
486 0     0     sub { my $self = shift;
487 0           my %p = @_;
488 0           my @where = map { [ $_->[0], '=', $self->select( $_->[1] ) ] } @reverse_pairs;
  0            
489 0 0         if ( $p{where} )
490             {
491 0           @where = ( '(', @where, ')' );
492              
493 0           push @where,
494 0 0         Alzabo::Utils::is_arrayref( $p{where}->[0] ) ? @{ $p{where} } : $p{where};
495              
496 0           delete $p{where};
497             }
498 0           return $table->rows_where( where => \@where,
499             %p ); },
500 0 0         foreign_key => $fk,
501             parent => 0,
502             ) or return;
503              
504 0           $self->{row_class}->add_method_docs
505             ( Alzabo::MethodDocs->new
506             ( name => $name,
507             group => 'Methods that return child rows',
508             description =>
509             "a row cursor of child rows from the same table",
510             spec => 'same as Alzabo::Runtime::Table->rows_where',
511             ) );
512             }
513              
514             sub make_linking_table_method
515             {
516 0     0 0   my $self = shift;
517 0           my $fk = shift;
518              
519 0 0         return unless $fk->table_to->primary_key_size == 2;
520              
521             # Find the foreign key from the linking table to the _other_ table
522 0           my $fk_2;
523             {
524 0           my @fk = $fk->table_to->all_foreign_keys;
  0            
525 0 0         return unless @fk == 2;
526              
527             # Get the foreign key that's not the one we already have
528 0 0         $fk_2 = $fk[0]->is_same_relationship_as($fk) ? $fk[1] : $fk[0];
529             }
530              
531 0 0         return unless $fk_2;
532              
533             # Not a linking table unless all the PK columns in the linking
534             # table are part of the link.
535 0 0         return unless $fk->table_to->primary_key_size == $fk->table_to->columns;
536              
537             # Not a linking table unless the PK in the middle table is the
538             # same size as the sum of the two table's PK sizes
539 0 0         return unless ( $fk->table_to->primary_key_size ==
540             ( $fk->table_from->primary_key_size + $fk_2->table_to->primary_key_size ) );
541              
542 0           my $s = $fk->table_to->schema;
543 0           my @t = ( $fk->table_to, $fk_2->table_to );
544 0           my $select = [ $t[1] ];
545              
546             my $name = $self->_make_method
547             ( type => 'linking_table',
548             class => $self->{row_class},
549             returns => 'row cursor',
550             code =>
551 0     0     sub { my $self = shift;
552 0           my %p = @_;
553 0 0         if ( $p{where} )
554             {
555 0 0         $p{where} = [ $p{where} ] unless Alzabo::Utils::is_arrayref( $p{where}[0] );
556             }
557 0           foreach my $pair ( $fk->column_pairs )
558             {
559 0           push @{ $p{where} }, [ $pair->[1], '=', $self->select( $pair->[0]->name ) ];
  0            
560             }
561              
562 0           return $s->join( tables => [[@t, $fk_2]],
563             select => $select,
564             %p ); },
565 0 0         foreign_key => $fk,
566             foreign_key_2 => $fk_2,
567             ) or return;
568              
569 0           $self->{row_class}->add_method_docs
570             ( Alzabo::MethodDocs->new
571             ( name => $name,
572             group => 'Methods that follow a linking table',
573             description =>
574             "a row cursor of related rows from the " . $fk_2->table_to->name . " table, " .
575             "via the " . $fk->table_to->name . " linking table",
576             spec => 'same as Alzabo::Runtime::Table->rows_where',
577             ) );
578             }
579              
580             sub make_lookup_columns_methods
581             {
582 0     0 0   my $self = shift;
583 0           my $fk = shift;
584              
585 0 0         return if $fk->is_one_to_many;
586              
587             # Make sure the relationship is to the foreign table's primary key
588 0           my @to = $fk->columns_to;
589 0 0 0       return unless ( ( scalar grep { $_->is_primary_key } @to ) == @to &&
  0            
590             ( $fk->table_to->primary_key_size == @to ) );
591              
592 0           foreach ( sort { $a->name cmp $b->name } $fk->table_to->columns )
  0            
593             {
594 0 0         next if $_->is_primary_key;
595              
596 0           my $col_name = $_->name;
597              
598             my $name = $self->_make_method
599             ( type => 'lookup_columns',
600             class => $self->{row_class},
601             returns => 'scalar value of column',
602             code =>
603 0     0     sub { my $self = shift;
604 0           my $row = $self->rows_by_foreign_key( foreign_key => $fk, @_ );
605 0 0         return unless $row;
606 0           return $row->select($col_name) },
607 0 0         foreign_key => $fk,
608             column => $_,
609             ) or next;
610              
611 0           $self->{row_class}->add_method_docs
612             ( Alzabo::MethodDocs->new
613             ( name => $name,
614             group => 'Methods that follow a lookup table',
615             description =>
616             "returns the value of " . (join '.', $fk->table_to->name, $col_name) . " for the given row by following the foreign key relationship",
617             spec => 'same as Alzabo::Runtime::Table->rows_where',
618             ) );
619             }
620             }
621              
622             sub make_hooks
623             {
624 0     0 0   my $self = shift;
625 0           my $table = shift;
626 0           my $type = shift;
627              
628 0 0         my $class = $type eq 'insert' ? $self->{table_class} : $self->{row_class};
629              
630 0           my $pre = "pre_$type";
631 0           my $post = "post_$type";
632              
633 0 0 0       return unless $class->can($pre) || $class->can($post);
634              
635 0           my $method = join '::', $class, $type;
636              
637             {
638 1     1   8 no strict 'refs';
  1         2  
  1         2758  
  0            
639 0 0         return if *{$method}{CODE};
  0            
640             }
641              
642 0           print STDERR "Making $type hooks method $class\->$type\n"
643             if Alzabo::Debug::METHODMAKER;
644              
645 0           my $meth = "make_$type\_hooks";
646 0           $self->$meth($table);
647             }
648              
649             sub make_insert_hooks
650             {
651 0     0 0   my $self = shift;
652              
653 0           my $code = '';
654 0           $code .= " return \$s->schema->run_in_transaction( sub {\n";
655 0           $code .= " my \$new;\n";
656 0 0         $code .= " \$s->pre_insert(\\\%p);\n" if $self->{table_class}->can('pre_insert');
657 0           $code .= " \$new = \$s->SUPER::insert(\%p);\n";
658 0 0         $code .= " \$s->post_insert({\%p, row => \$new});\n" if $self->{table_class}->can('post_insert');
659 0           $code .= " return \$new;\n";
660 0           $code .= " } );\n";
661              
662 0           eval <<"EOF";
663             {
664             package $self->{table_class};
665             sub insert
666             {
667             my \$s = shift;
668             my \%p = \@_;
669              
670             $code
671              
672             }
673             }
674             EOF
675              
676 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
677              
678 0           my $hooks =
679             $self->_hooks_doc_string( $self->{table_class}, 'pre_insert', 'post_insert' );
680              
681 0           $self->{table_class}->add_class_docs
682             ( Alzabo::ClassDocs->new
683             ( group => 'Hooks',
684             description => "$hooks",
685             ) );
686             }
687              
688             sub _hooks_doc_string
689             {
690 0     0     my $self = shift;
691 0           my ($class, $hook1, $hook2) = @_;
692              
693 0           my @hooks;
694 0 0         push @hooks, $hook1 if $class->can($hook1);
695              
696 0 0         push @hooks, $hook2 if $class->can($hook2);
697              
698 0           my $hooks = 'has';
699 0 0         $hooks .= @hooks > 1 ? '' : ' a ';
700 0           $hooks .= join ' and ', @hooks;
701 0 0         $hooks .= @hooks > 1 ? ' hooks' : ' hook';
702              
703 0           return $hooks;
704             }
705              
706             sub make_update_hooks
707             {
708 0     0 0   my $self = shift;
709              
710 0           my $code = '';
711 0           $code .= " \$s->schema->run_in_transaction( sub {\n";
712 0 0         $code .= " \$s->pre_update(\\\%p);\n" if $self->{row_class}->can('pre_update');
713 0           $code .= " \$s->SUPER::update(\%p);\n";
714 0 0         $code .= " \$s->post_update(\\\%p);\n" if $self->{row_class}->can('post_update');
715 0           $code .= " } );\n";
716              
717 0           eval <<"EOF";
718             {
719             package $self->{row_class};
720              
721             sub update
722             {
723             my \$s = shift;
724             my \%p = \@_;
725              
726             $code
727              
728             }
729             }
730             EOF
731              
732 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
733              
734 0           my $hooks =
735             $self->_hooks_doc_string( $self->{row_class}, 'pre_update', 'post_update' );
736              
737 0           $self->{row_class}->add_class_docs
738             ( Alzabo::ClassDocs->new
739             ( group => 'Hooks',
740             description => "$hooks",
741             ) );
742             }
743              
744             sub make_select_hooks
745             {
746 0     0 0   my $self = shift;
747              
748 0           my ($pre, $post) = ('', '');
749 0 0         $pre = " \$s->pre_select(\\\@cols);\n"
750             if $self->{row_class}->can('pre_update');
751              
752 0 0         $post = " \$s->post_select(\\\%r);\n"
753             if $self->{row_class}->can('post_update');
754              
755 0           eval <<"EOF";
756             {
757             package $self->{row_class};
758              
759             sub select
760             {
761             my \$s = shift;
762             my \@cols = \@_;
763              
764             return \$s->schema->run_in_transaction( sub {
765              
766             $pre
767              
768             my \@r;
769             my %r;
770              
771             if (wantarray)
772             {
773             \@r{ \@cols } = \$s->SUPER::select(\@cols);
774             }
775             else
776             {
777             \$r{ \$cols[0] } = (scalar \$s->SUPER::select(\$cols[0]));
778             }
779             $post
780             return wantarray ? \@r{\@cols} : \$r{ \$cols[0] };
781             } );
782             }
783              
784             sub select_hash
785             {
786             my \$s = shift;
787             my \@cols = \@_;
788              
789             return \$s->schema->run_in_transaction( sub {
790              
791             $pre
792              
793             my \%r = \$s->SUPER::select_hash(\@cols);
794              
795             $post
796              
797             return \%r;
798             } );
799             }
800             }
801             EOF
802              
803 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
804              
805 0           my $hooks =
806             $self->_hooks_doc_string( $self->{row_class}, 'pre_select', 'post_select' );
807              
808 0           $self->{row_class}->add_class_docs
809             ( Alzabo::ClassDocs->new
810             ( group => 'Hooks',
811             description => "$hooks",
812             ) );
813             }
814              
815             sub make_delete_hooks
816             {
817 0     0 0   my $self = shift;
818              
819 0           my $code = '';
820 0           $code .= " \$s->schema->run_in_transaction( sub {\n";
821 0 0         $code .= " \$s->pre_delete(\\\%p);\n" if $self->{row_class}->can('pre_delete');
822 0           $code .= " \$s->SUPER::delete(\%p);\n";
823 0 0         $code .= " \$s->post_delete(\\\%p);\n" if $self->{row_class}->can('post_delete');
824 0           $code .= " } );\n";
825              
826 0           eval <<"EOF";
827             package $self->{row_class};
828              
829             sub delete
830             {
831             my \$s = shift;
832             my \%p = \@_;
833              
834             $code
835              
836             }
837             EOF
838              
839 0 0         Alzabo::Exception::Eval->throw( error => $@ ) if $@;
840              
841 0           my $hooks =
842             $self->_hooks_doc_string( $self->{row_class}, 'pre_delete', 'post_delete' );
843              
844 0           $self->{row_class}->add_class_docs
845             ( Alzabo::ClassDocs->new
846             ( group => 'Hooks',
847             description => "$hooks",
848             ) );
849             }
850              
851             sub name
852             {
853 0     0 0   my $self = shift;
854 0           my %p = @_;
855              
856 0 0         return $p{table}->name if $p{type} eq 'table';
857              
858 0 0         return $p{column}->name if $p{type} eq 'table_column';
859              
860 0 0         return $p{column}->name if $p{type} eq 'row_column';
861              
862 0 0         if ( $p{type} eq 'foreign_key' )
863             {
864 0           return $p{foreign_key}->table_to->name;
865             }
866              
867 0 0         if ( $p{type} eq 'linking_table' )
868             {
869 0           my $method = $p{foreign_key}->table_to->name;
870 0           my $tname = $p{foreign_key}->table_from->name;
871 0           $method =~ s/^$tname\_?//;
872 0           $method =~ s/_?$tname$//;
873              
874 0           return $method;
875             }
876              
877 0 0         return join '_', map { lc $_->name } $p{foreign_key}->table_to, $p{column}
  0            
878             if $p{type} eq 'lookup_columns';
879              
880 0 0         return $p{column}->name if $p{type} eq 'lookup_columns';
881              
882 0 0         return $p{parent} ? 'parent' : 'children'
    0          
883             if $p{type} eq 'self_relation';
884              
885 0           die "unknown type in call to naming sub: $p{type}\n";
886             }
887              
888             package Alzabo::DocumentationContainer;
889              
890             my %store;
891             sub add_method_docs
892             {
893 0     0     my $class = shift;
894              
895 0           my $docs = shift;
896              
897 0           my $store = $class->_get_store($class);
898              
899 0           my $group = $docs->group;
900 0           my $name = $docs->name;
901              
902 0   0       $store->{methods}{by_group}{$group} ||= Tie::IxHash->new;
903 0           $store->{methods}{by_group}{$group}->Push( $name => $docs );
904              
905 0   0       $store->{methods}{by_name} ||= Tie::IxHash->new;
906 0           $store->{methods}{by_name}->Push( $name => $docs );
907             }
908              
909             sub add_class_docs
910             {
911 0     0     my $class = shift;
912              
913 0           my $docs = shift;
914              
915 0           my $store = $class->_get_store($class);
916              
917 0           my $group = $docs->group;
918              
919 0   0       $store->{class}{by_group}{$group} ||= [];
920 0           push @{ $store->{class}{by_group}{$group} }, $docs;
  0            
921             }
922              
923             sub add_contained_class
924             {
925 0     0     my $class = shift;
926              
927 0           my ($type, $contained) = @_;
928              
929 0           my $store = $class->_get_store($class);
930              
931 0           push @{ $store->{contained_classes}{by_name} }, $contained;
  0            
932              
933 0           push @{ $store->{contained_classes}{by_type}{$type} }, $contained;
  0            
934             }
935              
936             sub _get_store
937             {
938 0     0     my $class = shift;
939 0   0       $class = ref $class || $class;
940              
941 0   0       $store{$class} ||= {};
942              
943 0           return $store{$class};
944             }
945              
946             sub method_names
947             {
948 0     0     my $class = shift;
949              
950 0           my $store = $class->_get_store($class);
951              
952 0           return $store->{methods}{by_name}->Keys;
953             }
954              
955             sub methods_by_name
956             {
957 0     0     my $class = shift;
958              
959 0           my $store = $class->_get_store($class);
960              
961 0           return $store->{methods}{by_name}->Values;
962             }
963              
964             sub method_groups
965             {
966 0     0     my $class = shift;
967              
968 0           my $store = $class->_get_store($class);
969              
970 0           return keys %{ $store->{methods}{by_group} };
  0            
971             }
972              
973             sub methods_by_group
974             {
975 0     0     my $class = shift;
976              
977 0           my $store = $class->_get_store($class);
978              
979 0           my $group = shift;
980              
981 0 0         return $store->{methods}{by_group}{$group}->Values
982             if exists $store->{methods}{by_group}{$group};
983             }
984              
985             sub class_groups
986             {
987 0     0     my $class = shift;
988              
989 0           my $store = $class->_get_store($class);
990              
991 0           return keys %{ $store->{class}{by_group} };
  0            
992             }
993              
994             sub class_docs_by_group
995             {
996 0     0     my $class = shift;
997              
998 0           my $store = $class->_get_store($class);
999              
1000 0           my $group = shift;
1001              
1002 0 0         return @{ $store->{class}{by_name}{$group} }
  0            
1003             if exists $store->{class}{by_name}{$group};
1004             }
1005              
1006             sub class_docs
1007             {
1008 0     0     my $class = shift;
1009              
1010 0           my $store = $class->_get_store($class);
1011              
1012 0           my $group = shift;
1013              
1014 0           return map { @{ $store->{class}{by_group}{$_} } }
  0            
  0            
1015 0           keys %{ $store->{class}{by_group} };
1016             }
1017              
1018             sub contained_classes
1019             {
1020 0     0     my $class = shift;
1021              
1022 0           my $store = $class->_get_store($class);
1023              
1024 0 0         return @{ $store->{contained_classes}{by_name} }
  0            
1025             if exists $store->{contained_classes}{by_name};
1026              
1027 0           return;
1028             }
1029              
1030             sub method
1031             {
1032 0     0     my $class = shift;
1033              
1034 0           my $store = $class->_get_store($class);
1035              
1036 0           my $name = shift;
1037              
1038 0 0         return $store->{methods}{by_name}->FETCH($name)
1039             if exists $store->{methods}{by_name};
1040             }
1041              
1042             sub docs_as_pod
1043             {
1044 0     0     my $self = shift;
1045 0   0       my $class = ref $self || $self;
1046 0           my $contained = shift;
1047              
1048 0           my $store = $class->_get_store($class);
1049              
1050 0           my $pod;
1051              
1052 0 0         $pod .= "=pod\n\n" unless $contained;
1053              
1054 0           $pod .= "=head1 $class\n\n";
1055              
1056 0           foreach my $class_doc ( $class->class_docs )
1057             {
1058 0           $pod .= $class_doc->as_pod;
1059             }
1060              
1061 0           foreach my $group ( $class->method_groups )
1062             {
1063 0           $pod .= "=head2 $group\n\n";
1064              
1065 0           foreach my $method ( $class->methods_by_group($group) )
1066             {
1067 0           $pod .= $method->as_pod;
1068             }
1069             }
1070              
1071 0           $pod .= $_ foreach $self->contained_docs;
1072              
1073 0 0         $pod .= "=cut\n\n" unless $contained;
1074              
1075 0           return $pod;
1076             }
1077              
1078             sub contained_docs
1079             {
1080 0     0     my $self = shift;
1081              
1082 0           return map { $_->docs_as_pod(1) } $self->contained_classes;
  0            
1083             }
1084              
1085             package Alzabo::Docs;
1086              
1087 0     0     sub group { shift->{group} }
1088 0     0     sub description { shift->{description} }
1089              
1090             # copied from Params::ValidatePP
1091             {
1092             my %type_to_string =
1093             ( Params::Validate::SCALAR() => 'scalar',
1094             Params::Validate::ARRAYREF() => 'arrayref',
1095             Params::Validate::HASHREF() => 'hashref',
1096             Params::Validate::CODEREF() => 'coderef',
1097             Params::Validate::GLOB() => 'glob',
1098             Params::Validate::GLOBREF() => 'globref',
1099             Params::Validate::SCALARREF() => 'scalarref',
1100             Params::Validate::UNDEF() => 'undef',
1101             Params::Validate::OBJECT() => 'object',
1102             );
1103              
1104             sub _typemask_to_strings
1105             {
1106 0     0     shift;
1107 0           my $mask = shift;
1108              
1109 0           my @types;
1110 0           foreach ( Params::Validate::SCALAR, Params::Validate::ARRAYREF,
1111             Params::Validate::HASHREF, Params::Validate::CODEREF,
1112             Params::Validate::GLOB, Params::Validate::GLOBREF,
1113             Params::Validate::SCALARREF, Params::Validate::UNDEF,
1114             Params::Validate::OBJECT )
1115             {
1116 0 0         push @types, $type_to_string{$_} if $mask & $_;
1117             }
1118 0 0         return @types ? @types : ('unknown');
1119             }
1120             }
1121              
1122             package Alzabo::MethodDocs;
1123              
1124 1     1   9 use Params::Validate qw( validate SCALAR ARRAYREF HASHREF );
  1         3  
  1         85  
1125              
1126 1     1   7 use base qw(Alzabo::Docs);
  1         2  
  1         1132  
1127              
1128             sub new
1129             {
1130 0     0     my $class = shift;
1131 0           my %p = validate( @_, { name => { type => SCALAR },
1132             group => { type => SCALAR },
1133             description => { type => SCALAR },
1134             spec => { type => SCALAR | ARRAYREF | HASHREF,
1135             default => undef },
1136             } );
1137              
1138 0           return bless \%p, $class;
1139             }
1140              
1141 0     0     sub name { shift->{name} }
1142 0     0     sub spec { shift->{spec} }
1143              
1144             sub as_pod
1145             {
1146 0     0     my $self = shift;
1147              
1148 0           my $desc = ucfirst $self->{description};
1149              
1150 0           my $spec = $self->spec;
1151              
1152 0           my $params;
1153 0 0         if ( defined $spec )
1154             {
1155 0 0         if ( Alzabo::Utils::is_arrayref( $spec ) )
    0          
1156             {
1157 0           $params = "=over 4\n\n";
1158              
1159 0           foreach my $p (@$spec)
1160             {
1161 0           $params .= "=item * ";
1162 0 0         if ( exists $p->{type} )
1163             {
1164             # hack!
1165 0           my $types =
1166             join ', ', $self->_typemask_to_strings( $p->{type} );
1167 0           $params .= "($types)";
1168             }
1169 0           $params .= "\n\n";
1170             }
1171              
1172 0           $params .= "=back\n\n";
1173             }
1174             elsif ( Alzabo::Utils::is_hashref($spec) )
1175             {
1176 0           $params = "=over 4\n\n";
1177              
1178 0           while ( my ($name, $p) = each %$spec )
1179             {
1180 0           $params .= "=item * $name ";
1181 0 0         if ( exists $p->{type} )
1182             {
1183             # hack!
1184 0           my $types =
1185             join ', ', $self->_typemask_to_strings( $p->{type} );
1186 0           $params .= "($types)";
1187             }
1188 0           $params .= "\n\n";
1189             }
1190              
1191 0           $params .= "=back\n\n";
1192             }
1193             else
1194             {
1195 0           $params = "Parameters: $spec\n\n";
1196             }
1197             }
1198              
1199 0           my $pod = <<"EOF";
1200             =head3 $self->{name}
1201              
1202             $desc
1203              
1204             EOF
1205 0 0         $pod .= $params if $params;
1206              
1207 0           return $pod;
1208             }
1209              
1210              
1211             package Alzabo::ClassDocs;
1212              
1213 1     1   7 use Params::Validate qw( validate SCALAR );
  1         2  
  1         52  
1214              
1215 1     1   7 use base qw(Alzabo::Docs);
  1         2  
  1         702  
1216              
1217             sub new
1218             {
1219 0     0     my $class = shift;
1220 0           my %p = validate( @_, { group => { type => SCALAR },
1221             description => { type => SCALAR },
1222             } );
1223              
1224 0           return bless \%p, $class;
1225             }
1226              
1227             sub as_pod
1228             {
1229 0     0     my $self = shift;
1230              
1231 0           return ucfirst "$self->{description}\n\n";
1232             }
1233              
1234             1;
1235              
1236              
1237             __END__