give Directory proper interface
Tara L Andrews [Thu, 29 Dec 2011 01:25:07 +0000 (02:25 +0100)]
lib/Text/Tradition/Directory.pm
script/save_to_db.pl [changed mode: 0644->0755]
t/data/simple.dot [new file with mode: 0644]
t/text_tradition_directory.t [new file with mode: 0644]

index 9db309e..3487d90 100644 (file)
@@ -8,6 +8,125 @@ use KiokuDB::TypeMap::Entry::Naive;
 
 extends 'KiokuX::Model';
 
+=head1 NAME
+
+Text::Tradition::Directory - a KiokuDB interface for storing and retrieving traditions
+
+=head1 SYNOPSIS
+
+  use Text::Tradition::Directory;
+  my $d = Text::Tradition::Directory->new( 
+    'dsn' => 'dbi:SQLite:mytraditions.db',
+    'extra_args' => { 'create' => 1 },
+  );
+  
+  my $tradition = Text::Tradition->new( @args );
+  $d->save_tradition( $tradition );
+  my $stemma = Text::Tradition::Stemma->new( 
+       'dot' => $dotfile, 'collation' => $tradition->collation );
+  $d->save_stemma( $stemma );
+  
+  foreach my $id ( $d->traditions ) {
+       print $d->tradition( $id )->name;
+       print $d->stemma( $id )->as_svg;
+  }
+    
+=head1 DESCRIPTION
+
+Text::Tradition::Directory is an interface for storing and retrieving text traditions and all their data, including an associated stemma hypothesis.  It is an instantiation of a KiokuDB::Model, storing traditions and associated stemmas by UUID.
+
+=head1 METHODS
+
+=head2 new
+
+Returns a Directory object.  Apart from those documented in L<KiokuX::Model>,
+options include:
+
+=over
+
+=item * preload - Load all traditions and stemmata into memory upon instantiation.  Defaults to true.  (TODO manage on-demand loading)
+
+=back
+
+=head2 tradition_ids
+
+Returns the ID of all traditions in the database.
+
+=head2 tradition( $id )
+
+Returns the Text::Tradition object of the given ID.
+
+=head2 stemma( $id )
+
+Returns the Text::Tradition::Stemma object associated with the given tradition ID.
+
+=head2 save_tradition( $tradition )
+
+Writes the given tradition to the database, returning its UUID.
+
+=head2 save_stemma( $stemma )
+
+Writes the given stemma to the database, returning its UUID.
+
+=begin testing
+
+use File::Temp;
+use Text::Tradition;
+use Text::Tradition::Stemma;
+use_ok 'Text::Tradition::Directory';
+
+my $fh = File::Temp->new();
+my $file = $fh->filename;
+$fh->close;
+my $dsn = "dbi:SQLite:dbname=$file";
+
+my $d = Text::Tradition::Directory->new( 'dsn' => $dsn,
+    'extra_args' => { 'create' => 1 } );
+is( ref $d, 'Text::Tradition::Directory', "Got directory object" );
+
+my $t = Text::Tradition->new( 
+    'name'  => 'inline', 
+    'input' => 'Tabular',
+    'file'  => 't/data/simple.txt',
+    );
+my $uuid = $d->save_tradition( $t );
+ok( $uuid, "Saved test tradition" );
+
+my $s = Text::Tradition::Stemma->new( 
+       'collation' => $t->collation,
+       'dotfile' => 't/data/simple.dot' );
+my $sid = $d->save_stemma( $s );
+ok( $sid, "Saved test stemma" );
+
+is( $d->tradition( $uuid ), $t, "Correct tradition returned for id" );
+is( $d->stemma( $uuid ), $s, "Correct stemma returned for id" );
+is( scalar $d->tradition_ids, 1, "Only one tradition in DB" );
+
+# Connect to a new instance
+my $e = Text::Tradition::Directory->new( 'dsn' => $dsn );
+is( scalar $e->tradition_ids, 1, "One tradition preloaded from DB" );
+my $te = $e->tradition( $uuid );
+is( $te->name, $t->name, "New instance returns correct tradition" );
+my $se = $e->stemma( $uuid );
+is( $se->graph, $s->graph, "New instance returns correct stemma" );
+is( $e->tradition( 'NOT-A-UUID' ), undef, "Undef returned for non-tradition" );
+is( $e->stemma( 'NOT-A-UUID' ), undef, "Undef returned for non-stemma" );
+$te->name( "Changed name" );
+my $new_id = $e->save_tradition( $te );
+is( $new_id, $uuid, "Updated tradition ID did not change" );
+
+my $f = Text::Tradition::Directory->new( 'dsn' => $dsn, 'preload' => 0 );
+is( scalar $f->tradition_ids, 0, "No traditions preloaded from DB" );
+### TODO This doesn't work, as I cannot get an object scope in the
+### 'tradition' wrapper.
+# my $tf = $f->tradition( $uuid );
+# is( $tf->name, $t->name, "Next instance returns correct tradition" );
+# is( $tf->name, "Changed name", "Change to tradition carried through" );
+
+=end testing
+
+=cut
+
 has data_hash => (
     traits => ['Hash'],
        default => sub { {} },
@@ -16,17 +135,16 @@ has data_hash => (
         stemma           => 'get',
         add_tradition => 'set',
         add_stemma       => 'set',
-        traditions    => 'keys',
+        tradition_ids => 'keys',
     },
 );
        
-has typemap => (
+has +typemap => (
        is => 'rw',
        isa => 'KiokuDB::TypeMap',
        default => sub { 
                KiokuDB::TypeMap->new(
                        isa_entries => {
-                               "Graph::Easy::Base" => KiokuDB::TypeMap::Entry::Naive->new,
                                "Graph" => KiokuDB::TypeMap::Entry::Naive->new,
                                "Graph::AdjacencyMap" => KiokuDB::TypeMap::Entry::Naive->new,
                        }
@@ -34,15 +152,58 @@ has typemap => (
        },
 );
 
+has preload => (
+       is => 'ro',
+       isa => 'Bool',
+       default => 1,
+       );
+
 around 'tradition' => sub {
        my( $orig, $self, @arg ) = @_;
        my $data = $self->$orig( @arg );
+       unless( $data ) {
+               # Connect to the DB and fetch the thing.
+               $self->new_scope;
+               my $id = shift @arg;
+               my $trad = $self->lookup( $id );
+               if( ref( $trad ) eq 'Text::Tradition' ) {
+                       $self->add_tradition( $id => $trad );
+                       return $trad;
+               } 
+               # If we got this far...
+               return undef;
+       }
        return $data->{'object'};
 };
 
 around 'stemma' => sub {
        my( $orig, $self, @arg ) = @_;
        my $data = $self->$orig( @arg );
+       unless( $data ) {
+               # Connect to the DB and fetch the thing.
+               $self->new_scope;
+               my $id = shift @arg;
+               my $trad = $self->lookup( $id );
+               if( ref( $trad ) eq 'Text::Tradition' ) {
+                       # Add it
+                       $self->add_tradition( $id => $trad );
+                       # Find the stemma whose collation belongs to $trad
+                       my $ret = $self->grep( sub { $_->collation eq $trad->collation } );
+                       my $stemma;
+                       until ( $ret->is_done ) {
+                               foreach my $st ( $ret->items ) {
+                                       warn "Found two saved stemmas for tradition $id" if $stemma;
+                                       $stemma = $st;
+                               }
+                       }
+                       if( $stemma ) {
+                               $self->add_stemma( $stemma );
+                               return $stemma;
+                       }
+               } 
+               # If we got this far...
+               return undef;
+       }
        return $data->{'stemma'};
 };
 
@@ -62,34 +223,72 @@ sub BUILD {
        my $self = shift;
        my $args = shift;
        
-       if( exists $args->{'dsn'} ) {
-               # Connect to self, get the traditions and stemmas, and save them
-               # in the directory.
-               my $scope = $self->new_scope;
-               my $stream = $self->root_set;
-               my %stemmata;
-               until( $stream->is_done ) {
-                       foreach my $obj ( $stream->items ) {
-                               my $uuid = $self->object_to_id( $obj );
-                               if( ref( $obj ) eq 'Text::Tradition' ) {
-                                       $self->add_tradition( $uuid => $obj );
-                               } elsif( ref( $obj ) eq 'Text::Tradition::Stemma' ) {
-                                       $stemmata{$obj->collation} = $obj;
-                               } else {
-                                       warn "Found root object in DB that is neither tradition nor stemma: $obj";
-                               }
+       $self->fetch_all if( $self->dsn && $self->preload );
+}
+
+# Connect to self, get the traditions and stemmas, and save them
+# in the directory.
+sub fetch_all {
+       my $self = shift;
+       my $scope = $self->new_scope;
+       my $stream = $self->root_set;
+       my %stemmata;
+       until( $stream->is_done ) {
+               foreach my $obj ( $stream->items ) {
+                       my $uuid = $self->object_to_id( $obj );
+                       if( ref( $obj ) eq 'Text::Tradition' ) {
+                               $self->add_tradition( $uuid => $obj );
+                       } elsif( ref( $obj ) eq 'Text::Tradition::Stemma' ) {
+                               $stemmata{$obj->collation} = $obj;
+                       } else {
+                               warn "Found root object in DB that is neither tradition nor stemma: $obj";
                        }
                }
-               # Now match the stemmata to their traditions.
-               foreach my $id ( $self->traditions ) {
-                       my $c = $self->tradition( $id )->collation;
-                       if( exists $stemmata{$c} ) {
-                               $self->add_stemma( $id => $stemmata{$c} );
-                       }
+       }
+       # Now match the stemmata to their traditions.
+       foreach my $id ( $self->tradition_ids ) {
+               my $c = $self->tradition( $id )->collation;
+               if( exists $stemmata{$c} ) {
+                       $self->add_stemma( $id => $stemmata{$c} );
                }
        }
 }
+       
+
+sub save_tradition {
+       my( $self, $tradition ) = @_;
+       # Write the thing to the db and return its ID.
+       unless( ref( $tradition ) eq 'Text::Tradition' ) {
+               warn "Object $tradition is not a Text::Tradition";
+               return undef;
+       }
+       my $scope = $self->new_scope;
+       my $uuid = $self->store( $tradition );
+       $self->add_tradition( $uuid => $tradition );
+       return $uuid;
+}
+
+sub save_stemma {
+       my( $self, $stemma ) = @_;
+       unless( ref( $stemma ) eq 'Text::Tradition::Stemma' ) {
+               warn "Object $stemma is not a Text::Tradition::Stemma";
+               return undef;
+       }
+       my $scope = $self->new_scope;
+       # Get the tradition to which this stemma belongs.
+       my $tradition = $stemma->collation->tradition;
+       # Make sure the tradition is in the DB.
+       my $tid = $self->save_tradition( $tradition );
+       unless( $tid ) {
+               warn "Could not access this stemma's tradition; aborting";
+               return undef;
+       }
+       my $sid = $self->store( $stemma );
+       $self->add_stemma( $tid => $stemma );
+       return $tid;
+}
+       
 
 1;
-               
+       
                
\ No newline at end of file
old mode 100644 (file)
new mode 100755 (executable)
index 59e5ef4..d7d485f
@@ -4,21 +4,18 @@ use lib 'lib';
 use strict;
 use warnings;
 use File::Basename;
-use KiokuDB;
-use KiokuDB::TypeMap::Entry::Naive;
 use Text::Tradition;
+use Text::Tradition::Directory;
 use Text::Tradition::Stemma;
 
+binmode( STDOUT, ':utf8' );
+binmode( STDERR, ':utf8' );
+
 # Make a KiokuDB store from the traditions data we have.
 
-my $kdb = KiokuDB->connect( "dbi:SQLite:dbname=db/traditions.db", 
-       create => 1,
-       typemap => KiokuDB::TypeMap->new(
-            isa_entries => {
-                "Graph" => KiokuDB::TypeMap::Entry::Naive->new,
-                "Graph::AdjacencyMap" => KiokuDB::TypeMap::Entry::Naive->new,
-            },
-        ),
+my $kdb = Text::Tradition::Directory->new(
+       'dsn' => "dbi:SQLite:dbname=db/traditions.db",
+       'extra_args' => { 'create' => 1 },
     );
     
 my %stemma_map = (
@@ -51,9 +48,8 @@ if( $dir ) {
                                );
                }
                        
-               my $scope = $kdb->new_scope;
-               my $tid = $kdb->store( $tradition );
-               my $sid = $kdb->store( $stemma ) if $stemma;
+               my $tid = $kdb->save_tradition( $tradition );
+               my $sid = $kdb->save_stemma( $stemma ) if $stemma;
                print STDERR "Stored tradition for " . $tradition->name . " at $tid\n";
                print STDERR "\tand stemma at $sid\n" if $stemma;
        }
@@ -61,27 +57,19 @@ if( $dir ) {
 
 # Now try reading the objects from the DB.
 
-my $scope = $kdb->new_scope;
-
-my $stream = $kdb->root_set;
-until( $stream->is_done ) {
-       foreach my $t ( $stream->items ) {
-               print STDERR "*** Object " . $kdb->object_to_id( $t ) . " ***\n";
-               if( ref( $t ) eq 'Text::Tradition' ) {
-                       print STDERR "Got tradition " . $t->name . " out of the database\n";
-                       my @wits = map { $_->sigil } $t->witnesses;
-                       print STDERR "...with witnesses @wits\n";
-                       my $c = $t->collation;
-                       print STDERR "Collation has " . scalar( $c->readings ) . " readings\n";
-                       print STDERR "Collation has " . scalar( $c->paths ) . " paths\n";
-                       print STDERR "Collation has " . scalar( $c->relationships ) . " relationship links\n";
-               } elsif( ref( $t ) eq 'Text::Tradition::Stemma' ) {
-                       print STDERR "Got stemma for tradition " . $t->collation->tradition->name 
-                               . " out of the database\n";
-                       print STDERR "Stemma graph is " . $t->graph . "\n";
-               } else {
-                       print STDERR "Got unexpected object of type " . ref( $t ) 
-                               . " out of the database\n";
-               }
+foreach my $tid ( $kdb->tradition_ids ) {
+       my $t = $kdb->tradition( $tid );
+       print STDERR "Got tradition " . $t->name . " out of the database\n";
+       my @wits = map { $_->sigil } $t->witnesses;
+       print STDERR "...with witnesses @wits\n";
+       my $c = $t->collation;
+       print STDERR "Collation has " . scalar( $c->readings ) . " readings\n";
+       print STDERR "Collation has " . scalar( $c->paths ) . " paths\n";
+       print STDERR "Collation has " . scalar( $c->relationships ) . " relationship links\n";
+       my $s = $kdb->stemma( $tid );
+       if( $s ) {
+               print STDERR "Got stemma for tradition " . $s->collation->tradition->name 
+                       . " out of the database\n";
+               print STDERR "Stemma graph is " . $s->graph . "\n";
        }
 }
\ No newline at end of file
diff --git a/t/data/simple.dot b/t/data/simple.dot
new file mode 100644 (file)
index 0000000..193e356
--- /dev/null
@@ -0,0 +1,11 @@
+digraph Stemma {
+       1 [ class="hypothetical" ];
+       2 [ class="hypothetical" ];
+       A [ class="extant" ];
+       B [ class="extant" ];
+       C [ class="extant" ];
+       1 -> A;
+       1 -> 2;
+       2 -> B;
+       2 -> C;
+}
\ No newline at end of file
diff --git a/t/text_tradition_directory.t b/t/text_tradition_directory.t
new file mode 100644 (file)
index 0000000..e2c2667
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Test::More 'no_plan';
+$| = 1;
+
+
+
+# =begin testing
+{
+use File::Temp;
+use Text::Tradition;
+use Text::Tradition::Stemma;
+use_ok 'Text::Tradition::Directory';
+
+my $fh = File::Temp->new();
+my $file = $fh->filename;
+$fh->close;
+my $dsn = "dbi:SQLite:dbname=$file";
+
+my $d = Text::Tradition::Directory->new( 'dsn' => $dsn,
+    'extra_args' => { 'create' => 1 } );
+is( ref $d, 'Text::Tradition::Directory', "Got directory object" );
+
+my $t = Text::Tradition->new( 
+    'name'  => 'inline', 
+    'input' => 'Tabular',
+    'file'  => 't/data/simple.txt',
+    );
+my $uuid = $d->save_tradition( $t );
+ok( $uuid, "Saved test tradition" );
+
+my $s = Text::Tradition::Stemma->new( 
+       'collation' => $t->collation,
+       'dotfile' => 't/data/simple.dot' );
+my $sid = $d->save_stemma( $s );
+ok( $sid, "Saved test stemma" );
+
+is( $d->tradition( $uuid ), $t, "Correct tradition returned for id" );
+is( $d->stemma( $uuid ), $s, "Correct stemma returned for id" );
+is( scalar $d->tradition_ids, 1, "Only one tradition in DB" );
+
+# Connect to a new instance
+my $e = Text::Tradition::Directory->new( 'dsn' => $dsn );
+is( scalar $e->tradition_ids, 1, "One tradition preloaded from DB" );
+my $te = $e->tradition( $uuid );
+is( $te->name, $t->name, "New instance returns correct tradition" );
+my $se = $e->stemma( $uuid );
+is( $se->graph, $s->graph, "New instance returns correct stemma" );
+is( $e->tradition( 'NOT-A-UUID' ), undef, "Undef returned for non-tradition" );
+is( $e->stemma( 'NOT-A-UUID' ), undef, "Undef returned for non-stemma" );
+$te->name( "Changed name" );
+my $new_id = $e->save_tradition( $te );
+is( $new_id, $uuid, "Updated tradition ID did not change" );
+
+my $f = Text::Tradition::Directory->new( 'dsn' => $dsn, 'preload' => 0 );
+is( scalar $f->tradition_ids, 0, "No traditions preloaded from DB" );
+### TODO This doesn't work, as I cannot get an object scope in the
+### 'tradition' wrapper.
+# my $tf = $f->tradition( $uuid );
+# is( $tf->name, $t->name, "Next instance returns correct tradition" );
+# is( $tf->name, "Changed name", "Change to tradition carried through" );
+}
+
+
+
+
+1;