Added schema filters
Mark Addison [Sun, 12 Dec 2004 18:38:11 +0000 (18:38 +0000)]
Changes
lib/SQL/Translator.pm
t/36-filters.t [new file with mode: 0644]

diff --git a/Changes b/Changes
index 0c45e9e..04e4003 100644 (file)
--- a/Changes
+++ b/Changes
@@ -16,6 +16,7 @@
     - Variables and config can be passed on the command line
       with --tt-var and --tt-conf options to sqlt.
 
+*   Added schema filters.
 
 # -----------------------------------------------------------
 # 0.06    2004-05-13
index 82855be..8677abd 100644 (file)
@@ -1,7 +1,7 @@
 package SQL::Translator;
 
 # ----------------------------------------------------------------------
-# $Id: Translator.pm,v 1.61 2004-11-09 05:27:45 grommit Exp $
+# $Id: Translator.pm,v 1.62 2004-12-12 18:38:11 grommit Exp $
 # ----------------------------------------------------------------------
 # Copyright (C) 2002-4 The SQLFairy Authors
 #
@@ -27,7 +27,7 @@ use base 'Class::Base';
 require 5.004;
 
 $VERSION  = '0.06';
-$REVISION = sprintf "%d.%02d", q$Revision: 1.61 $ =~ /(\d+)\.(\d+)/;
+$REVISION = sprintf "%d.%02d", q$Revision: 1.62 $ =~ /(\d+)\.(\d+)/;
 $DEBUG    = 0 unless defined $DEBUG;
 $ERROR    = "";
 
@@ -90,13 +90,21 @@ sub init {
     }
 
     #
+    # Initialize the filters.
+    #
+    if ( $config->{filters} && ref $config->{filters} eq "ARRAY" ) {
+        $self->filters( @{$config->{filters}} )
+        || return $self->error('Error inititializing filters: '.$self->error);
+    }
+
+    #
     # Set the data source, if 'filename' or 'file' is provided.
     #
     $config->{'filename'} ||= $config->{'file'} || "";
     $self->filename( $config->{'filename'} ) if $config->{'filename'};
 
     #
-    # Finally, if there is a 'data' parameter, use that in 
+    # Finally, if there is a 'data' parameter, use that in
     # preference to filename and file
     #
     if ( my $data = $config->{'data'} ) {
@@ -109,7 +117,7 @@ sub init {
     $self->{'debug'} = defined $config->{'debug'} ? $config->{'debug'} : $DEBUG;
 
     $self->add_drop_table( $config->{'add_drop_table'} );
-    
+
     $self->no_comments( $config->{'no_comments'} );
 
     $self->show_warnings( $config->{'show_warnings'} );
@@ -152,9 +160,9 @@ sub no_comments {
 # ----------------------------------------------------------------------
 sub producer {
     shift->_tool({
-            name => 'producer', 
+            name => 'producer',
             path => "SQL::Translator::Producer",
-            default_sub => "produce" 
+            default_sub => "produce",
     }, @_);
 }
 
@@ -186,9 +194,9 @@ sub producer_args { shift->_args("producer", @_); }
 # ----------------------------------------------------------------------
 sub parser {
     shift->_tool({
-        name => 'parser', 
+        name => 'parser',
         path => "SQL::Translator::Parser",
-        default_sub => "parse" 
+        default_sub => "parse",
     }, @_);
 }
 
@@ -197,6 +205,40 @@ sub parser_type { $_[0]->{'parser_type'}; }
 sub parser_args { shift->_args("parser", @_); }
 
 # ----------------------------------------------------------------------
+# e.g.
+#   $sqlt->filters => [
+#       sub { },
+#       [ "NormalizeNames", field => "lc", tabel => "ucfirst" ],
+#       [
+#           "DataTypeMap",
+#           "TEXT" => "BIGTEXT",
+#       ],
+#   ],
+# ----------------------------------------------------------------------
+sub filters {
+    my $self = shift;
+    my $filters = $self->{filters} ||= [];
+    return @$filters unless @_;
+
+    # Set. Convert args to list of [\&code,\%args]
+    foreach (@_) {
+        $_ = [$_,{}] if not ref($_) eq "ARRAY";
+        my ($name,$args) = @$_;
+        if ( isa($name,"CODE") ) {
+            push @$filters, $_;
+            next;
+        }
+        else {
+            $self->debug("Adding $name filter. Args:".Dumper($args)."\n");
+            my $code = _load_sub("$name\::filter", "SQL::Translator::Filter");
+            return $self->error("ERROR:".$self->error) unless $code;
+            push @$filters, [$code,$args];
+        }
+    }
+    return @$filters;
+}
+
+# ----------------------------------------------------------------------
 sub show_warnings {
     my $self = shift;
     my $arg  = shift;
@@ -348,7 +390,7 @@ sub translate {
     my ($parser_output, $producer_output);
 
     # Parse arguments
-    if (@_ == 1) { 
+    if (@_ == 1) {
         # Passed a reference to a hash?
         if (isa($_[0], 'HASH')) {
             # yep, a hashref
@@ -431,12 +473,14 @@ sub translate {
     $producer_type = $self->producer_type;
 
     # ----------------------------------------------------------------
-    # Execute the parser, then execute the producer with that output.
+    # Execute the parser, the filters and then execute the producer.
     # Allowances are made for each piece to die, or fail to compile,
     # since the referenced subroutines could be almost anything.  In
     # the future, each of these might happen in a Safe environment,
     # depending on how paranoid we want to be.
     # ----------------------------------------------------------------
+
+    # Run parser
     unless ( defined $self->{'schema'} ) {
         eval { $parser_output = $parser->($self, $$data) };
         if ($@ || ! $parser_output) {
@@ -445,14 +489,25 @@ sub translate {
             return $self->error($msg);
         }
     }
-
     $self->debug("Schema =\n", Dumper($self->schema), "\n");
 
+    # Validate the schema if asked to.
     if ($self->validate) {
         my $schema = $self->schema;
         return $self->error('Invalid schema') unless $schema->is_valid;
     }
 
+    # Run filters
+    my $filt_num = 0;
+    foreach ($self->filters) {
+        $filt_num++;
+        my ($code,$args) = @$_;
+        eval { $code->($self->schema, $args) };
+        my $err = $@ || $self->error || 0;
+        return $self->error("Error with filter $filt_num : $err") if $err;
+    }
+
+    # Run producer
     eval { $producer_output = $producer->($self) };
     if ($@ || ! $producer_output) {
         my $err = $@ || $self->error || "no results";
@@ -565,7 +620,7 @@ sub _tool {
         my ($code,$sub);
         ($code,$sub) = _load_sub("$tool\::$default_sub", $path);
         ($code,$sub) = _load_sub("$tool", $path) unless $code;
-        
+
         # get code reference and assign
         my (undef,$module,undef) = $sub =~ m/((.*)::)?(\w+)$/;
         $self->{$name} = $code;
@@ -653,7 +708,7 @@ sub _list {
 #
 # Returns package name of the module actually loaded or false and sets error.
 #
-# Note, you can't load a name from the root namespace (ie one without '::' in 
+# Note, you can't load a name from the root namespace (ie one without '::' in
 # it), therefore a single word name without a path fails.
 # ----------------------------------------------------------------------
 sub load {
@@ -667,7 +722,7 @@ sub load {
         my $file = $module; $file =~ s[::][/]g; $file .= ".pm";
         __PACKAGE__->debug("Loading $name as $file\n");
         return $module if $INC{$file}; # Already loaded
-        
+
         eval { require $file };
         next if $@ =~ /Can't locate $file in \@INC/; 
         eval { $file->import(@_) } unless $@;
@@ -687,13 +742,13 @@ sub load {
 # ----------------------------------------------------------------------
 sub _load_sub {
     my ($tool, @path) = @_;
-    
-    # Passed a module name or module and sub name 
+
+    # Passed a module name or module and sub name
     my (undef,$module,$func_name) = $tool =~ m/((.*)::)?(\w+)$/;
     if ( my $module = load($module => @path) ) {
         my $sub = "$module\::$func_name";
-        return ( \&{ $sub }, $sub );
-    } 
+        return wantarray ? ( \&{ $sub }, $sub ) : \&$sub;
+    }
     return undef;
 }
 
@@ -718,7 +773,7 @@ sub format_pk_name {
 }
 
 # ----------------------------------------------------------------------
-# The other format_*_name methods rely on this one.  It optionally 
+# The other format_*_name methods rely on this one.  It optionally
 # accepts a subroutine ref as the first argument (or uses an identity
 # sub if one isn't provided or it doesn't already exist), and applies
 # it to the rest of the arguments (if any).
@@ -791,15 +846,15 @@ SQL::Translator - manipulate structured data definitions (SQL and more)
       # Print debug info
       debug               => 1,
       # Print Parse::RecDescent trace
-      trace               => 0, 
+      trace               => 0,
       # Don't include comments in output
-      no_comments         => 0, 
+      no_comments         => 0,
       # Print name mutations, conflicts
-      show_warnings       => 0, 
+      show_warnings       => 0,
       # Add "drop table" statements
-      add_drop_table      => 1, 
+      add_drop_table      => 1,
       # Validate schema object
-      validate            => 1, 
+      validate            => 1,
       # Make all table names CAPS in producers which support this option
       format_table_name   => sub {my $tablename = shift; return uc($tablename)},
       # Null-op formatting, only here for documentation's sake
@@ -812,7 +867,7 @@ SQL::Translator - manipulate structured data definitions (SQL and more)
       from       => 'MySQL',
       to         => 'Oracle',
       # Or an arrayref of filenames, i.e. [ $file1, $file2, $file3 ]
-      filename   => $file, 
+      filename   => $file,
   ) or die $translator->error;
 
   print $output;
@@ -861,6 +916,10 @@ producer_args
 
 =item *
 
+filters
+
+=item *
+
 filename / file
 
 =item *
@@ -998,6 +1057,53 @@ entirety of the data to be parsed.
 There is also C<parser_type> and C<parser_args>, which perform
 analogously to C<producer_type> and C<producer_args>
 
+=head2 filters
+
+Set or retreive the filters to run over the schema during the
+translation, before the producer creates its output. Filters are sub
+routines called, in order, with the schema object to filter as the 1st
+arg and a hashref of options as the 2nd. They are free to do whatever
+they want to the schema object, which will be handed to any following
+filters, then used by the producer.
+
+Filters are set as an array, which gives the order they run in.
+Like parsers and producers, they can be defined by a module name, a
+module name relative to the SQL::Translator::Filter namespace, a module
+name and function name together or a reference to an anonymous subroutine.
+When using a module name a function called C<filter> will be invoked in
+that package to do the work. To pass args to the filter set it as an array
+ref with the 1st value giving the filter and the rest being a hash of
+args.
+
+ $tr->filters(
+     sub {
+        my $schema = shift;
+        # Do stuff to schema here!
+     },
+     [ "Foo", foo => "bar", hello => "world" ],
+     [ "Filter3" ],
+ );
+
+Although you would normally set them in the constructor, which calls
+through to filters. i.e.
+
+  my $translator  = SQL::Translator->new(
+      ...
+      filters => [
+          sub { ... },
+          [ Foo, foo => "bar" ],
+      ],
+      ...
+  );
+
+See F<t/36-filters.t> for more examples.
+
+Multiple set calls to filters are cumulative with new filters added to
+the end of the current list.
+
+Returns the filters as a list of array refs, the 1st value being a
+reference to the filter sub routine and the 2nd a hashref its args.
+
 =head2 show_warnings
 
 Toggles whether to print warnings of name conflicts, identifier
@@ -1008,11 +1114,12 @@ current value.
 
 =head2 translate
 
-The C<translate> method calls the subroutines referenced by the
-C<parser> and C<producer> data members (described above).  It accepts
-as arguments a number of things, in key => value format, including
-(potentially) a parser and a producer (they are passed directly to the
-C<parser> and C<producer> methods).
+The C<translate> method calls the subroutine referenced by the
+C<parser> data member, then calls any C<filters> and finally calls
+the C<producer> sub routine (these members are described above).
+It accepts as arguments a number of things, in key => value format,
+including (potentially) a parser and a producer (they are passed
+directly to the C<parser> and C<producer> methods).
 
 Here is how the parameter list to C<translate> is parsed:
 
diff --git a/t/36-filters.t b/t/36-filters.t
new file mode 100644 (file)
index 0000000..5032bf0
--- /dev/null
@@ -0,0 +1,158 @@
+#!/usr/bin/perl -w
+# vim:filetype=perl
+
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl test.pl'
+
+# SQL::Translator::Filter::HelloWorld - Test filter in a package
+#=============================================================================
+package SQL::Translator::Filter::HelloWorld;
+
+use strict;
+use vars qw/$VERSION/;
+$VERSION=0.1;
+
+sub filter {
+    my ($schema,$args) = (shift,shift);
+
+    my $greeting = $args->{greeting} || "Hello";
+    $schema->add_table(
+        name => "HelloWorld",
+    );
+}
+
+# Hack to allow sqlt to see our module as it wasn't loaded from a .pm
+$INC{'SQL/Translator/Filter/HelloWorld.pm'}
+    = 'lib/SQL/Translator/Filter/HelloWorld.pm';
+
+#=============================================================================
+
+package main;
+
+use strict;
+use Test::More;
+use Test::Exception;
+use Test::SQL::Translator qw(maybe_plan);
+
+use Data::Dumper;
+
+BEGIN {
+    maybe_plan(14, 'Template', 'Test::Differences')
+}
+use Test::Differences;
+use SQL::Translator;
+
+my $in_yaml = qq{--- #YAML:1.0
+schema:
+  tables:
+    person:
+      name: person
+      fields:
+        first_name:
+          data_type: foovar
+          name: First_Name
+};
+
+my $ans_yaml = qq{--- #YAML:1.0
+schema:
+  procedures: {}
+  tables:
+    HelloWorld:
+      comments: ''
+      constraints: []
+      fields: {}
+      indices: []
+      name: HelloWorld
+      options: []
+      order: 2
+    PERSON:
+      comments: ''
+      constraints: []
+      fields:
+        first_name:
+          data_type: foovar
+          default_value: ~
+          extra: {}
+          is_nullable: 1
+          is_primary_key: 0
+          is_unique: 0
+          name: first_name
+          order: 1
+          size:
+            - 0
+      indices: []
+      name: PERSON
+      options: []
+      order: 1
+  triggers: {}
+  views: {}
+translator:
+  add_drop_table: 0
+  filename: ~
+  no_comments: 0
+  parser_args: {}
+  parser_type: SQL::Translator::Parser::YAML
+  producer_args: {}
+  producer_type: SQL::Translator::Producer::YAML
+  show_warnings: 1
+  trace: 0
+  version: 0.06
+};
+
+# Parse the test XML schema
+my $obj;
+$obj = SQL::Translator->new(
+    debug          => 0,
+    show_warnings  => 1,
+    parser         => "YAML",
+    data           => $in_yaml,
+    to             => "YAML",
+    filters => [
+        # Check they get called ok
+        sub {
+            pass("Filter 1 called");
+            isa_ok($_[0],"SQL::Translator::Schema", "Filter 1, arg0 ");
+            ok( ref($_[1]) eq "HASH", "Filter 1, arg1 is a hashref ");
+        },
+        sub {
+            pass("Filter 2 called");
+            isa_ok($_[0],"SQL::Translator::Schema", "Filter 2, arg0 ");
+            ok( ref($_[1]) eq "HASH", "Filter 2, arg1 is a hashref ");
+        },
+
+        # Sub filter with args
+        [ sub {
+            pass("Filter 3 called");
+            isa_ok($_[0],"SQL::Translator::Schema", "Filter 3, arg0 ");
+            ok( ref($_[1]) eq "HASH", "Filter 3, arg1 is a hashref ");
+            is( $_[1]->{hello}, "world", "Filter 3, got args ");
+        },
+        { hello=>"world" } ],
+
+        # Uppercase all the table names.
+        sub {
+            my $schema = shift;
+            foreach ($schema->get_tables) {
+                $_->name(uc $_->name);
+            }
+        },
+
+        # lowercase all the field names.
+        sub {
+            my $schema = shift;
+            foreach ( map { $_->get_fields } $schema->get_tables ) {
+                $_->name(lc $_->name);
+            }
+        },
+
+        # Filter from SQL::Translator::Filter::*
+        [ 'HelloWorld' ],
+    ],
+
+) or die "Failed to create translator object: ".SQL::Translator->error;
+
+my $out;
+lives_ok { $out = $obj->translate; }  "Translate ran";
+is $obj->error, ''                   ,"No errors";
+ok $out ne ""                        ,"Produced something!";
+eq_or_diff $out, $ans_yaml           ,"Output looks right";