Merge branch 'master' of github.com:tla/stemmatology
[scpubgit/stemmatology.git] / lib / Text / Tradition / Directory.pm
1 package Text::Tradition::Directory;
2
3 use strict;
4 use warnings;
5 use Moose;
6 use KiokuDB::GC::Naive;
7 use KiokuDB::TypeMap;
8 use KiokuDB::TypeMap::Entry::Naive;
9 use Text::Tradition::Error;
10
11 extends 'KiokuX::Model';
12
13 =head1 NAME
14
15 Text::Tradition::Directory - a KiokuDB interface for storing and retrieving traditions
16
17 =head1 SYNOPSIS
18
19   use Text::Tradition::Directory;
20   my $d = Text::Tradition::Directory->new( 
21     'dsn' => 'dbi:SQLite:mytraditions.db',
22     'extra_args' => { 'create' => 1 },
23   );
24   
25   my $tradition = Text::Tradition->new( @args );
26   my $stemma = $tradition->add_stemma( dotfile => $dotfile ); 
27   $d->save_tradition( $tradition );
28   
29   foreach my $id ( $d->traditions ) {
30         print $d->tradition( $id )->name;
31   }
32     
33 =head1 DESCRIPTION
34
35 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.
36
37 =head1 METHODS
38
39 =head2 new
40
41 Returns a Directory object. 
42
43 =head2 tradition_ids
44
45 Returns the ID of all traditions in the database.
46
47 =head2 tradition( $id )
48
49 Returns the Text::Tradition object of the given ID.
50
51 =head2 save( $tradition )
52
53 Writes the given tradition to the database, returning its ID.
54
55 =begin testing
56
57 use TryCatch;
58 use File::Temp;
59 use Text::Tradition;
60 use_ok 'Text::Tradition::Directory';
61
62 my $fh = File::Temp->new();
63 my $file = $fh->filename;
64 $fh->close;
65 my $dsn = "dbi:SQLite:dbname=$file";
66 my $uuid;
67 my $t = Text::Tradition->new( 
68         'name'  => 'inline', 
69         'input' => 'Tabular',
70         'file'  => 't/data/simple.txt',
71         );
72
73 {
74         my $d = Text::Tradition::Directory->new( 'dsn' => $dsn,
75                 'extra_args' => { 'create' => 1 } );
76         is( ref $d, 'Text::Tradition::Directory', "Got directory object" );
77         
78         my $scope = $d->new_scope;
79         $uuid = $d->save( $t );
80         ok( $uuid, "Saved test tradition" );
81         
82         my $s = $t->add_stemma( dotfile => 't/data/simple.dot' );
83         ok( $d->save( $t ), "Updated tradition with stemma" );
84         is( $d->tradition( $uuid ), $t, "Correct tradition returned for id" );
85         is( $d->tradition( $uuid )->stemma(0), $s, "...and it has the correct stemma" );
86         try {
87                 $d->save( $s );
88         } catch( Text::Tradition::Error $e ) {
89                 is( $e->ident, 'database error', "Got exception trying to save stemma directly" );
90                 like( $e->message, qr/Cannot directly save non-Tradition object/, 
91                         "Exception has correct message" );
92         }
93 }
94 my $nt = Text::Tradition->new(
95         'name' => 'CX',
96         'input' => 'CollateX',
97         'file' => 't/data/Collatex-16.xml',
98         );
99 is( ref( $nt ), 'Text::Tradition', "Made new tradition" );
100
101 {
102         my $f = Text::Tradition::Directory->new( 'dsn' => $dsn );
103         my $scope = $f->new_scope;
104         is( scalar $f->tradition_ids, 1, "Directory index has our tradition" );
105         my $nuuid = $f->save( $nt );
106         ok( $nuuid, "Stored second tradition" );
107         is( scalar $f->tradition_ids, 2, "Directory index has both traditions" );
108         my $tf = $f->tradition( $uuid );
109         is( $tf->name, $t->name, "Retrieved the tradition from a new directory" );
110         my $sid = $f->object_to_id( $tf->stemma(0) );
111         try {
112                 $f->tradition( $sid );
113         } catch( Text::Tradition::Error $e ) {
114                 is( $e->ident, 'database error', "Got exception trying to fetch stemma directly" );
115                 like( $e->message, qr/not a Text::Tradition/, "Exception has correct message" );
116         }
117         try {
118                 $f->delete( $sid );
119         } catch( Text::Tradition::Error $e ) {
120                 is( $e->ident, 'database error', "Got exception trying to delete stemma directly" );
121                 like( $e->message, qr/Cannot directly delete non-Tradition object/, 
122                         "Exception has correct message" );
123         }
124         $f->delete( $uuid );
125         ok( !$f->exists( $uuid ), "Object is deleted from DB" );
126         ok( !$f->exists( $sid ), "Object stemma also deleted from DB" );
127         is( scalar $f->tradition_ids, 1, "Object is deleted from index" );
128 }
129
130 SKIP: {
131         skip 'Have yet to figure out garbage collection', 1;
132         my $g = Text::Tradition::Directory->new( 'dsn' => $dsn );
133         my $scope = $g->new_scope;
134         is( scalar $g->tradition_ids, 1, "Now one object in new directory index" );
135 }
136
137 =end testing
138
139 =cut
140
141 has +typemap => (
142         is => 'rw',
143         isa => 'KiokuDB::TypeMap',
144         default => sub { 
145                 KiokuDB::TypeMap->new(
146                         isa_entries => {
147                                 "Graph" => KiokuDB::TypeMap::Entry::Naive->new,
148                                 "Graph::AdjacencyMap" => KiokuDB::TypeMap::Entry::Naive->new,
149                         }
150                 );
151         },
152 );
153
154 before [ qw/ store update insert delete / ] => sub {
155         my $self = shift;
156         my @nontrad;
157         foreach my $obj ( @_ ) {
158                 if( ref( $obj ) && ref( $obj ) ne 'Text::Tradition' ) {
159                         # Is it an id => Tradition hash?
160                         if( ref( $obj ) eq 'HASH' && keys( %$obj ) == 1 ) {
161                                 my( $k ) = keys %$obj;
162                                 next if ref( $obj->{$k} ) eq 'Text::Tradition';
163                         }
164                         push( @nontrad, $obj );
165                 }
166         }
167         if( @nontrad ) {
168                 throw( "Cannot directly save non-Tradition object of type "
169                         . ref( $nontrad[0] ) );
170         }
171 };
172
173 # If a tradition is deleted, remove it from the index.
174 after delete => sub {
175         my $self = shift;
176         my $gc = KiokuDB::GC::Naive->new( backend => $self->directory->backend );
177         $self->directory->backend->delete( $gc->garbage->members );
178 };
179
180 sub save {
181         my $self = shift;
182         return $self->store( @_ );
183 }
184
185 sub tradition {
186         my( $self, $id ) = @_;
187         my $obj = $self->lookup( $id );
188         unless( ref( $obj ) eq 'Text::Tradition' ) {
189                 throw( "Retrieved object is a " . ref( $obj ) . ", not a Text::Tradition" );
190         }
191         return $obj;
192 }
193
194 sub tradition_ids {
195         my $self = shift;
196         my @ids;
197         $self->scan( sub { push( @ids, $self->object_to_id( @_ ) ) } );
198         return @ids;
199 }
200
201 sub throw {
202         Text::Tradition::Error->throw( 
203                 'ident' => 'database error',
204                 'message' => $_[0],
205                 );
206 }
207
208 1;
209         
210 =head1 LICENSE
211
212 This package is free software and is provided "as is" without express
213 or implied warranty.  You can redistribute it and/or modify it under
214 the same terms as Perl itself.
215
216 =head1 AUTHOR
217
218 Tara L Andrews E<lt>aurum@cpan.orgE<gt>