support trigger
Matt S Trout [Sun, 7 Nov 2010 06:58:11 +0000 (06:58 +0000)]
lib/Class/Tiny.pm
lib/Method/Generate/Accessor.pm
lib/Method/Generate/Constructor.pm
lib/Role/Tiny.pm
lib/Sub/Quote.pm
t/accessor-trigger.t [new file with mode: 0644]

index 91d6650..4484eca 100644 (file)
@@ -7,6 +7,7 @@ our %MAKERS;
 
 sub import {
   my $target = caller;
+  my $class = shift;
   strictures->import;
   *{_getglob("${target}::extends")} = sub {
     *{_getglob("${target}::ISA")} = \@_;
@@ -16,27 +17,15 @@ sub import {
     die "Only one role supported at a time by with" if @_ > 1;
     Role::Tiny->apply_role_to_package($_[0], $target);
   };
+  $MAKERS{$target} = {};
   *{_getglob("${target}::has")} = sub {
     my ($name, %spec) = @_;
     ($MAKERS{$target}{accessor} ||= do {
       require Method::Generate::Accessor;
       Method::Generate::Accessor->new
     })->generate_method($target, $name, \%spec);
-    ($MAKERS{$target}{constructor} ||= do {
-      require Method::Generate::Constructor;
-      Method::Generate::Constructor
-        ->new(package => $target)
-        ->install_delayed
-        ->register_attribute_specs(do {
-          my @spec;
-          if (my $super = do { no strict 'refs'; ${"${target}::ISA"}[0] }) {
-            if (my $con = $MAKERS{$super}{constructor}) {
-              @spec = %{$con->all_attribute_specs};
-            }
-          }
-          @spec;
-        });
-    })->register_attribute_specs($name, \%spec);
+    $class->_constructor_maker_for($target)
+          ->register_attribute_specs($name, \%spec);
   };
   foreach my $type (qw(before after around)) {
     *{_getglob "${target}::${type}"} = sub {
@@ -51,4 +40,30 @@ sub import {
   }
 }
 
+sub _constructor_maker_for {
+  my ($class, $target) = @_;
+  return unless $MAKERS{$target};
+  $MAKERS{$target}{constructor} ||= do {
+    require Method::Generate::Constructor;
+    Method::Generate::Constructor
+      ->new(
+        package => $target,
+        accessor_generator => do {
+          require Method::Generate::Accessor;
+          Method::Generate::Accessor->new;
+        }
+      )
+      ->install_delayed
+      ->register_attribute_specs(do {
+        my @spec;
+        if (my $super = do { no strict 'refs'; ${"${target}::ISA"}[0] }) {
+          if (my $con = $MAKERS{$super}{constructor}) {
+            @spec = %{$con->all_attribute_specs};
+          }
+        }
+        @spec;
+      });
+  }
+}
+
 1;
index b473764..04eede0 100644 (file)
@@ -9,36 +9,75 @@ use B 'perlstring';
 sub generate_method {
   my ($self, $into, $name, $spec, $quote_opts) = @_;
   die "Must have an is" unless my $is = $spec->{is};
-  my $name_str = perlstring $name;
+  local $self->{captures} = {};
   my $body = do {
     if ($is eq 'ro') {
-      $self->_generate_get($name_str)
+      $self->_generate_get($name)
     } elsif ($is eq 'rw') {
-      $self->_generate_getset($name_str)
+      $self->_generate_getset($name, $spec)
     } else {
       die "Unknown is ${is}";
     }
   };
   quote_sub
     "${into}::${name}" => '    '.$body."\n",
-    (ref($quote_opts) ? ({}, $quote_opts) : ())
+    $self->{captures}, $quote_opts||{}
   ;
 }
 
 sub _generate_get {
-  my ($self, $name_str) = @_;
-  "\$_[0]->{${name_str}}";
+  my ($self, $name) = @_;
+  $self->_generate_simple_get('$_[0]', $name);
+}
+
+sub generate_simple_get {
+  shift->_generate_simple_get(@_);
+}
+
+sub _generate_simple_get {
+  my ($self, $me, $name) = @_;
+  my $name_str = perlstring $name;
+  "${me}->{${name_str}}";
 }
 
 sub _generate_set {
-  my ($self, $name_str, $value) = @_;
+  my ($self, $name, $value, $spec) = @_;
+  my $simple = $self->_generate_simple_set($name, $value);
+  if (my $trigger = $spec->{trigger}) {
+    my $value = '$value';
+    my $fire = $self->_generate_trigger($name, '$_[0]', '$value', $trigger);
+    return 'do { '
+      .'my $value = '.$simple.'; '.$fire.'; '
+      .'$value }'
+    ;
+  }
+  return $simple;
+}
+
+sub generate_trigger {
+  my $self = shift;
+  local $self->{captures} = {};
+  my $code = $self->_generate_trigger(@_);
+  return ($code, $self->{captures});
+}
+
+sub _generate_trigger {
+  my ($self, $name, $obj, $value, $trigger) = @_;
+  my $cap_name = qq{\$trigger_for_${name}};
+  $self->{captures}->{$cap_name} = \$trigger;
+  "${cap_name}->(${obj}, ${value})";
+}
+
+sub _generate_simple_set {
+  my ($self, $name, $value) = @_;
+  my $name_str = perlstring $name;
   "\$_[0]->{${name_str}} = ${value}";
 }
 
 sub _generate_getset {
-  my ($self, $name_str) = @_;
-  q{(@_ > 1 ? }.$self->_generate_set($name_str, q{$_[1]})
-    .' : '.$self->_generate_get($name_str).')';
+  my ($self, $name, $spec) = @_;
+  q{(@_ > 1 ? }.$self->_generate_set($name, q{$_[1]}, $spec)
+    .' : '.$self->_generate_get($name).')';
 }
 
 1;
index 41af94e..6d45aee 100644 (file)
@@ -4,6 +4,7 @@ use strictures 1;
 use Sub::Quote;
 use base qw(Class::Tiny::Object);
 use Sub::Defer;
+use B 'perlstring';
 
 sub register_attribute_specs {
   my ($self, %spec) = @_;
@@ -15,6 +16,10 @@ sub all_attribute_specs {
   $_[0]->{attribute_specs}
 }
 
+sub accessor_generator {
+  $_[0]->{accessor_generator}
+}
+
 sub install_delayed {
   my ($self) = @_;
   my $package = $self->{package};
@@ -31,15 +36,17 @@ sub generate_method {
   foreach my $no_init (grep !exists($spec->{$_}{init_arg}), keys %$spec) {
     $spec->{$no_init}{init_arg} = $no_init;
   }
+  local $self->{captures} = {};
   my $body = '    my $class = shift;'."\n";
   $body .= $self->_generate_args;
   $body .= $self->_check_required($spec);
   $body .= '    my $new = bless({}, $class);'."\n";
   $body .= $self->_assign_new($spec);
+  $body .= $self->_fire_triggers($spec);
   $body .= '    return $new;'."\n";
   quote_sub
     "${into}::${name}" => $body,
-    (ref($quote_opts) ? ({}, $quote_opts) : ())
+    $self->{captures}, $quote_opts||{}
   ;
 }
 
@@ -77,4 +84,24 @@ sub _check_required {
     ."    }\n";
 }
 
+sub _fire_triggers {
+  my ($self, $spec) = @_;
+  my @fire = map {
+    [ $_, $spec->{$_}{init_arg}, $spec->{$_}{trigger} ]
+  } grep { $spec->{$_}{init_arg} && $spec->{$_}{trigger} } keys %$spec;
+  my $acc = $self->accessor_generator;
+  my $captures = $self->{captures};
+  my $fire = '';
+  foreach my $name (keys %$spec) {
+    my ($init, $trigger) = @{$spec->{$name}}{qw(init_arg trigger)};
+    next unless $init && $trigger;
+    my ($code, $add_captures) = $acc->generate_trigger(
+      $name, '$new', $acc->generate_simple_get('$new', $name), $trigger
+    );
+    @{$captures}{keys %$add_captures} = values %$add_captures;
+    $fire .= "    ${code} if exists \$args->{${\perlstring $init}};\n";
+  }
+  return $fire;
+}
+
 1;
index b830f64..337c25d 100644 (file)
@@ -97,22 +97,10 @@ sub apply_role_to_package {
     if ($INFO{$to}) {
       @{$INFO{$to}{attributes}||={}}{keys %$attr_info} = values %$attr_info;
     } else {
-      my $con = $Class::Tiny::MAKERS{$to}{constructor} ||= do {
-        require Method::Generate::Constructor;
-        Method::Generate::Constructor
-          ->new(package => $to)
-          ->install_delayed
-          ->register_attribute_specs(do {
-            my @spec;
-            if (my $super = do { no strict 'refs'; ${"${to}::ISA"}[0] }) {
-              if (my $con = $Class::Tiny::MAKERS{$super}{constructor}) {
-                @spec = %{$con->all_attribute_specs};
-              }
-            }
-            @spec;
-          });
-      };
-      $con->register_attribute_specs(%$attr_info);
+      # only fiddle with the constructor if the target is a Class::Tiny class
+      if (my $con = Class::Tiny->_constructor_maker_for($to)) {
+        $con->register_attribute_specs(%$attr_info);
+      }
     }
   }
 
index 86526ab..0fb6884 100644 (file)
@@ -54,21 +54,23 @@ sub _unquote_all_outstanding {
     $make_sub .= "}\n";
     $assembled_code .= $make_sub;
   }
+  my $debug_code = $assembled_code;
   if (@localize_these) {
-    $ENV{SUB_QUOTE_DEBUG} && warn
+    $debug_code =
       "# localizing: ".join(', ', @localize_these)."\n"
       .$assembled_code;
     $assembled_code = join("\n",
       (map { "local *${_};" } @localize_these),
-      'eval '.perlstring $assembled_code
+      'eval '.perlstring($assembled_code).'; die $@ if $@;'
     );
   } else {
     $ENV{SUB_QUOTE_DEBUG} && warn $assembled_code;
   }
-  _clean_eval $assembled_code, \@assembled_captures;
-  if ($@) {
-    die "Eval went very, very wrong:\n\n${assembled_code}\n\n$@";
+  $assembled_code .= "\n1;";
+  unless (_clean_eval $assembled_code, \@assembled_captures) {
+    die "Eval went very, very wrong:\n\n${debug_code}\n\n$@";
   }
+  $ENV{SUB_QUOTE_DEBUG} && warn $debug_code;
   %QUOTE_OUTSTANDING = ();
 }
 
diff --git a/t/accessor-trigger.t b/t/accessor-trigger.t
new file mode 100644 (file)
index 0000000..0af7402
--- /dev/null
@@ -0,0 +1,32 @@
+use strictures 1;
+use Test::More;
+
+my @one_tr;
+
+{
+  package Foo;
+
+  use Class::Tiny;
+
+  has one => (is => 'rw', trigger => sub { push @one_tr, $_[1] });
+}
+
+my $foo = Foo->new;
+
+ok(!@one_tr, "trigger not fired with no value");
+
+$foo = Foo->new(one => 1);
+
+is_deeply(\@one_tr, [ 1 ], "trigger fired on new");
+
+my $res = $foo->one(2);
+
+is_deeply(\@one_tr, [ 1, 2 ], "trigger fired on set");
+
+is($res, 2, "return from set ok");
+
+is($foo->one, 2, "return from accessor ok");
+
+is_deeply(\@one_tr, [ 1, 2 ], "trigger not fired for accessor as get");
+
+done_testing;