Added support for fancy triggers, and a test.
Paul Driver [Fri, 18 Jul 2008 20:09:41 +0000 (20:09 +0000)]
lib/Moose/Meta/Attribute.pm
lib/Moose/Meta/Class.pm
lib/Moose/Meta/Method/Accessor.pm
lib/Moose/Meta/Method/Constructor.pm
t/020_attributes/004_attribute_triggers.t

index d631120..1ad7550 100644 (file)
@@ -312,8 +312,21 @@ sub _process_options {
     }
 
     if (exists $options->{trigger}) {
-        ('CODE' eq ref $options->{trigger})
-            || confess "Trigger must be a CODE ref on attribute ($name)";
+        my $trig = $options->{trigger};
+        if ('HASH' eq ref $trig) {
+            my $legal = qr{^(?:before|after|around)$};
+            foreach my $key (keys %$trig) {
+                ($key =~ $legal)
+                    || confess "$key is an illegal trigger specifier"
+                    . " on attribute ($name)";
+                ('CODE' eq ref $trig->{$key})
+                    || confess "$key trigger must be CODE ref"
+                    . " on attribute ($name)";
+            }
+        }
+        elsif ('CODE' ne ref $trig) {
+            confess "Trigger must be a CODE or HASH ref on attribute ($name)";
+        }
     }
 
     if (exists $options->{auto_deref} && $options->{auto_deref}) {
@@ -351,6 +364,73 @@ sub _process_options {
 
 }
 
+sub _with_inline_triggers {
+    my ($self, $instance, $value, $attr, $gen_code) = @_;
+    my @ga = ($instance, $value, $attr);
+    return $gen_code->(@ga) unless $self->has_trigger;
+
+    my $trigger_args = "$instance, $value, $attr";
+
+    if ('CODE' eq ref $self->trigger) {
+        return $gen_code->(@ga) . "$attr->trigger->($trigger_args);\n";
+    }
+
+    my $code = '';
+    my ($before, $around, $after) = @{$self->trigger}{qw(before around after)};
+
+    if ($before) {
+        $code .= "$attr->trigger->{before}->($trigger_args);\n";
+    }
+
+    if ($around) {
+        $code .= "$attr->trigger->{around}->(sub {\n"
+            . 'my ($instance, $value, $attr) = @_;' . "\n"
+            . $gen_code->('$instance', '$value', '$attr') 
+            . "}, $trigger_args);\n";
+    }
+    else {
+        $code .= $gen_code->(@ga);
+    }
+
+    if ($after) {
+        $code .= "$attr->trigger->{after}->($trigger_args);\n";
+    }
+
+    return $code;
+}
+
+sub _with_triggers {
+    my ($self, $instance, $value, $fn) = @_;
+    my @trigger_args = ($instance, $value, $self);
+    my ($before, $around, $after);
+
+    if ($self->has_trigger) {
+        my $trig = $self->trigger;
+
+        if ('HASH' eq ref $trig) {
+            ($before, $around, $after) = @{$trig}{qw(before around after)}
+        }
+        else {
+            $after = $trig;
+        }
+    }
+
+    if ($before) {
+        $before->(@trigger_args);
+    }
+
+    if ($around) {
+        $around->($fn, @trigger_args);
+    }
+    else {
+        $fn->(@trigger_args);
+    }
+
+    if ($after) {
+        $after->(@trigger_args);
+    }
+}
+
 sub initialize_instance_slot {
     my ($self, $meta_instance, $instance, $params) = @_;
     my $init_arg = $self->init_arg();
@@ -400,9 +480,14 @@ sub initialize_instance_slot {
                      . $type_constraint->get_message($val);
     }
 
-    $self->set_initial_value($instance, $val);
-    $meta_instance->weaken_slot_value($instance, $self->name)
-        if ref $val && $self->is_weak_ref;
+    $self->_with_triggers($instance, $val, sub {
+        my ($ins, $val, $attr) = @_;
+        my $mi = Class::MOP::Class->initialize(blessed($ins))
+                                  ->get_meta_instance;
+        $attr->set_initial_value($ins, $val);
+        $mi->weaken_slot_value($ins, $attr->name)
+            if ref $val && $attr->is_weak_ref;
+    });
 }
 
 ## Slot management
@@ -470,18 +555,14 @@ sub set_value {
                      . $type_constraint->get_message($value);
     }
 
-    my $meta_instance = Class::MOP::Class->initialize(blessed($instance))
-                                         ->get_meta_instance;
-
-    $meta_instance->set_slot_value($instance, $attr_name, $value);
-
-    if (ref $value && $self->is_weak_ref) {
-        $meta_instance->weaken_slot_value($instance, $attr_name);
-    }
-
-    if ($self->has_trigger) {
-        $self->trigger->($instance, $value, $self);
-    }
+    $self->_with_triggers($instance, $value, sub {
+        my ($ins, $val, $attr) = @_;
+        my $mi = Class::MOP::Class->initialize(blessed($ins))
+                                  ->get_meta_instance;
+        $mi->set_slot_value($ins, $attr->name, $val);
+        $mi->weaken_slot_value($ins, $attr->name)
+            if (ref $val && $attr->is_weak_ref);
+    });
 }
 
 sub get_value {
index 92a84b1..e24f08e 100644 (file)
@@ -120,36 +120,6 @@ sub excludes_role {
     return 0;
 }
 
-sub new_object {
-    my ($class, %params) = @_;
-    my $self = $class->SUPER::new_object(%params);
-    foreach my $attr ($class->compute_all_applicable_attributes()) {
-        # if we have a trigger, then ...
-        if ($attr->can('has_trigger') && $attr->has_trigger) {
-            # make sure we have an init-arg ...
-            if (defined(my $init_arg = $attr->init_arg)) {
-                # now make sure an init-arg was passes ...
-                if (exists $params{$init_arg}) {
-                    # and if get here, fire the trigger
-                    $attr->trigger->(
-                        $self, 
-                        # check if there is a coercion
-                        ($attr->should_coerce
-                            # and if so, we need to grab the 
-                            # value that is actually been stored
-                            ? $attr->get_read_method_ref->($self)
-                            # otherwise, just get the value from
-                            # the constructor params
-                            : $params{$init_arg}), 
-                        $attr
-                    );
-                }
-            }       
-        }
-    }
-    return $self;
-}
-
 sub construct_instance {
     my ($class, %params) = @_;
     my $meta_instance = $class->get_meta_instance;
@@ -470,10 +440,6 @@ and altering the Constructor metaclass.
 
 Override original to lock C<add_role> and memoize C<calculate_all_roles>
 
-=item B<new_object>
-
-We override this method to support the C<trigger> attribute option.
-
 =item B<construct_instance>
 
 This provides some Moose specific extensions to this method, you
index e96b83a..818d377 100644 (file)
@@ -51,7 +51,6 @@ sub generate_accessor_method_inline {
         . $self->_inline_check_coercion . "\n"
         . $self->_inline_check_constraint($value_name) . "\n"
         . $self->_inline_store($inv, $value_name) . "\n"
-        . $self->_inline_trigger($inv, $value_name) . "\n"
     . ' }' . "\n"
     . $self->_inline_check_lazy . "\n"
     . $self->_inline_post_body(@_) . "\n"
@@ -75,7 +74,6 @@ sub generate_writer_method_inline {
     . $self->_inline_check_constraint($value_name)
     . $self->_inline_store($inv, $value_name)
     . $self->_inline_post_body(@_)
-    . $self->_inline_trigger($inv, $value_name)
     . ' }');
 }
 
@@ -215,21 +213,29 @@ sub _inline_init_slot {
 sub _inline_store {
     my ($self, $instance, $value) = @_;
     my $attr = $self->associated_attribute;
-    
     my $mi = $attr->associated_class->get_meta_instance;
     my $slot_name = sprintf "'%s'", $attr->slots;
-    
-    my $code = $mi->inline_set_slot_value($instance, $slot_name, $value)    . ";";
-    $code   .= $mi->inline_weaken_slot_value($instance, $slot_name, $value) . ";"
-        if $attr->is_weak_ref;
-    return $code;
-}
 
-sub _inline_trigger {
-    my ($self, $instance, $value) = @_;
-    my $attr = $self->associated_attribute;
-    return '' unless $attr->has_trigger;
-    return sprintf('$attr->trigger->(%s, %s, $attr);', $instance, $value);
+    my $gen_code = sub {
+        my ($ins_name, $val_name) = @_;
+        
+        my $code = $mi->inline_set_slot_value(
+            $ins_name, $slot_name, $val_name) . ";\n";
+
+        if ($attr->is_weak_ref) {
+            $code .= $mi->inline_weaken_slot_value(
+                $ins_name, $slot_name, $val_name) . ";\n";
+        }
+
+        return $code;
+    };
+
+    if ($attr->can('_with_inline_triggers')) {
+        return $attr->_with_inline_triggers(
+          $instance, $value, '$attr', $gen_code);
+    }
+
+    return $gen_code->($instance, $value, '$attr');
 }
 
 sub _inline_get {
index 6dc6f04..5803fdb 100644 (file)
@@ -83,7 +83,6 @@ sub initialize_body {
         $self->_generate_slot_initializer($_)
     } 0 .. (@{$self->attributes} - 1));
 
-    $source .= ";\n" . $self->_generate_triggers();    
     $source .= ";\n" . $self->_generate_BUILDALL();
 
     $source .= ";\n" . 'return $instance';
@@ -147,32 +146,6 @@ sub _generate_BUILDALL {
     return join ";\n" => @BUILD_calls;
 }
 
-sub _generate_triggers {
-    my $self = shift;
-    my @trigger_calls;
-    foreach my $i (0 .. $#{ $self->attributes }) {
-        my $attr = $self->attributes->[$i];
-        if ($attr->can('has_trigger') && $attr->has_trigger) {
-            if (defined(my $init_arg = $attr->init_arg)) {
-                push @trigger_calls => (
-                    '(exists $params->{\'' . $init_arg . '\'}) && do {' . "\n    "
-                    .   '$attrs->[' . $i . ']->trigger->('
-                    .       '$instance, ' 
-                    .        $self->meta_instance->inline_get_slot_value(
-                                 '$instance',
-                                 ("'" . $attr->name . "'")
-                             ) 
-                             . ', '
-                    .        '$attrs->[' . $i . ']'
-                    .   ');'
-                    ."\n}"
-                );
-            } 
-        }
-    }
-    return join ";\n" => @trigger_calls;    
-}
-
 sub _generate_slot_initializer {
     my $self  = shift;
     my $index = shift;
@@ -269,37 +242,37 @@ sub _generate_slot_initializer {
 sub _generate_slot_assignment {
     my ($self, $attr, $value, $index) = @_;
 
-    my $source;
-    
-    if ($attr->has_initializer) {
-        $source = (
-            '$attrs->[' . $index . ']->set_initial_value($instance, ' . $value . ');'
-        );        
-    }
-    else {
-        $source = (
-            $self->meta_instance->inline_set_slot_value(
-                '$instance',
-                ("'" . $attr->name . "'"),
-                $value
-            ) . ';'
-        );        
-    }
+    my $attr_name = "\$attrs->[$index]";
+    my $mi = $self->meta_instance;
+
+    my $gen_code = sub {
+        my ($ins_name, $val_name, $attr_name) = @_;
+        my @miargs = ($ins_name, (sprintf "'%s'", $attr->name), $val_name);
+        my $source;
+
+        if ($attr->has_initializer) {
+           $source = "$attr_name->set_initial_value($ins_name, $val_name);\n"; 
+        }
+        else {
+            $source = $mi->inline_set_slot_value(@miargs) . ";\n";
+        }
+
+        my $is_moose = $attr->isa('Moose::Meta::Attribute'); # XXX FIXME        
+
+        if ($is_moose && $attr->is_weak_ref) {
+            $source .= $mi->inline_weaken_slot_value(@miargs)
+                    .  "if ref $val_name;\n";
+        }
+
+        return $source;
+    };
     
-    my $is_moose = $attr->isa('Moose::Meta::Attribute'); # XXX FIXME        
-
-    if ($is_moose && $attr->is_weak_ref) {
-        $source .= (
-            "\n" .
-            $self->meta_instance->inline_weaken_slot_value(
-                '$instance',
-                ("'" . $attr->name . "'")
-            ) .
-            ' if ref ' . $value . ';'
-        );
+    if ($attr->can('_with_inline_triggers')) {
+        return $attr->_with_inline_triggers(
+            '$instance', $value, $attr_name, $gen_code);
     }
 
-    return $source;
+    return $gen_code->('$instance', $value, $attr_name);
 }
 
 sub _generate_type_coercion {
index b5cf34e..e4088a2 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 
 use Scalar::Util 'isweak';
 
-use Test::More tests => 26;
+use Test::More tests => 43;
 use Test::Exception;
 
 BEGIN {
@@ -102,19 +102,92 @@ BEGIN {
     ok(isweak($baz->{foo}), '... baz.foo is a weak reference');
 }
 
+# before/around/after triggers
+{
+    package Fweet;
+    use Moose;
+
+    has calls => (
+        is      => 'ro',
+        isa     => 'ArrayRef',
+        default => sub {[]},
+    );
+
+    sub called {
+        my ($self, $str, @args) = @_;
+        push(@{$self->calls}, $str);
+    }
+
+    has noise => (
+        is => 'rw',
+        default => 'Sartak',
+        trigger => {
+            before => sub {
+                $_[0]->called('before');
+            },
+            around => sub {
+                my ($ori, $self, $val, @whatever) = @_;
+                $self->called('around');
+                $ori->($self, $val.'-diddly', @whatever);
+            },
+            after => sub {
+                $_[0]->called('after');
+            },
+        },
+    );
+}
+
+sub fancy_trigger_tests
+{
+    my $type = shift;
+    my $blah;
+    ::lives_ok {
+        $blah = Fweet->new;
+    } "... $type constructor";
+    my $expected_calls = [qw(before around after)];
+
+    is_deeply($blah->calls, $expected_calls, "$type default triggered");
+    is($blah->noise, 'Sartak-diddly', "$type default around modified value");
+    @{$blah->calls} = ();
+
+    $blah->noise('argle-bargle');
+    is_deeply($blah->calls, $expected_calls, "$type set triggered");
+    is($blah->noise, 'argle-bargle-diddly', "$type set around modified value");
+
+    $blah = Fweet->new(noise => 'woot');
+    is_deeply($blah->calls, $expected_calls, "$type constructor triggered");
+    is($blah->noise, 'woot-diddly', "$type constructor around modified value");
+}
+
+{
+  fancy_trigger_tests('normal');
+  ::lives_ok {
+    Fweet->meta->make_immutable;
+  } '... make_immutable works';
+  fancy_trigger_tests('inline');
+}
+
 # some errors
 
 {
     package Bling;
     use Moose;
+
+    ::dies_ok {
+        has('bling' => (is => 'rw', trigger => {FAIL => sub {}}));
+    } '... hash specifier has to be before/around/after';
+
+    ::dies_ok {
+        has('bling' => (is => 'rw', trigger => {around => 'FAIL'}));
+    } '... hash specifier value must be CODE ref';
     
     ::dies_ok { 
         has('bling' => (is => 'rw', trigger => 'Fail'));
-    } '... a trigger must be a CODE ref';
+    } '... a trigger must be a CODE or HASH ref';
     
     ::dies_ok { 
         has('bling' => (is => 'rw', trigger => []));
-    } '... a trigger must be a CODE ref';    
+    } '... a trigger must be a CODE or HASH ref';    
 }