beginnings of IndexableBy
Matt S Trout [Sun, 10 Jan 2010 20:29:08 +0000 (20:29 +0000)]
lib/DBIx/Data/Collection/Set.pm
lib/DBIx/Data/Collection/Set/IndexableBy.pm [new file with mode: 0644]
t/01basic_collection.t
t/02indexed_by.t [new file with mode: 0644]

index 1acf096..41b4ab9 100644 (file)
@@ -6,7 +6,7 @@ use Data::Perl::Stream::Array;
 
 has _store => (is => 'ro', required => 1, init_arg => 'store');
 
-has _class => (is => 'ro', predicate => '_has_class');
+has _class => (is => 'ro', predicate => '_has_class', init_arg => 'class');
 
 has _set_over => (is => 'ro', required => 1, init_arg => 'set_over');
 
@@ -23,13 +23,15 @@ method _build__member_cache {
   while (my ($raw) = $stream->next) {
     my $obj = do {
       if (my ($obj) = $self->_key_cache_get_raw($raw)) {
-        $obj # can't $self->_merge($obj, $raw) since $obj might have changed
+        # can't just $self->_merge($obj, $raw) since $obj might have changed
+        $self->_refresh($obj, $raw)
       } else {
         $self->_add_to_key_cache($self->_inflate($raw))
       }
     };
     push @cache, $obj;
   }
+  $self->_notify_observers(all_members => \@cache);
   \@cache
 }
 
@@ -86,6 +88,18 @@ method _key_cache_get_id ($id) {
     : ()
 }
 
+## observers
+
+has _observer_callbacks => (
+  is => 'ro', isa => 'ArrayRef', default => sub { [] }
+);
+
+method _notify_observers ($event, $payload) {
+  foreach my $cb (@{$self->_observer_callbacks}) {
+    $self->$cb($event, $payload);
+  }
+}
+
 ## thunking between the store representation and the set representation
 #
 # _inflate is raw data -> final repr
@@ -109,6 +123,14 @@ method _merge ($obj, $raw) {
   $obj
 }
 
+method _refresh ($obj, $raw) {
+  # if $obj has been changed but not flushed we'd destroy data doing
+  # a blind merge - but if $obj has change tracking of some sort then
+  # we -could- do something safely, so this method exists to be mangled
+  # by subclasses
+  $obj
+}
+
 method _deflate_spec ($spec) {
   $spec
 }
@@ -166,6 +188,7 @@ method _get_from_store ($raw) {
 method add ($new) {
   $self->_add_to_store($new);
   $self->_add_to_caches($new);
+  $self->_notify_observers(add => $new);
   $new
 }
 
@@ -186,6 +209,7 @@ method _add_to_caches ($new) {
 method remove ($old) {
   $self->_remove_from_store($old);
   $self->_remove_from_caches($old);
+  $self->_notify_observers(remove => $old);
   $old
 }
 
diff --git a/lib/DBIx/Data/Collection/Set/IndexableBy.pm b/lib/DBIx/Data/Collection/Set/IndexableBy.pm
new file mode 100644 (file)
index 0000000..63e4901
--- /dev/null
@@ -0,0 +1,24 @@
+package DBIx::Data::Collection::Set::IndexableBy;
+
+use Moose;
+use Method::Signatures::Simple;
+
+extends 'DBIx::Data::Collection::Set';
+
+has _indexed_by => (is => 'ro', default => sub { {} });
+
+has _indexable_by => (is => 'ro', required => 1, init_arg => 'indexable_by');
+
+method indexed_by ($by) {
+  if (my $cached = $self->_indexed_by->{$by}) {
+    return $cached
+  }
+  my $index_spec = $self->_indexable_by->{$by};
+  die "${self} not indexable by ${by}" unless $index_spec;
+  my $new = DBIx::Data::Collection::Set->new($index_spec);
+  $self->_indexed_by->{$by} = $new
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
index 9fded99..e9fb8e1 100644 (file)
@@ -12,13 +12,11 @@ use warnings FATAL => 'all';
 
 my $dsn = 'dbi:SQLite:tmp.db';
 
-my @expect;
-
 sub sort_set {
   sort { $a->{name} cmp $b->{name} } @_
 }
 
-{
+sub setup_db {
   unlink('tmp.db');
   my $dbh = DBI->connect($dsn);
   $dbh->do(q{
@@ -30,111 +28,127 @@ sub sort_set {
   my $pop = $dbh->prepare(q{INSERT INTO person (name) VALUES (?)});
   my @names = qw(Joe Jim Bob Pterry);
   $pop->execute($_) for @names;
-  @expect = sort_set do {
+  return sort_set do {
     my $id = 0; map +{ id => ++$id, name => $_ }, @names
   };
 }
 
+my $db_store = DBIx::Data::Store->connect($dsn);
+
+sub make_store {
+  my ($crud) = @_;
+  DBIx::Data::Store::CRUD->new(
+    raw_store => $db_store,
+    select_sql => q{SELECT id, name FROM person},
+    select_column_order => [ qw(id name) ],
+    %$crud
+  );
+}
+
 sub make_set {
-  my ($set, $crud) = @_;
-  DBIx::Data::Collection::Set->new(
+  my ($set, $crud, $class) = @_;
+  ($class || 'DBIx::Data::Collection::Set')->new(
     set_over => [ 'id' ],
-    store => DBIx::Data::Store::CRUD->new(
-      raw_store => DBIx::Data::Store->connect($dsn),
-      select_sql => q{SELECT id, name FROM person},
-      select_column_order => [ qw(id name) ],
-      %$crud,
-    ),
+    store => make_store($crud),
     %$set
   );
 }
 
-my $set = make_set;
+sub run_tests {
 
-is_deeply([ sort_set $set->flatten ], \@expect, 'Basic data out ok (flatten)');
+  my @expect = setup_db;
 
-{
-  my $stream = $set->as_stream;
+  my $set = make_set;
 
-  my @got; while (my ($next) = $stream->next) { push @got, $next }
+  is_deeply([ sort_set $set->flatten ], \@expect, 'Basic data out ok (flatten)');
 
-  is_deeply([ sort_set @got ], \@expect, 'Basic data out ok (stream)');
-}
+  {
+    my $stream = $set->as_stream;
 
-$set = make_set { class => 'Spoon' };
+    my @got; while (my ($next) = $stream->next) { push @got, $next }
 
-is_deeply(
-  [ sort_set $set->flatten ],
-  [ map { bless({ %$_ }, 'Spoon') } @expect ],
-  'Basic data with class out ok'
-);
+    is_deeply([ sort_set @got ], \@expect, 'Basic data out ok (stream)');
+  }
 
-$set = make_set {}, {
-  insert_sql => q{INSERT INTO person (name) VALUES (?) },
-  insert_argument_order => [ 'name' ],
-  insert_command_constructor => sub {
-    require DBIx::Data::Store::Command::Insert::LastInsertId;
-    my $self = shift;
-    DBIx::Data::Store::Command::Insert::LastInsertId->new(
-      id_column => 'id',
-      raw_store => $self->raw_store,
-      insert_call_command => $self->raw_store->new_call_command(@_)
-    );
-  },
-  delete_sql => q{DELETE FROM person WHERE id = ?},
-  delete_argument_order => [ 'id' ],
-};
+  $set = make_set { class => 'Spoon' };
 
-my $doug = $set->add({ name => 'Doug' });
+  is_deeply(
+    [ sort_set $set->flatten ],
+    [ map { bless({ %$_ }, 'Spoon') } @expect ],
+    'Basic data with class out ok'
+  );
 
-ok($doug->{id}, 'id filled out in new row');
+  $set = make_set {}, {
+    insert_sql => q{INSERT INTO person (name) VALUES (?) },
+    insert_argument_order => [ 'name' ],
+    insert_command_constructor => sub {
+      require DBIx::Data::Store::Command::Insert::LastInsertId;
+      my $self = shift;
+      DBIx::Data::Store::Command::Insert::LastInsertId->new(
+        id_column => 'id',
+        raw_store => $self->raw_store,
+        insert_call_command => $self->raw_store->new_call_command(@_)
+      );
+    },
+    delete_sql => q{DELETE FROM person WHERE id = ?},
+    delete_argument_order => [ 'id' ],
+  };
 
-my ($set_doug) = grep $_->{name} eq 'Doug', $set->flatten;
+  my $doug = $set->add({ name => 'Doug' });
 
-ok($set_doug, 'new row exists in flatten');
+  ok($doug->{id}, 'id filled out in new row');
 
-cmp_ok(refaddr($doug), '==', refaddr($set_doug), 'Same hashref returned');
+  my ($set_doug) = grep $_->{name} eq 'Doug', $set->flatten;
 
-$set->remove($doug);
+  ok($set_doug, 'new row exists in flatten');
 
-is_deeply([ sort_set $set->flatten ], \@expect, 'new row gone after remove');
+  cmp_ok(refaddr($doug), '==', refaddr($set_doug), 'Same hashref returned');
 
-$set = make_set;
+  $set->remove($doug);
 
-is_deeply([ sort_set $set->flatten ], \@expect, 'new row still gone on reload');
+  is_deeply([ sort_set $set->flatten ], \@expect, 'new row gone after remove');
 
-$set = make_set {}, {
-  update_sql => q{UPDATE person SET name = ? WHERE id = ?},
-  update_argument_order => [ qw(name id) ]
-};
+  $set = make_set;
 
-my ($pterry) = grep $_->{name} eq 'Pterry', $set->flatten;
+  is_deeply([ sort_set $set->flatten ], \@expect, 'new row still gone on reload');
 
-$pterry->{name} = 'Sir Pterry'; # http://xrl.us/bgse8s
+  $set = make_set {}, {
+    update_sql => q{UPDATE person SET name = ? WHERE id = ?},
+    update_argument_order => [ qw(name id) ]
+  };
+
+  my ($pterry) = grep $_->{name} eq 'Pterry', $set->flatten;
 
-$set->_update_in_store($pterry);
+  $pterry->{name} = 'Sir Pterry'; # http://xrl.us/bgse8s
 
-$set = make_set;
+  $set->_update_in_store($pterry);
 
-my ($fresh_pterry) = grep $_->{name} =~ /Pterry/, $set->flatten;
+  $set = make_set;
 
-is($fresh_pterry->{name}, 'Sir Pterry', 'Update persisted correctly');
+  my ($fresh_pterry) = grep $_->{name} =~ /Pterry/, $set->flatten;
 
-$set = make_set {}, {
-  select_single_sql => q{SELECT id, name FROM person WHERE id = ?},
-  select_single_argument_order => [ qw(id) ],
-};
+  is($fresh_pterry->{name}, 'Sir Pterry', 'Update persisted correctly');
+
+  $set = make_set {}, {
+    select_single_sql => q{SELECT id, name FROM person WHERE id = ?},
+    select_single_argument_order => [ qw(id) ],
+  };
 
-my $pterry_id = (grep $_->{name} eq 'Pterry', @expect)[0]->{id};
+  my $pterry_id = (grep $_->{name} eq 'Pterry', @expect)[0]->{id};
 
-$pterry = $set->get({ id => $pterry_id });
+  $pterry = $set->get({ id => $pterry_id });
 
-is($pterry->{name}, 'Sir Pterry', 'Pterry retrieved by id');
+  is($pterry->{name}, 'Sir Pterry', 'Pterry retrieved by id');
 
-ok(!defined($set->get({ id => -1 })), 'undef on missing id');
+  ok(!defined($set->get({ id => -1 })), 'undef on missing id');
 
-$pterry->{name} = 'Pterry';
+  $pterry->{name} = 'Pterry';
+
+  is_deeply([ sort_set $set->flatten ], \@expect, 'Basic data after fetch by id');
+
+  done_testing;
+}
 
-is_deeply([ sort_set $set->flatten ], \@expect, 'Basic data after fetch by id');
+run_tests unless caller;
 
-done_testing;
+1;
diff --git a/t/02indexed_by.t b/t/02indexed_by.t
new file mode 100644 (file)
index 0000000..5b82cec
--- /dev/null
@@ -0,0 +1,46 @@
+use strict;
+use warnings FATAL => 'all';
+use Test::More;
+use DBIx::Data::Collection::Set::IndexableBy;
+
+BEGIN {
+  package BasicCollection;
+  require 't/01basic_collection.t'
+}
+
+sub setup_db { BasicCollection::setup_db @_ }
+
+sub make_store { BasicCollection::make_store @_ }
+
+sub make_set {
+  BasicCollection::make_set({
+      indexable_by => {
+        name => {
+          set_over => [ 'name' ],
+          store => make_store({
+            select_single_sql => q{SELECT id, name FROM person WHERE name = ?},
+            select_single_argument_order => [ qw(name) ],
+          }),
+        },
+      },
+    },
+    {
+      select_single_sql => q{SELECT id, name FROM person WHERE id = ?},
+      select_single_argument_order => [ qw(id) ],
+    },
+    'DBIx::Data::Collection::Set::IndexableBy'
+  )
+}
+
+
+sub run_tests {
+  my @expect = setup_db;
+  my $set = make_set;
+  use Devel::Dwarn;
+  Dwarn $set->get({ id => 1 });
+  my $by_name = $set->indexed_by('name');
+  Dwarn $by_name->get({ name => 'Pterry' });
+  done_testing;
+}
+
+run_tests unless caller;