Skip to content

Commit

Permalink
Merge pull request genome#58 from brummett/observe-once
Browse files Browse the repository at this point in the history
Observe once
  • Loading branch information
brummett committed Feb 18, 2015
2 parents ccf223b + 5b26fd5 commit 7f8f8d4
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 3 deletions.
5 changes: 4 additions & 1 deletion lib/UR/Context.pm
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,16 @@ sub send_notification_to_observers {
$sig_depth++;
if (@matches > 1) {
no warnings;
# sort by priority
@matches = sort { $a->[2] <=> $b->[2] } @matches;
};

foreach my $callback_info (@matches) {
my ($callback, $note) = @$callback_info;
my ($callback, $note, undef, $id, $once) = @$callback_info;
UR::Observer->get($id)->delete() if $once;
$callback->($subject, $property, @data);
}

$sig_depth--;

return scalar(@matches);
Expand Down
2 changes: 2 additions & 0 deletions lib/UR/Object.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,8 @@ Adds an observer to an object, monitoring one or more of its properties for chan
The specified callback is fired upon property changes which match the observation request.
See L<UR::Observer> for details.
=item create_mock
$mock = SomeClass->create_mock(
Expand Down
9 changes: 8 additions & 1 deletion lib/UR/Observer.pm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ UR::Object::Type->define(
aspect => { is => 'String', is_optional => 1 },
priority => { is => 'Number', is_optional => 1, default_value => 1 },
note => { is => 'String', is_optional => 1 },
once => { is => 'Boolean', is_optional => 1, default_value => 0 },
],
is_transactional => 1,
);
Expand Down Expand Up @@ -92,7 +93,7 @@ sub _create_or_define {
my ($subscription, $delete_subscription);

$self->_insert_record_into_all_change_subscriptions($subject_class_name, $aspect, $subject_id,
[$callback, $self->note, $self->priority, $self->id]);
[$callback, $self->note, $self->priority, $self->id, $self->once]);

return $self;
}
Expand Down Expand Up @@ -326,6 +327,12 @@ $self is the object that is changing, not the UR::Observer instance (unless,
of course, you have created an observer on UR::Observer). The return value of
the callback is ignored.
=item once
If the 'once' attribute is true, the observer is deleted immediately after
the callback is run. This has the effect of running the callback only once,
no matter how many times the observer condition is triggered.
=item note
A text string that is ignored by the system
Expand Down
65 changes: 64 additions & 1 deletion t/URT/t/21_observer.t
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use File::Basename;
use lib File::Basename::dirname(__FILE__)."/../../../lib";
use lib File::Basename::dirname(__FILE__)."/../..";
use URT;
use Test::More tests => 40;
use Test::More tests => 42;

UR::Object::Type->define(
class_name => 'URT::Parent',
Expand Down Expand Up @@ -156,6 +156,69 @@ is_deeply($observations,
is(get_change_count(), $change_count + 1, '1 change recorded');


subtest 'once observers' => sub {
plan tests => 12;

my($parent_observer_fired, $person_observer_fired) = (0,0);
ok(my $person_obs = URT::Person->add_observer(aspect => 'last_name', once => 1, callback => sub { $person_observer_fired++ } ),
'Add once observer to "last_name" aspect of URT::Person');
ok(my $parent_obs = URT::Parent->add_observer(aspect => 'last_name', once => 1, callback => sub { $parent_observer_fired++ } ),
'Add once observer to "last_name" aspect of URT::Parent');

$observations = {};
ok($p1->last_name('once'), 'changed person 1');
is_deeply($observations,
{ 'URT::Person' => { '' => 1, 'last_name' => 1 },
'URT::Parent' => { '' => 1, 'last_name' => 1 },
},
'Regular callbacks were fired') or diag explain $observations;
is($parent_observer_fired, 1, '"once" observer on URT::Parent was fired');
is($person_observer_fired, 1, '"once" observer on URT::Person was fired');

isa_ok($person_obs, 'UR::DeletedRef', 'Person observer is deleted');
isa_ok($parent_obs, 'UR::DeletedRef', 'Parent observer is deleted');

($parent_observer_fired, $person_observer_fired) = (0,0);
$observations = {};
ok($p1->last_name('once again'), 'changed person 1');
is_deeply($observations,
{ 'URT::Person' => { '' => 1, 'last_name' => 1 },
'URT::Parent' => { '' => 1, 'last_name' => 1 },
},
'Regular callbacks were fired') or diag explain $observations;
is($parent_observer_fired, 0, '"once" observer on URT::Parent was not fired');
is($person_observer_fired, 0, '"once" observer on URT::Person was not fired');
};

subtest 'once observer is removed before callback run' => sub {
plan tests => 5;

my $obj = URT::Person->create(first_name => 'bob', last_name => 'schmoe');
my $callback_run = 0;
our $in_observer = 0;
my $observer = $obj->add_observer(aspect => 'first_name',
once => 1,
callback => sub {
my($obj, $aspect, $old, $new) = @_;
local $in_observer = $in_observer + 1;
die "recursive call to observer" if $in_observer > 1;
$obj->$aspect($new . $new); # double up the new value
$callback_run++;
}
);
$obj->first_name('changed');
is($obj->first_name, 'changedchanged', 'Observer modified the new value');
is($callback_run, 1, 'callback was run once');
isa_ok($observer, 'UR::DeletedRef', 'Observer is deleted');


$callback_run = 0;
$obj->first_name('bob');
is($obj->first_name, 'bob', 'Changed value back');
is($callback_run, 0, 'Callback was not run');
};


sub get_change_count {
my @c = map { scalar($_->__changes__) } URT::Person->get;
my $sum = 0;
Expand Down

0 comments on commit 7f8f8d4

Please sign in to comment.