turnkey producer still broken, will try to get it working on the plane...
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / Turnkey.pm
1 package Turnkey::Node;
2
3 use strict;
4 use Class::MakeMethods::Template::Hash (
5   new => [ 'new' ],
6   'array_of_objects -class Turnkey::Edge' => [ qw( edges ) ],
7   'array_of_objects -class Turnkey::CompoundEdge' => [ qw( compoundedges ) ],
8   'array_of_objects -class Turnkey::HyperEdge' => [ qw( hyperedges ) ],
9   'hash' => [ qw( many via has) ],
10   scalar => [ qw( base name order primary_key primary_key_accessor table) ],
11 );
12
13
14 package Turnkey::Edge;
15
16 use strict;
17 use Class::MakeMethods::Template::Hash (
18   new => ['new'],
19   scalar => [ qw( type ) ],
20   array => [ qw( traversals ) ],
21   object => [
22                          'thisfield'    => {class => 'SQL::Translator::Schema::Field'},
23                          'thatfield'    => {class => 'SQL::Translator::Schema::Field'},
24                          'thisnode'     => {class => 'Turnkey::Node'},
25                          'thatnode'     => {class => 'Turnkey::Node'},
26
27                         ],
28 );
29
30 sub flip {
31   my $self = shift;
32   return Turnkey::Edge->new( thisfield => $self->thatfield,
33                                                          thatfield => $self->thisfield,
34                                                          thisnode  => $self->thatnode,
35                                                          thatnode  => $self->thisnode,
36                                                          type => $self->type eq 'import' ? 'export' : 'import'
37                                                    );
38 }
39
40
41 package Turnkey::HyperEdge;
42
43 use strict;
44 use base qw(Turnkey::Edge);
45 use Class::MakeMethods::Template::Hash (
46   'array_of_objects -class SQL::Translator::Schema::Field' => [ qw( thisviafield thatviafield thisfield thatfield) ],
47   'array_of_objects -class Turnkey::Node'                  => [ qw( thisnode thatnode ) ],
48   object => [ 'vianode' => {class => 'Turnkey::Node'} ],
49 );
50
51
52 package Turnkey::CompoundEdge;
53
54 use strict;
55 use base qw(Turnkey::Edge);
56 use Class::MakeMethods::Template::Hash (
57   new => ['new'],
58   object => [
59                          'via'  => {class => 'Turnkey::Node'},
60                         ],
61   'array_of_objects -class Turnkey::Edge' => [ qw( edges ) ],
62 );
63
64
65 package SQL::Translator::Producer::Turnkey;
66
67 use strict;
68 use vars qw[ $VERSION $DEBUG ];
69 $VERSION = sprintf "%d.%02d", q$Revision: 1.6 $ =~ /(\d+)\.(\d+)/;
70 $DEBUG   = 1 unless defined $DEBUG;
71
72 use SQL::Translator::Schema::Constants;
73 use SQL::Translator::Utils qw(header_comment);
74 use Data::Dumper;
75 use Template;
76
77 my %CDBI_auto_pkgs = (
78     MySQL      => 'mysql',
79     PostgreSQL => 'Pg',
80     Oracle     => 'Oracle',
81 );
82
83 # -------------------------------------------------------------------
84 sub produce {
85     my $t             = shift;
86         my $create        = undef;
87     my $no_comments   = $t->no_comments;
88     my $schema        = $t->schema;
89     my $args          = $t->producer_args;
90
91         my $parser_type   = (split /::/, $t->parser_type)[-1];
92
93     local $DEBUG      = $t->debug;
94
95         my %meta          = (
96                                                  format_fk => $t->format_fk_name,
97                                                  template  => $args->{'template'}      || '',
98                                                  baseclass => $args->{'main_pkg_name'} || $t->format_package_name('DBI'),
99                                                  db_user   => $args->{'db_user'}       || '',
100                                                  db_pass   => $args->{'db_pass'}       || '',
101                                                  parser    => $t->parser_type,
102                                                  producer  => __PACKAGE__,
103                                                  dsn       => $args->{'dsn'} || sprintf( 'dbi:%s:_', $CDBI_auto_pkgs{ $parser_type }
104                                                                                                                                  ? $CDBI_auto_pkgs{ $parser_type }
105                                                                                                                                  : $parser_type
106                                                                                                                            )
107                                                  );
108
109
110         #
111         # build package objects
112         #
113         my %nodes;
114         my $order;
115         foreach my $table ($schema->get_tables){
116
117           die __PACKAGE__." table ".$table->name." doesn't have a primary key!" unless $table->primary_key;
118           die __PACKAGE__." table ".$table->name." can't have a composite primary key!" if ($table->primary_key->fields)[1];
119
120           my $node = Turnkey::Node->new();
121           $nodes{ $table->name } = $node;
122
123           $node->order( ++$order );
124           $node->name( $t->format_package_name($table->name) );
125           $node->base( $meta{'baseclass'} );
126           $node->table( $table );
127           $node->primary_key( ($table->primary_key->fields)[0] );
128           # Primary key may have a differenct accessor method name
129           $node->primary_key_accessor(
130                                                                   defined($t->format_pk_name)
131                                                                   ? $t->format_pk_name->( $node->name, $node->primary_key )
132                                                                   : undef
133                                                                  );
134         }
135
136         foreach my $node (values %nodes){
137           foreach my $field ($node->table->get_fields){
138                 next unless $field->is_foreign_key;
139
140                 my $that = $nodes{ $field->foreign_key_reference->reference_table };
141                 #this means we have an incomplete schema
142                 next unless $that;
143
144                 my $edge = Turnkey::Edge->new(
145                                                                           type => 'import',
146                                                                           thisnode => $node,
147                                                                           thisfield => $field,
148                                                                           thatnode => $that,
149                                                                           thatfield => ($field->foreign_key_reference->reference_fields)[0]
150                                                                          );
151
152
153                 $node->has($that->name, $node->has($that->name)+1);
154                 $that->many($node->name, $that->many($node->name)+1);
155
156
157                 $node->push_edges( $edge );
158                 $that->push_edges( $edge->flip );
159           }
160         }
161
162         #
163         # type MM relationships
164         #
165         foreach my $lnode (sort values %nodes){
166           next if $lnode->table->is_data;
167           foreach my $inode1 (sort values %nodes){
168                 next if $inode1 eq $lnode;
169
170                 my @inode1_imports = grep { $_->type eq 'import' and $_->thatnode eq $inode1 } $lnode->edges;
171                 next unless @inode1_imports;
172
173                 foreach my $inode2 (sort values %nodes){
174                   my %i = map {$_->thatnode->name => 1} grep { $_->type eq 'import'} $lnode->edges;
175                   if(scalar(keys %i) == 1) {
176                   } else {
177                         last if $inode1 eq $inode2;
178                   }
179
180                   next if $inode2 eq $lnode;
181                   my @inode2_imports =  grep { $_->type eq 'import' and $_->thatnode eq $inode2 } $lnode->edges;
182                   next unless @inode2_imports;
183
184                   my $cedge = Turnkey::CompoundEdge->new();
185                   $cedge->via($lnode);
186
187                   $cedge->push_edges( map {$_->flip} grep {$_->type eq 'import' and ($_->thatnode eq $inode1 or $_->thatnode eq $inode2)} $lnode->edges);
188
189                   if(scalar(@inode1_imports) == 1 and scalar(@inode2_imports) == 1){
190                         $cedge->type('one2one');
191
192                         $inode1->via($inode2->name,$inode1->via($inode2->name)+1);
193                         $inode2->via($inode1->name,$inode2->via($inode1->name)+1);
194                   }
195                   elsif(scalar(@inode1_imports)  > 1 and scalar(@inode2_imports) == 1){
196                         $cedge->type('many2one');
197
198                         $inode1->via($inode2->name,$inode1->via($inode2->name)+1);
199                         $inode2->via($inode1->name,$inode2->via($inode1->name)+1);
200                   }
201                   elsif(scalar(@inode1_imports) == 1 and scalar(@inode2_imports)  > 1){
202                         #handled above
203                   }
204                   elsif(scalar(@inode1_imports)  > 1 and scalar(@inode2_imports)  > 1){
205                         $cedge->type('many2many');
206
207                         $inode1->via($inode2->name,$inode1->via($inode2->name)+1);
208                         $inode2->via($inode1->name,$inode2->via($inode1->name)+1);
209                   }
210
211                   $inode1->push_compoundedges($cedge);
212                   $inode2->push_compoundedges($cedge) unless $inode1 eq $inode2;
213
214                 }
215           }
216         }
217
218     #
219     # create methods
220     #
221         foreach my $node_from (values %nodes){
222           next unless $node_from->table->is_data;
223           foreach my $cedge ( $node_from->compoundedges ){
224                 my $hyperedge = Turnkey::HyperEdge->new;
225
226                 my $node_to;
227
228                 foreach my $edge ($cedge->edges){
229                   if($edge->thisnode->name eq $node_from->name){
230                         $hyperedge->vianode($edge->thatnode);
231
232                         if($edge->thatnode->name ne $cedge->via->name){
233                           $node_to ||= $nodes{ $edge->thatnode->table->name };
234                         }
235
236                           $hyperedge->push_thisnode($edge->thisnode);
237                           $hyperedge->push_thisfield($edge->thisfield);
238                           $hyperedge->push_thisviafield($edge->thatfield);
239
240                   } else {
241
242                         if($edge->thisnode->name ne $cedge->via->name){
243                           $node_to ||= $nodes{ $edge->thisnode->table->name };
244                         }
245
246                           $hyperedge->push_thatnode($edge->thisnode);
247                           $hyperedge->push_thatfield($edge->thisfield);
248                           $hyperedge->push_thatviafield($edge->thatfield);
249                   }
250                 }
251
252                    if($hyperedge->count_thisnode == 1 and $hyperedge->count_thatnode == 1){ $hyperedge->type('one2one')   }
253                 elsif($hyperedge->count_thisnode  > 1 and $hyperedge->count_thatnode == 1){ $hyperedge->type('many2one')  }
254                 elsif($hyperedge->count_thisnode == 1 and $hyperedge->count_thatnode  > 1){ $hyperedge->type('one2many')  }
255                 elsif($hyperedge->count_thisnode  > 1 and $hyperedge->count_thatnode  > 1){ $hyperedge->type('many2many') }
256
257 #warn $node_from->name ."\t". $node_to->name ."\t". $hyperedge->type ."\t". $hyperedge->vianode->name;
258
259                 $node_from->push_hyperedges($hyperedge);
260           }
261         }
262
263         $meta{"nodes"} = \%nodes;
264         return(translateForm($t, \%meta));
265 }
266
267 ###########################################
268 # Here documents for the tt2 templates    #
269 ###########################################
270
271 my $turnkey_dbi_tt2 = <<EOF;
272 [% MACRO printPackage(node) BLOCK %]
273 # --------------------------------------------
274
275 package [% node.name %];
276 use base '[% node.base %]';
277 use Class::DBI::Pager;
278
279 [% node.name %]->set_up_table('[% node.table.name %]');
280 [% printPKAccessors(node.primary_key, node.table.name) %]
281 [% printHasA(node.edges, node) %]
282 [% printHasMany(node.edges, node) %]
283 [% printHasCompound(node.compoundedges, node.hyperedges, node.name) %]
284 [% END %]
285
286 [% MACRO printPKAccessors(array, name) BLOCK %]
287 #
288 # Primary key accessors
289 #
290 [% FOREACH item = array %]
291 sub id { shift->[% item %] }
292 sub [% name %] { shift->[% item %] }
293 [% END %]
294 [% END %]
295
296 [% MACRO printHasA(edges, name) BLOCK %]
297 #
298 # Has A
299 #
300 [% FOREACH edge = edges %]
301   [%- IF edge.type == 'import' -%]
302 [% node.name %]->has_a([% edge.thisfield.name %] => '[% edge.thatnode.name %]');
303     [%- IF node.has(edge.thatnode.name) < 2 %]
304 sub [% edge.thatnode.table.name %] { return shift->[% edge.thisfield.name %] }
305     [%- ELSE %]
306 sub [% format_fk(edge.thisnode.table.name,edge.thisfield.name) %] { return shift->[% edge.thisfield.name %] }
307     [%- END %]
308   [%- END %]
309 [% END %]
310 [% END %]
311
312 [% MACRO printHasMany(edges, node) BLOCK %]
313 #
314 # Has Many
315 #
316 [% FOREACH edge = edges %]
317   [%- IF edge.type == 'export' -%]
318 [% node.name %]->has_many([% edge.thatnode.table.name %]_[% edge.thatfield.name %], '[% edge.thatnode.name %]' => '[% edge.thatfield.name %]');
319     [%- IF node.via(edge.thatnode.name) >= 1 %]
320 sub [% edge.thatnode.table.name %]_[% format_fk(edge.thatnode.table.name,edge.thatfield.name) %]s { return shift->[% edge.thatnode.table.name %]_[% edge.thatfield.name %] }
321     [%- ELSIF edge.thatnode.table.is_data %]
322 sub [% edge.thatnode.table.name %]s { return shift->[% edge.thatnode.table.name %]_[% edge.thatfield.name %] }
323     [%- END %]
324   [%- END %]
325 [% END %]
326 [% END %]
327
328 [% MACRO printHasCompound(cedges,hedges,name) BLOCK %]
329 #
330 # Has Compound Many
331 #
332 [% FOREACH cedge = cedges %]
333 [% FOREACH edge = cedge.edges %]
334   [%- NEXT IF edge.thisnode.name != name -%]
335 sub [% cedge.via.table.name %]_[% format_fk(edge.thatnode.table.name,edge.thatfield.name) %]s { return shift->[% cedge.via.table.name %]_[% edge.thatfield.name %] }
336 [% END %]
337 [% END %]
338 [% FOREACH h = hedges %]
339   [%- NEXT IF h.thisnode.name != name -%]
340   [%- IF h.type == 'one2one' %]
341 1sub [% h.thatnode.table.name %]s { my \$self = shift; return map \$_->[% h.thatviafield.name %], \$self->[% h.vianode.table.name %]_[% h.thisviafield.name %] }
342   [%- ELSIF h.type == 'one2many' %]
343 2
344   [%- ELSIF h.type == 'many2one' %]
345 3sub [% h.thatnode.table.name %]s { my \$self = shift; return map \$_->[% h.thatviafield.name %], \$self->[% h.vianode.table.name %]_[% h.thisviafield.name %] }
346   [%- ELSIF h.type == 'many2many' %]
347 4
348   [%- END %]
349 [% END %]
350 [% END %]
351
352 [% MACRO printList(array) BLOCK %][% FOREACH item = array %][% item %] [% END %][% END %]
353 package [% baseclass %];
354
355 # Created by SQL::Translator::Producer::Turnkey
356 # Template used: classdbi
357
358 use strict;
359 use base qw(Class::DBI::Pg);
360
361 Durian::Model::DBI->set_db('Main', '[% db_str  %]', '[% db_user %]', '[% db_pass %]');
362
363 [% FOREACH node = nodes %]
364     [% printPackage(node.value) %]
365 [% END %]
366 EOF
367
368 my $turnkey_atom_tt2 = <<'EOF';
369 [% ###### DOCUMENT START ###### %]
370
371 [% FOREACH node = linkable %]
372
373 ##############################################
374
375 package Durian::Atom::[% node.key FILTER ucfirst %];
376
377 [% pname = node.key FILTER ucfirst%]
378 [% pkey = "Durian::Model::${pname}" %]
379
380 use base qw(Durian::Atom);
381 use Data::Dumper;
382
383 sub can_render {
384         return 1;
385 }
386
387 sub render {
388         my $self = shift;
389         my $dbobject = shift;
390     # Assumption here that if it's not rendering on it's own dbobject
391     # then it's a list. This will be updated when AtomLists are implemented -boconnor
392         if(ref($dbobject) eq 'Durian::Model::[% node.key FILTER ucfirst %]') {
393                 return(_render_record($dbobject));
394         }
395         else { return(_render_list($dbobject)); }
396 }
397
398 sub _render_record {
399         my $dbobject = shift;
400         my @output = ();
401         my $row = {};
402         my $field_hash = {};
403         [% FOREACH field = nodes.$pkey.columns_essential %]
404         $field_hash->{[% field %]} = $dbobject->[% field %]();
405     [% END %]
406         $row->{data} = $field_hash;
407         $row->{id} = $dbobject->id();
408         push @output, $row;
409         return(\@output);
410 }
411
412 sub _render_list {
413         my $dbobject = shift;
414         my @output = ();
415         my @objects = $dbobject->[% node.key %]s;
416         foreach my $object (@objects)
417     {
418                 my $row = {};
419             my $field_hash = {};
420           [% FOREACH field = nodes.$pkey.columns_essential %]
421                 $field_hash->{[% field %]} = $object->[% field %]();
422           [% END %]
423                 $row->{data} = $field_hash;
424             $row->{id} = $object->id();
425             push @output, $row;
426     }
427         return(\@output);
428 }
429
430 sub head {
431         return 1;
432 }
433
434 1;
435
436 [% END %]
437 EOF
438
439 my $turnkey_xml_tt2 = <<EOF;
440 <?xml version="1.0" encoding="UTF-8"?>
441 <!DOCTYPE Durian SYSTEM "Durian.dtd">
442 <Durian>
443
444 <!-- The basic layout is fixed -->
445   <container bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" height="90%" orientation="vertical" type="root" width="100%" xlink:label="RootContainer">
446         <container cellpadding="3" cellspacing="0" orientation="horizontal" type="container" height="100%" width="100%" xlink:label="MiddleContainer">
447           <container align="center" cellpadding="2" cellspacing="0" class="leftbar" orientation="vertical" type="minor" width="0%" xlink:label="MidLeftContainer"/>
448           <container cellpadding="0" cellspacing="0" orientation="vertical" width="100%" type="major" xlink:label="MainContainer"/>
449         </container>
450   </container>
451
452 <!-- Atom Classes -->
453 [% FOREACH node = linkable %]
454   <atom class="Durian::Atom::[% node.key FILTER ucfirst %]"  name="[% node.key FILTER ucfirst %]" xlink:label="[% node.key FILTER ucfirst %]Atom"/>
455 [%- END -%]
456
457 <!-- Atom Bindings -->
458 <atomatombindings>
459 [% FOREACH focus_atom = linkable %]
460   [% FOREACH link_atom = focus_atom.value %]
461   <atomatombinding xlink:from="#[% focus_atom.key FILTER ucfirst %]Atom" xlink:to="#[% link_atom.key FILTER ucfirst %]Atom" xlink:label="[% focus_atom.key FILTER ucfirst %]Atom2[% link_atom.key FILTER ucfirst %]Atom"/>
462   [%- END -%]
463 [%- END -%]
464 </atomatombindings>
465
466 <atomcontainerbindings>
467 [% FOREACH focus_atom = linkable %]
468   <atomcontainerbindingslayout xlink:label="Durian::Model::[% focus_atom.key FILTER ucfirst %]">
469   [% FOREACH link_atom = focus_atom.value %]
470     <atomcontainerbinding xlink:from="#MidLeftContainer" xlink:label="MidLeftContainer2[% link_atom.key FILTER ucfirst %]Atom"  xlink:to="#[% link_atom.key FILTER ucfirst %]Atom"/>
471   [%- END -%]
472   <atomcontainerbinding xlink:from="#MainContainer"    xlink:label="MainContainer2[% focus_atom.key FILTER ucfirst %]Atom"    xlink:to="#[% focus_atom.key FILTER ucfirst %]Atom"/>
473   </atomcontainerbindingslayout>
474   [%- END -%]
475 </atomcontainerbindings>
476
477 <uribindings>
478   <uribinding uri="/" class="Durian::Util::Frontpage"/>
479 </uribindings>
480
481 <classbindings>
482 [% FOREACH focus_atom = linkable %]
483    <classbinding class="Durian::Model::[% focus_atom.key FILTER ucfirst %]" plugin="#[% focus_atom.key FILTER ucfirst %]Atom" rank="0"/>
484 [%- END -%]
485
486 </classbindings>
487
488 </Durian>
489 EOF
490
491 my $turnkey_template_tt2 = <<'EOF';
492 [% TAGS [- -] %]
493 [% MACRO renderpanel(panel,dbobject) BLOCK %]
494   <!-- begin panel: [% panel.label %] -->
495   <table border="0" width="[% panel.width %]" height="[% panel.height %]" bgcolor="[% panel.bgcolor %]" valign="top" cellpadding="[% panel.cellpadding %]" cellspacing="[% panel.cellspacing %]" align="[% panel.align %]" valign="[% panel.valign %]">
496     <tr>
497     [% FOREACH p = panel.containers %]
498       [% IF p.can_render(panel) %]
499         <td valign="top" class="[% p.class %]" align="[% panel.align %]" height="[% p.height || 1 %]" width="[% p.width %]">
500           [% IF p.type == 'Container' %]
501             [% renderpanel(p,dbobject) %]
502           [% ELSE %]
503             <table cellpadding="0" cellspacing="0" align="left" height="100%" width="100%">
504               [% IF p.name %]
505                 <tr bgcolor="#4444FF" height="1">
506                   <td><font color="#FFFFFF">[% p.name %][% IF panel.type == 'major' %]: [% dbobject.name %][% END %]</font></td>
507                   <td align="right" width="0"><!--<nobr><img src="/images/v.gif"/><img src="/images/^.gif"/>[% IF p.delible == 'yes' %]<img src="/images/x.gif"/>[% END %]</nobr>--></td>
508                 </tr>
509               [% END %]
510               <tr><td colspan="2" bgcolor="#FFFFFF">
511               <!-- begin atom: [% p.label %] -->
512               <table cellpadding="0" cellspacing="0" align="left" height="100%" width="100%"><!-- [% ref(atom) %] [% ref(dbobject) %] -->
513                 [% renderatom(p,dbobject) %] <!-- used to be renderplugin(p,panel) -->
514               </table>
515             </table>
516           [% END %]
517         </td>
518         [% IF panel.orientation == 'vertical' %]
519           </tr><tr>
520         [% END %]
521       [% END %]
522     [% END %]
523     </tr>
524   </table>
525   <!-- end panel: [% panel.label %] -->
526 [% END %]
527 [% MACRO renderatom(atom, dbobject) SWITCH atom.name %]
528   [- FOREACH node = linkable -]
529     [% CASE '[- node.key FILTER ucfirst -]' %]
530       [% render[- node.key FILTER ucfirst -]Atom(atom.render(dbobject)) %]
531   [- END -]
532     [% CASE DEFAULT %]
533       [% renderlist(atom.render(dbobject)) %]
534 [% END %]
535 [- FOREACH node = linkable -]
536 [% MACRO render[- node.key FILTER ucfirst -]Atom(lstArr) BLOCK %]
537   [% FOREACH record = lstArr %]
538     [% fields = record.data %]
539     [- pname = node.key FILTER ucfirst -]
540     [- pkey = "Durian::Model::${pname}" -]
541     [- FOREACH field = nodes.$pkey.columns_essential -]
542       <tr><td><b>[- field -]</b></td><td>[% fields.[- field -] %]</td></tr>
543     [- END -]
544     [% id = record.id %]
545     <tr><td><a href="?id=[% id %];class=Durian::Model::[- node.key FILTER ucfirst -]">Link</a></td><td></td></tr>
546   [% END %]
547 [% END %]
548 [- END -]
549 [% MACRO renderlist(lstArr) BLOCK %]
550   [%  FOREACH item = lstArr %]
551     <tr>[% item %]</tr>
552   [% END %]
553 [% END %]
554 EOF
555
556
557 sub translateForm
558 {
559   my $t = shift;
560   my $meta = shift;
561 #  my $output = shift;
562   my $args = $t->producer_args;
563   my $tt2     = $meta->{'template'};
564   my $tt2Ref;
565
566      if ($tt2 eq 'atom')     { $tt2Ref = \$turnkey_atom_tt2;     }
567   elsif ($tt2 eq 'classdbi') { $tt2Ref = \$turnkey_dbi_tt2;      }
568   elsif ($tt2 eq 'xml')      { $tt2Ref = \$turnkey_xml_tt2;      }
569   elsif ($tt2 eq 'template') { $tt2Ref = \$turnkey_template_tt2; }
570   else                       { die __PACKAGE__." didn't recognize your template option: $tt2" }
571
572   my $config = {
573       EVAL_PERL    => 1,               # evaluate Perl code blocks
574   };
575
576   # create Template object
577   my $template = Template->new($config);
578
579   my $result;
580   # specify input filename, or file handle, text reference, etc.
581   # process input template, substituting variables
582   $template->process($tt2Ref, $meta, \$result) || die $template->error();
583   return($result);
584 }
585
586 1;
587
588 # -------------------------------------------------------------------
589
590 =pod
591
592 =head1 NAME
593
594 SQL::Translator::Producer::Turnkey - create Turnkey classes from schema
595
596 =head1 SYNOPSIS
597
598 Creates output for use with the Turnkey project.
599
600 =head1 SEE ALSO
601
602 L<http://turnkey.sourceforge.net>.
603
604 =head1 AUTHORS
605
606 Allen Day E<lt>allenday@ucla.eduE<gt>
607 Ying Zhang E<lt>zyolive@yahoo.comE<gt>,
608 Brian O'Connor E<lt>brian.oconnor@excite.comE<gt>.