package Attribute::Property;

# $Id: Property.pm,v 1.48 2003/04/21 16:04:14 juerd Exp $
# v 1.25 -> CPAN as 1.02

use 5.006;
use Attribute::Handlers;
use Carp;

# use Want qw(want rreturn);
BEGIN {
    if (eval { require Want }) {
        *want    = Want::want;
        *rreturn = Want::rreturn;
    } else {
        *want    = sub { 0 };
        *rreturn = sub { 0 };
    }
}

no strict;
no warnings;

our $VERSION = '1.04';

$Carp::Internal{Attribute::Handlers}++;	 # may we be forgiven for our sins
$Carp::Internal{+__PACKAGE__}++;

my %p;

sub UNIVERSAL::Property : ATTR(CODE) {
	my (undef, $s, $r) = @_;
	croak "Cannot use Property attribute with anonymous sub" unless ref $s;
	my $n = *$s{NAME};
	*$s = defined &$s
		? sub : lvalue {
			croak "Too many arguments for $n method" if @_ > 2;
			if (want 'RVALUE') {
	   		        rreturn $_[0]{$n} if @_ != 2;
			        $r->($_[0], local $_ = $_[1]) 
				        or croak "Invalid value for $n property";
			        rreturn $_[0]{$n} = $_;
			}
			tie my $foo, __PACKAGE__, ${ \$_[0]{$n} }, $r, $_[0],$n;
			@_ == 2 ? ( $foo = $_[1] ) : $foo
		}
		: sub : lvalue {
			croak "Too many arguments for $n method" if @_ > 2;
			@_ == 2 ? ( $_[0]{$n} = $_[1] ) : ${ \$_[0]{$n} }
		};
	undef $p{*$s{PACKAGE}}{$n};
}

sub TIESCALAR { bless \@_, shift }  # @_ = (class, lvalue, subref, object, name)
sub FETCH { $_[0][0] }

sub STORE {
	$_[0][1]->($_[0][2], local $_ = $_[1])
	    or croak "Invalid value for $_[0][3] property";
	$_[0][0] = $_;
}

sub UNIVERSAL::New : ATTR(CODE) {
	my ($P, $s, $r) = @_;
	my $n = *$s{NAME};
	undef $r if not defined &$s;
	*$s = sub {
		my $c = shift;
		croak qq(Can't call method "$n" on a reference) if ref $c;
		croak "Odd number of arguments for $c->$n" if @_ % 2;
		my $o = bless {}, $c;
		my $l = \&Carp::shortmess;
		local *Carp::shortmess = sub { $_[-1] .= " in $c->$n"; &$l; };
		while (my ($p, $v) = splice @_, 0, 2) {
			exists $p{$P}{$p} or croak qq(No such property "$p");
			$o->$p($v);
		}
		return $r->($o) if $r;
		return $o;
	};
}

1;

=head1 NAME

Attribute::Property - Easy lvalue accessors with validation. ($foo->bar = 42)

=head1 SYNOPSIS

=head2 CLASS

    use Attribute::Property;
    use Carp;

    package SomeClass;

    sub new : New { further initialization here ... }
    
    sub nondigits : Property { /^\D+\z/ }
    sub digits    : Property { /^\d+\z/ or croak "custom error message" }
    sub anyvalue  : Property;
    sub another   : Property;

    sub value     : Property {
	my $self = shift;  # Object is accessible as $_[0]
	s/^\s+//;          # New value can be altered through $_ or $_[1]

	$_ <= $self->maximum or croak "Value exceeds maximum";
    }

    package Person;

    sub new  : New;
    sub name : Property;
    sub age  : Property { /^\d+\z/ and $_ > 0 }

=head2 USAGE

    my $object = SomeClass->new(digits => '123');

    $object->nondigits = "abc";
    $object->digits    = "123";
    $object->anyvalue  = "abc123\n";

    $object->anyvalue('archaic style still works');

    my $john = Person->new(name => 'John Doe', age => 19);
    
    $john->age++;
    printf "%s is now %d years old", $john->name, $john->age;

    # These would croak
    $object->nondigits = "987";
    $object->digits    = "xyz";

=head1 DESCRIPTION

This module introduces two attributes that make object oriented programming
much easier.  You can just define a constructor and some properties without
having to write accessors.

=over 4

=item C<Property>

    sub color : Property;
    sub color : Property { /^#[0-9A-F]{6}$/ }

The C<Property> attribute turns a method into an object property.  The original
code block is used only to validate new values, the module croaks if it returns
false.  The method returns an I<lvalue>, meaning that you can create a reference
to it, assign to it and apply a regex to it.

Undefined subs (subs that have been declared but do not have a code block) with
the C<Property> attribute will be properties without any value validation.

In the validation code block, the object is in C<$_[0]> and the value to be
validated is aliased as C<$_[1]> and for regexing convenience as C<$_>.

Feel free to croak explicitly if you don't want the default error message.

=item C<New>

    sub new : New;
    sub new : New { my $self = shift; ...; return $self; }

The C<New> attribute turns a method into an object constructor.  The original
code block can be used for further initialization, but it is completely
optional.

The constructor takes named arguments in C<< property => value >> pairs and
populates the hash with the given pairs.  After validating them, of course.

The new object is passed to the initialization code block as C<$_[0]>.  Be
sure to return the object if you use any initialization block.  If there is
no initialization code block, Attribute::Property takes care of returning
the new object.

=back

=head1 PREREQUISITES

Your object must be a blessed hash reference.  The property names will be used
for the hash keys.

For class properties of C<Some::Module>, the hash C<%Some::Module> is used.
For class properties of packages without C<::>, the behaviour is undefined.

In short: C<< $foo->bar = 14 >> and C<< $foo->bar(14) >> assign 14 to 
C<< $foo->{bar} >> after positive validation.  The same thing happens with C<< my
$foo = Class->new(bar => 14); >> given that C<Class::new> uses the C<New>
property.

If you have the Want module installed, Attribute::Property will use it to make
rvalue method calls more efficient.

=head1 COMPATIBILITY

Old fashioned C<< $object->property(VALUE) >> is still available.

This module requires a modern Perl (5.6.0+), fossils like Perl 5.00x don't
support our chicanery.

=head1 BUGS

=over 2

=item *

The C<New> attribute should really be called C<Constructor>, but that would
conflict with the existing Attribute::Constructor module.

=back

=head1 LICENSE

There is no license.  This software was released into the public domain.  Do
with it what you want, but on your own risk.  Both authors disclaim any
responsibility.

=head1 AUTHORS

Juerd Waalboer <juerd@cpan.org> <http://juerd.nl/>

Matthijs van Duin <xmath@cpan.org>

=cut

# vim: ft=perl sts=0 noet sw=8 ts=8

