From: Tara L Andrews Date: Thu, 29 Dec 2011 01:25:07 +0000 (+0100) Subject: give Directory proper interface X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=12523041b91eb6461a56355b2184978f0d6aa7f3;p=scpubgit%2Fstemmatology.git give Directory proper interface --- diff --git a/lib/Text/Tradition/Directory.pm b/lib/Text/Tradition/Directory.pm index 9db309e..3487d90 100644 --- a/lib/Text/Tradition/Directory.pm +++ b/lib/Text/Tradition/Directory.pm @@ -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, +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 diff --git a/script/save_to_db.pl b/script/save_to_db.pl old mode 100644 new mode 100755 index 59e5ef4..d7d485f --- a/script/save_to_db.pl +++ b/script/save_to_db.pl @@ -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 index 0000000..193e356 --- /dev/null +++ b/t/data/simple.dot @@ -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 index 0000000..e2c2667 --- /dev/null +++ b/t/text_tradition_directory.t @@ -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;