factor out sub quote unrolling code, add isa support
Matt S Trout [Sun, 7 Nov 2010 09:06:04 +0000 (09:06 +0000)]
lib/Method/Generate/Accessor.pm
lib/Method/Generate/Constructor.pm
lib/Sub/Quote.pm
t/accessor-isa.t [new file with mode: 0644]

index 7814fb8..c9b9dd5 100644 (file)
@@ -43,15 +43,22 @@ sub _generate_simple_get {
 sub _generate_set {
   my ($self, $name, $value, $spec) = @_;
   my $simple = $self->_generate_simple_set($name, $value);
-  if (my $trigger = $spec->{trigger}) {
-    my $value = '$value';
+  my ($trigger, $isa_check) = @{$spec}{qw(trigger isa)};
+  return $simple unless $trigger or $isa_check;
+  my $code = 'do {';
+  if ($isa_check) {
+    $code .= ' '.$self->_generate_isa_check($name, '$_[1]', $isa_check).';';
+  }
+  if ($trigger) {
     my $fire = $self->_generate_trigger($name, '$_[0]', '$value', $trigger);
-    return 'do { '
-      .'my $value = '.$simple.'; '.$fire.'; '
-      .'$value }'
-    ;
+    $code .=
+      ' my $value = '.$simple.'; '.$fire.'; '
+      .'$value';
+  } else {
+    $code .= ' '.$simple;
   }
-  return $simple;
+  $code .= ' }';
+  return $code;
 }
 
 sub generate_trigger {
@@ -63,21 +70,38 @@ sub generate_trigger {
 
 sub _generate_trigger {
   my ($self, $name, $obj, $value, $trigger) = @_;
-  if (my $quoted = quoted_from_sub($trigger)) {
+  $self->_generate_call_code($name, 'trigger', "${obj}, ${value}", $trigger);
+}
+
+sub generate_isa_check {
+  my $self = shift;
+  local $self->{captures} = {};
+  my $code = $self->_generate_isa_check(@_);
+  return ($code, $self->{captures});
+}
+
+sub _generate_isa_check {
+  my ($self, $name, $value, $check) = @_;
+  $self->_generate_call_code($name, 'isa_check', $value, $check);
+}
+
+sub _generate_call_code {
+  my ($self, $name, $type, $values, $sub) = @_;
+  if (my $quoted = quoted_from_sub($sub)) {
     my $code = $quoted->[1];
-    my $at_ = 'local @_ = ('.join(', ', $obj, $value).');';
+    my $at_ = 'local @_ = ('.$values.');';
     if (my $captures = $quoted->[2]) {
-      my $cap_name = qq{\$trigger_captures_for_${name}};
+      my $cap_name = qq{\$${type}_captures_for_${name}};
       $self->{captures}->{$cap_name} = \$captures;
       return "do {\n".'      '.$at_."\n"
         .Sub::Quote::capture_unroll($cap_name, $captures, 6)
         ."     ${code}\n    }";
     }
-    return 'do { local @_ = ('.join(', ', $obj, $value).'); '.$code.' }';
+    return 'do { local @_ = ('.$values.'); '.$code.' }';
   }
-  my $cap_name = qq{\$trigger_for_${name}};
-  $self->{captures}->{$cap_name} = \$trigger;
-  return "${cap_name}->(${obj}, ${value})";
+  my $cap_name = qq{\$${type}_for_${name}};
+  $self->{captures}->{$cap_name} = \$sub;
+  return "${cap_name}->(${values})";
 }
 
 sub _generate_simple_set {
index 6d45aee..8e2b6b8 100644 (file)
@@ -40,6 +40,7 @@ sub generate_method {
   my $body = '    my $class = shift;'."\n";
   $body .= $self->_generate_args;
   $body .= $self->_check_required($spec);
+  $body .= $self->_check_isa($spec);
   $body .= '    my $new = bless({}, $class);'."\n";
   $body .= $self->_assign_new($spec);
   $body .= $self->_fire_triggers($spec);
@@ -76,7 +77,7 @@ sub _check_required {
   my @required_init =
     map $spec->{$_}{init_arg},
       grep $spec->{$_}{required},
-        keys %$spec;
+        sort keys %$spec;
   return '' unless @required_init;
   '    if (my @missing = grep !exists $args->{$_}, qw('
     .join(' ',@required_init).')) {'."\n"
@@ -84,15 +85,30 @@ sub _check_required {
     ."    }\n";
 }
 
+sub _check_isa {
+  my ($self, $spec) = @_;
+  my $acc = $self->accessor_generator;
+  my $captures = $self->{captures};
+  my $check = '';
+  foreach my $name (sort keys %$spec) {
+    my ($init, $isa) = @{$spec->{$name}}{qw(init_arg isa)};
+    next unless $init and $isa;
+    my $init_str = perlstring($init);
+    my ($code, $add_captures) = $acc->generate_isa_check(
+      $name, "\$args->{${init_str}}", $isa
+    );
+    @{$captures}{keys %$add_captures} = values %$add_captures;
+    $check .= "    ${code} if exists \$args->{${init_str}};\n";
+  }
+  return $check;
+}
+
 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) {
+  foreach my $name (sort keys %$spec) {
     my ($init, $trigger) = @{$spec->{$name}}{qw(init_arg trigger)};
     next unless $init && $trigger;
     my ($code, $add_captures) = $acc->generate_trigger(
index 72af94e..cd6bff6 100644 (file)
@@ -29,7 +29,8 @@ sub capture_unroll {
 sub _unquote_all_outstanding {
   return unless %QUOTE_OUTSTANDING;
   my ($assembled_code, @assembled_captures, @localize_these) = '';
-  foreach my $outstanding (keys %QUOTE_OUTSTANDING) {
+  # we sort the keys in order to make debugging more predictable
+  foreach my $outstanding (sort keys %QUOTE_OUTSTANDING) {
     my ($name, $code, $captures) = @{$QUOTE_OUTSTANDING{$outstanding}};
 
     push @localize_these, $name if $name;
@@ -107,7 +108,7 @@ sub quote_sub {
 
 sub quoted_from_sub {
   my ($sub) = @_;
-  $QUOTED{$sub};
+  $QUOTED{$sub||''};
 }
 
 sub unquote_sub {
diff --git a/t/accessor-isa.t b/t/accessor-isa.t
new file mode 100644 (file)
index 0000000..2e3e9fd
--- /dev/null
@@ -0,0 +1,80 @@
+use strictures 1;
+use Test::More;
+use Test::Fatal;
+
+sub run_for {
+  my $class = shift;
+
+  my $obj = $class->new(less_than_three => 1);
+
+  is($obj->less_than_three, 1, 'initial value set');
+
+  like(
+    exception { $obj->less_than_three(4) },
+    qr/4 is not less than three/, 'exception thrown on bad set'
+  );
+
+  is($obj->less_than_three, 1, 'initial value remains after bad set');
+
+  my $ret;
+
+  is(
+    exception { $ret = $obj->less_than_three(2) },
+    undef, 'no exception on correct set'
+  );
+
+  is($ret, 2, 'correct setter return');
+  is($obj->less_than_three, 2, 'correct getter return');
+
+  is(exception { $class->new }, undef, 'no exception with no value');
+  like(
+    exception { $class->new(less_than_three => 12) },
+    qr/12 is not less than three/, 'exception thrown on bad constructor arg'
+  );
+}
+
+{
+  package Foo;
+
+  use Class::Tiny;
+
+  has less_than_three => (
+    is => 'rw',
+    isa => sub { die "$_[0] is not less than three" unless $_[0] < 3 }
+  );
+}
+
+run_for 'Foo';
+
+{
+  package Bar;
+
+  use Sub::Quote;
+  use Class::Tiny;
+
+  has less_than_three => (
+    is => 'rw',
+    isa => quote_sub q{ die "$_[0] is not less than three" unless $_[0] < 3 }
+  );
+}
+
+run_for 'Bar';
+
+{
+  package Baz;
+
+  use Sub::Quote;
+  use Class::Tiny;
+
+  has less_than_three => (
+    is => 'rw',
+    isa => quote_sub(
+      q{ die "$_[0] is not less than ${word}" unless $_[0] < $limit },
+      { '$limit' => \3, '$word' => \'three' }
+    )
+  );
+}
+
+run_for 'Baz';
+
+done_testing;