Re: invalid installdirs values

[email protected] ("David E. Wheeler")
Newsgroups perl.module.build
Message-ID <[email protected]>
On Sep 26, 2008, at 19:21, David E. Wheeler wrote:

> On Sep 26, 2008, at 18:48, Eric Wilhelm wrote:
>
>> I would rather it just be a hash.  The ({}) thing is redundant.
>
> You mean you'd rather it be a list. I agree, but it needs to be a  
> hashref in order to disambiguate it from the original calling  
> syntax, in order to maintain backward compatibility.
>
>> So, $name is required, and if what's left is an even number, it's a
>> hash.  If it's odd, $default = shift(@_).
>
> Hrm. Okay, that works, too.

And here it is. This patch makes the following changes:

* Default values passed to `add_property()` may now be code  
references, in which case the return value of the code ref will be  
used as the default.

* Add property now has the above-discussed interface. The `default`  
argument or parameter can be a code reference, as just described. The  
`check` parameter is an optional code reference that must return true  
if a new value is valid, and false it is invalid. If the property is a  
hash, the entire hash will be verified once new values have been  
assigned. I've also more tightly scoped the use of `no strict 'refs';`  
in the method, so that accessors still work when called as class  
methods (Huh?).

* Added `property_error()`. This method should be called by check code  
refs that detect invalid values. Right now it just calls `die`. Let me  
know if it should do something different.

* Made use of the new functionality when defining the "installdirs"  
property, per Schwern's example. What other properties should we check?

* Added tests for this stuff.

* Documented `add_property()` and `property_error()`. I had no idea  
that `add_property()` was never documented before! Check those docs  
for more details on how `add_property()` works.

Best,

David
add_property.patch (application/octet-stream, 11.8 KB)
Index: t/add_property.t
===================================================================
--- t/add_property.t	(revision 0)
+++ t/add_property.t	(revision 0)
@@ -0,0 +1,88 @@
+#!/usr/bin/perl -w
+
+use strict;
+use lib $ENV{PERL_CORE} ? '../lib/Module/Build/t/lib' : 't/lib';
+use MBTest tests => 29;
+#use MBTest 'no_plan';
+use DistGen;
+
+BEGIN { use_ok 'Module::Build' or die; }
+ensure_blib 'Module::Build';
+
+my $tmp = MBTest->tmpdir;
+my $dist = DistGen->new( dir => $tmp );
+$dist->regen;
+$dist->chdir_in;
+
+ADDPROP: {
+    package My::Build::Prop;
+    use base 'Module::Build';
+    __PACKAGE__->add_property( 'foo' );
+    __PACKAGE__->add_property( 'bar', 'howdy' );
+    __PACKAGE__->add_property( 'baz', default => 'howdy' );
+    __PACKAGE__->add_property( 'code', default => sub { 'yay' } );
+    __PACKAGE__->add_property(
+        'check',
+        default => sub { 'howdy' },
+        check   => sub {
+            return 1 if $_ eq 'howdy';
+            shift->property_error(qq{"$_" is invalid});
+            return 0;
+        },
+    );
+    __PACKAGE__->add_property(
+        'hash',
+        default => { foo => 1 },
+        check   => sub {
+            return 1 if !defined $_ or exists $_->{foo};
+            shift->property_error(qq{hash is invalid});
+            return 0;
+        },
+    );
+}
+
+ok my $build = My::Build::Prop->new(
+    'module_name' => 'Simple',
+    quiet => 1,
+), 'Create new build object';
+
+is $build->foo, undef, 'Property "foo" should be undef';
+ok $build->foo(42), 'Set "foo"';
+is $build->foo, 42, 'Now "foo" should have new value';
+
+is $build->bar, 'howdy', 'Property "bar" should be its default';
+ok $build->bar('yo'), 'Set "bar"';
+is $build->bar, 'yo', 'Now "bar" should have new value';
+
+is $build->check, 'howdy', 'Property "check" should be its default';
+eval { $build->check('yo') };
+ok my $err = $@, 'Should get an error for an invalid value';
+like $err, qr/^ERROR: "yo" is invalid/, 'It should be the correct error';
+
+is $build->code, 'yay', 'Property "code" should have its code value';
+
+is_deeply $build->hash, { foo => 1 }, 'Property "hash" should be default';
+is $build->hash('foo'), 1, 'Should be able to get key in hash';
+ok $build->hash( bar => 3 ), 'Add a key to the hash prop';
+is_deeply $build->hash, { foo => 1, bar => 3 }, 'New key should be in hash';
+eval { $build->hash({ bar => 3 }) };
+ok $err = $@, 'Should get exception for assigning invalid hash';
+like $err, qr/^ERROR: hash is invalid/, 'It should be the correct error';
+eval { $build->hash( []) };
+ok $err = $@, 'Should get exception for assigning an array for a hash';
+like $err, qr/^Unexpected arguments for property 'hash'/,
+    'It should be the proper error';
+is $build->hash(undef), undef, 'Should be able to set hash to undef';
+
+# Check core properties.
+is $build->installdirs, 'site', 'Property "installdirs" should be default';
+ok $build->installdirs('core'), 'Set "installdirst" to "core"';
+is $build->installdirs, 'core', 'Now "installdirs" should be "core"';
+eval { $build->installdirs('perl') };
+ok $err = $@, 'Should have caught exception setting "installdirs" to "perl"';
+like $err, qr/^ERROR: Perhaps you meant installdirs to be "core" rather than "perl"\?/,
+    'And it should suggest "core" in the error message';
+eval { $build->installdirs('foo') };
+ok $err = $@, 'Should catch exception for invalid "installdirs" value';
+like $err, qr/ERROR: installdirs must be one of "core", "site", or "vendor"/,
+    'And it should suggest the proper values in the error message';
Index: lib/Module/Build/Base.pm
===================================================================
--- lib/Module/Build/Base.pm	(revision 11885)
+++ lib/Module/Build/Base.pm	(working copy)
@@ -665,7 +665,9 @@
     sub valid_properties_defaults {
       my %out;
       for (reverse shift->_mb_classes) {
-	@out{ keys %{ $valid_properties{$_} } } = values %{ $valid_properties{$_} };
+	@out{ keys %{ $valid_properties{$_} } } = map {
+            $_->()
+        } values %{ $valid_properties{$_} };
       }
       return \%out;
     }
@@ -685,55 +687,67 @@
     }
 
     sub add_property {
-      my ($class, $property, $default) = @_;
-      die "Property '$property' already exists" if $class->valid_property($property);
+        my ($class, $property) = (shift, shift);
+        die "Property '$property' already exists"
+            if $class->valid_property($property);
+        my %p = @_ == 1 ? ( default => shift ) : @_;
 
-      $valid_properties{$class}{$property} = $default;
+        my $type = ref $p{default};
+        $valid_properties{$class}{$property} = $type eq 'CODE'
+            ? $p{default}
+            : sub { $p{default} };
 
-      my $type = ref $default;
-      if ($type) {
-	push @{$additive_properties{$class}->{$type}}, $property;
-      }
+        push @{$additive_properties{$class}->{$type}}, $property
+            if $type;
 
-      unless ($class->can($property)) {
-        no strict 'refs';
-	if ( $type eq 'HASH' ) {
-          *{"$class\::$property"} = sub {
-            # XXX this needs 'use strict' again
-	    my $self = shift;
-	    my $x = $self->{properties};
-	    return $x->{$property} unless @_;
+        return $class if $class->can($property);
 
-	    if ( defined($_[0]) && !ref($_[0]) ) {
-	      if ( @_ == 1 ) {
-		return exists( $x->{$property}{$_[0]} ) ?
-		         $x->{$property}{$_[0]} : undef;
-              } elsif ( @_ % 2 == 0 ) {
-	        my %args = @_;
-	        while ( my($k, $v) = each %args ) {
-	          $x->{$property}{$k} = $v;
-	        }
-	      } else {
-		die "Unexpected arguments for property '$property'\n";
-	      }
-	    } else {
-	      $x->{$property} = $_[0];
-	    }
-	  };
+        my $check = $p{check} || sub { 1 };
+        my $sub = $type eq 'HASH' ? sub {
+            my $self = shift;
+            # Allow to work in class or object context.
+            my $x = do { no strict 'refs'; $self->{properties} };
+            return $x->{$property} unless @_;
 
-        } else {
-          *{"$class\::$property"} = sub {
-            # XXX this needs 'use strict' again
-	    my $self = shift;
-	    $self->{properties}{$property} = shift if @_;
-	    return $self->{properties}{$property};
-	  }
-        }
+            my $prop = $x->{$property};
+            if ( defined $_[0] && !ref $_[0] ) {
+                if ( @_ == 1 ) {
+                    return exists $prop->{$_[0]} ? $prop->{$_[0]} : undef;
+                } elsif ( @_ % 2 == 0 ) {
+                    my %new = (%{ $prop }, @_);
+                    local $_ = \%new;
+                    $x->{$property} = \%new if $check->($self);
+                    return $x->{$property};
+                } else {
+                    die "Unexpected arguments for property '$property'\n";
+                }
+            } else {
+                die "Unexpected arguments for property '$property'\n"
+                    if defined $_[0] && ref $_[0] ne 'HASH';
+                local $_ = $_[0];
+                $x->{$property} = shift if $check->($self);
+            }
+        } : sub {
+            my $self = shift;
+            # Allow to work in class or object context.
+            my $x = do { no strict 'refs'; $self->{properties} };
+            return $x->{$property} unless @_;
+            local $_ = $_[0];
+            $x->{$property} = shift if $check->($self);
+            return $x->{$property};
+        };
 
-      }
-      return $class;
+        no strict 'refs';
+        *{"$class\::$property"} = $sub;
+
+        return $class;
     }
 
+    sub property_error {
+        my $self = shift;
+        die 'ERROR: ', @_;
+    }
+
     sub _set_defaults {
       my $self = shift;
 
@@ -772,7 +786,6 @@
 __PACKAGE__->add_property(build_bat => 0);
 __PACKAGE__->add_property(config_dir => '_build');
 __PACKAGE__->add_property(include_dirs => []);
-__PACKAGE__->add_property(installdirs => 'site');
 __PACKAGE__->add_property(metafile => 'META.yml');
 __PACKAGE__->add_property(recurse_into => []);
 __PACKAGE__->add_property(use_rcfile => 1);
@@ -782,6 +795,20 @@
 __PACKAGE__->add_property(test_file_exts => ['.t']);
 __PACKAGE__->add_property(use_tap_harness => 0);
 __PACKAGE__->add_property(tap_harness_args => {});
+__PACKAGE__->add_property(
+    'installdirs',
+    default => 'site',
+    check   => sub {
+        return 1 if /^(core|site|vendor)$/;
+        return shift->property_error(
+            $_ eq 'perl'
+            ? 'Perhaps you meant installdirs to be "core" rather than "perl"?'
+            : 'installdirs must be one of "core", "site", or "vendor"'
+        );
+        return shift->property_error("Perhaps you meant 'core'?") if $_ eq 'perl';
+        return 0;
+    },
+);
 
 {
   my $Is_ActivePerl = eval {require ActivePerl::DocTools};
Index: lib/Module/Build/API.pod
===================================================================
--- lib/Module/Build/API.pod	(revision 11885)
+++ lib/Module/Build/API.pod	(working copy)
@@ -791,9 +791,89 @@
 defaults to C<MyModuleBuilder>.  The C<code> parameter specifies Perl
 code to use as the body of the subclass.
 
+=item add_property
+
+[version 0.31]
+
+  package 'My::Build';
+  use base 'Module::Build';
+  __PACKAGE__->add_property( 'pedantic' );
+  __PACKAGE__->add_property( answer => 42 );
+  __PACKAGE__->add_property(
+      'epoch',
+      default => sub { time },
+      check   => sub {
+          return 1 if /^\d+$/;
+          shift->property_error( "'$_' is not an epoch time" );
+          return 0;
+      },
+  );
+
+Adds a property to a Module::Build class. Properties are those attributes of a
+Module::Build object which can be passed to the constructorand which have
+accessors to get and set them. All of the core properties, such as
+C<module_name> and C<license>, are defined using this class method.
+
+The first argument to C<add_property()> is always the name of the property.
+The second argument can be either a default value for the property, or a list
+of key/value pairs. The supported keys are:
+
+=over
+
+=item C<default>
+
+The default value. May optionally be specified as a code reference, in which
+case the return value from the execution of the code reference will be used.
+If you need the default to be a code reference, just use a code reference to
+return it, e.g.:
+
+      default => sub { sub { ... } },
+
+=item C<check>
+
+A code reference that checks that a value specified for the property is valid.
+During the execution of the code reference, the new value will be included in
+the C<$_> variable. If the value is correct, the C<check> code reference
+should return true. If the value is not correct, it send an error message to
+C<property_error()> and return false.
+
 =back
 
+When this method is called, a new property will be installed in the
+Module::Build class, and an accessor will be built to allow the property to be
+get or set on the build object.
 
+  print $build->pedantic, $/;
+  $build->pedantic(0);
+
+If the default value is a hash reference, this generetes a special-case
+accessor method, wherein individual key/value pairs may be set or fetched:
+
+  print "stuff{foo} is: ", $build->stuff( 'foo' ), $/;
+  $build->stuff( foo => 'bar' );
+  print $build->stuff( 'foo' ), $/; # Outputs "bar"
+
+Of course, you can still set the entire hash reference at once, as well:
+
+  $build->stuff( { foo => 'bar', baz => 'yo' } );
+
+In either case, if a C<check> has been specified for the property, it will be
+applied to the entire hash. So the check code reference should look something
+like:
+
+      check => sub {
+            return 1 if defined $_ && exists $_->{foo};
+            shift->property_error(qq{Property "stuff" needs "foo"});
+            return 0;
+      },
+
+=item property_error
+
+[version 0.31]
+
+=back
+
+
 =head2 METHODS
 
 =over 4
@@ -883,7 +963,6 @@
 compiling & linking C code.  If no such object is available (e.g. if
 the system has no compiler installed) an exception will be thrown.
 
-
 =item check_installed_status($module, $version)
 
 [version 0.11]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.