package Class::Meta::Attribute;

=head1 NAME

Class::Meta::Attribute - Class::Meta class attribute introspection

=head1 SYNOPSIS

  # Assuming MyApp::Thingy was generated by Class::Meta.
  my $class = MyApp::Thingy->my_class;
  my $thingy = MyApp::Thingy->new;

  print "\nAttributes:\n";
  for my $attr ($class->attributes) {
      print "  o ", $attr->name, " => ", $attr->get($thingy), $/;
      if ($attr->authz >= Class::Meta::SET && $attr->type eq 'string') {
          $attr->get($thingy, 'hey there!');
          print "    Changed to: ", $attr->get($thingy) $/;
      }
  }

=head1 DESCRIPTION

An object of this class describes an attribute of a class created by
Class::Meta. It includes meta data such as the name of the attribute, its data
type, its accessibility, and whether or not a value is required. It also
provides methods to easily get and set the value of the attribute for a given
instance of the class.

Class::Meta::Attribute objects are created by Class::Meta; they are never
instantiated directly in client code. To access the attribute objects for a
Class::Meta-generated class, simply call its C<my_class()> method to retrieve
its Class::Meta::Class object, and then call the C<attributes()> method on the
Class::Meta::Class object.

=cut

##############################################################################
# Dependencies                                                               #
##############################################################################
use strict;

##############################################################################
# Package Globals                                                            #
##############################################################################
our $VERSION = '0.66';

##############################################################################
# Private Package Globals                                                    #
##############################################################################
my %type_pkg_for = (
    map( { $_ => 'Boolean' } qw(bool boolean) ),
    map( { $_ => 'Numeric' } qw(whole integer int decimal dec real float) ),
    map(
        { $_ => 'Perl' }
        qw(scalar scalarref array arrayref hash hashref code coderef closure)
    ),
    string => 'String',
);

##############################################################################
# Constructors                                                               #
##############################################################################

=head1 INTERFACE

=head2 Constructors

=head3 new

A protected method for constructing a Class::Meta::Attribute object. Do not
call this method directly; Call the
L<C<add_attribute()>|Class::Meta/"add_attribute"> method on a Class::Meta
object, instead.

=cut

sub new {
    my $pkg = shift;
    my $class = shift;

    # Check to make sure that only Class::Meta or a subclass is constructing a
    # Class::Meta::Attribute object.
    my $caller = caller;
    Class::Meta->handle_error("Package '$caller' cannot create $pkg "
                              . "objects")
      unless UNIVERSAL::isa($caller, 'Class::Meta')
        || UNIVERSAL::isa($caller, __PACKAGE__);

    # Make sure we can get all the arguments.
    $class->handle_error("Odd number of parameters in call to new() when "
                         . "named parameters were expected")
      if @_ % 2;
    my %p = @_;

    # Validate the name.
    $class->handle_error("Parameter 'name' is required in call to new()")
      unless $p{name};
    # Is this too paranoid?
    $class->handle_error("Attribute '$p{name}' is not a valid attribute "
                         . "name -- only alphanumeric and '_' characters "
                         . "allowed")
      if $p{name} =~ /\W/;

    # Grab the package name.
    $p{package} = $class->{package};

    # Set the required and once attributes.
    for (qw(required once)) {
        $p{$_} = $p{$_} ? 1 : 0;
    }

    # Make sure the name hasn't already been used for another attribute
    $class->handle_error("Attribute '$p{name}' already exists in class '"
                         . $class->{attrs}{$p{name}}{package} . "'")
      if ! delete $p{override} && exists $class->{attrs}{$p{name}};

    # Check the view.
    if (exists $p{view}) {
        $p{view} = Class::Meta::_str_to_const($p{view});
        $class->handle_error(
            "Not a valid view parameter: '$p{view}'"
        ) unless $p{view} == Class::Meta::PUBLIC
          or     $p{view} == Class::Meta::PROTECTED
          or     $p{view} == Class::Meta::TRUSTED
          or     $p{view} == Class::Meta::PRIVATE;
    } else {
        # Make it public by default.
        $p{view} = Class::Meta::PUBLIC;
    }

    # Check the authorization level.
    if (exists $p{authz}) {
        $p{authz} = Class::Meta::_str_to_const($p{authz});
        $class->handle_error(
            "Not a valid authz parameter: '$p{authz}'"
        ) unless $p{authz} == Class::Meta::NONE
          or     $p{authz} == Class::Meta::READ
          or     $p{authz} == Class::Meta::WRITE
          or     $p{authz} == Class::Meta::RDWR;
    } else {
        # Make it read/write by default.
        $p{authz} = Class::Meta::RDWR;
    }

    # Check the creation constant.
    if (exists $p{create}) {
        $p{create} = Class::Meta::_str_to_const($p{create});
        $class->handle_error(
            "Not a valid create parameter: '$p{create}'"
        ) unless $p{create} == Class::Meta::NONE
          or     $p{create} == Class::Meta::GET
          or     $p{create} == Class::Meta::SET
          or     $p{create} == Class::Meta::GETSET;
    } else {
        # Rely on the authz setting by default.
        $p{create} = $p{authz};
    }

    # Check the context.
    if (exists $p{context}) {
        $p{context} = Class::Meta::_str_to_const($p{context});
        $class->handle_error(
            "Not a valid context parameter: '$p{context}'"
        ) unless $p{context} == Class::Meta::OBJECT
          or     $p{context} == Class::Meta::CLASS;
    } else {
        # Put it in object context by default.
        $p{context} = Class::Meta::OBJECT;
    }

    # Check the type.
    $p{type} = delete $p{is} if exists $p{is};
    $p{type} ||= $class->default_type;
    $class->handle_error( "No type specified for the '$p{name}' attribute" )
        unless $p{type};
    unless ( eval { Class::Meta::Type->new($p{type}) } ) {
        my $pkg = $type_pkg_for{ $p{type} }
            or $class->handle_error( "Unknown type: '$p{type}'" );
        eval "require Class::Meta::Types::$pkg";
        $class->handle_error( "Unknown type: '$p{type}'" ) if $@;
        "Class::Meta::Types::$pkg"->import;
    }

    # Check the default.
    if (exists $p{default}) {
        # A code ref should be executed when the default is called.
        $p{_def_code} = delete $p{default}
          if ref $p{default} eq 'CODE';
    }

    # Create and cache the attribute object.
    $class->{attrs}{$p{name}} = bless \%p, ref $pkg || $pkg;

    # Index its view.
    push @{ $class->{all_attr_ord} }, $p{name};
    if ($p{view} > Class::Meta::PRIVATE) {
        push @{$class->{prot_attr_ord}}, $p{name}
          unless $p{view} == Class::Meta::TRUSTED;
        if ($p{view} > Class::Meta::PROTECTED) {
            push @{$class->{trst_attr_ord}}, $p{name};
            push @{$class->{attr_ord}}, $p{name}
              if $p{view} == Class::Meta::PUBLIC;
        }
    }

    # Store a reference to the class object.
    $p{class} = $class;

    # Let 'em have it.
    return $class->{attrs}{$p{name}};
}

##############################################################################
# Instance Methods                                                           #
##############################################################################

=head2 Instance Methods

=head3 name

  my $name = $attr->name;

Returns the name of the attribute.

=head3 type

  my $type = $attr->type;

Returns the name of the attribute's data type. Typical values are "scalar",
"string", and "boolean". See L<Class::Meta|Class::Meta/"Data Types"> for a
complete list.

=head3 is

  if ($attr->is('string')) {
      # ...
  }

A convenience method for C<< $attr->type eq $type >>.

=head3 desc

  my $desc = $attr->desc;

Returns a description of the attribute.

=head3 label

  my $label = $attr->label;

Returns a label for the attribute, suitable for use in a user interface. It is
distinguished from the attribute name, which functions to name the accessor
methods for the attribute.

=head3 required

  my $req = $attr->required;

Indicates if the attribute is required to have a value.

=head3 once

  my $once = $attr->once;

Indicates whether an attribute value can be set to a defined value only once.

=head3 package

  my $package = $attr->package;

Returns the package name of the class that attribute is associated with.

=head3 view

  my $view = $attr->view;

Returns the view of the attribute, reflecting its visibility. The possible
values are defined by the following constants:

=over 4

=item Class::Meta::PUBLIC

=item Class::Meta::PRIVATE

=item Class::Meta::TRUSTED

=item Class::Meta::PROTECTED

=back

=head3 context

  my $context = $attr->context;

Returns the context of the attribute, essentially whether it is a class or
object attribute. The possible values are defined by the following constants:

=over 4

=item Class::Meta::CLASS

=item Class::Meta::OBJECT

=back

=head3 authz

  my $authz = $attr->authz;

Returns the authorization for the attribute, which determines whether it can be
read or changed. The possible values are defined by the following constants:

=over 4

=item Class::Meta::READ

=item Class::Meta::WRITE

=item Class::Meta::RDWR

=item Class::Meta::NONE

=back

=head3 class

  my $class = $attr->class;

Returns the Class::Meta::Class object that this attribute is associated
with. Note that this object will always represent the class in which the
attribute is defined, and I<not> any of its subclasses.

=cut

sub name     { $_[0]->{name}     }
sub type     { $_[0]->{type}     }
sub desc     { $_[0]->{desc}     }
sub label    { $_[0]->{label}    }
sub required { $_[0]->{required} }
sub once     { $_[0]->{once}     }
sub package  { $_[0]->{package}  }
sub view     { $_[0]->{view}     }
sub context  { $_[0]->{context}  }
sub authz    { $_[0]->{authz}    }
sub class    { $_[0]->{class}    }
sub is       { $_[0]->{type} eq $_[1] }

##############################################################################

=head3 default

  my $default = $attr->default;

Returns the default value for a new instance of this attribute. Since the
default value can be determined dynamically, the value returned by
C<default()> may change on subsequent calls. It all depends on what was
passed for the C<default> parameter in the call to C<add_attribute()> on the
Class::Meta object that generated the class.

=cut

sub default {
    if (my $code = $_[0]->{_def_code}) {
        return $code->();
    }
    return $_[0]->{default};
}

##############################################################################

=head3 get

  my $value = $attr->get($thingy);

This method calls the "get" accessor method on the object passed as the sole
argument and returns the value of the attribute for that object. Note that it
uses a C<goto> to execute the accessor, so the call to C<set()> itself
will not appear in a call stack trace.

=cut

sub get {
    my $self = shift;
    my $code = $self->{_get} or $self->class->handle_error(
        q{Cannot get attribute '}, $self->name, q{'}
    );
    goto &$code;
}

##############################################################################

=head3 set

  $attr->set($thingy, $new_value);

This method calls the "set" accessor method on the object passed as the first
argument and passes any remaining arguments to assign a new value to the
attribute for that object. Note that it uses a C<goto> to execute the
accessor, so the call to C<set()> itself will not appear in a call stack
trace.

=cut

sub set {
    my $self = shift;
    my $code = $self->{_set} or $self->class->handle_error(
        q{Cannot set attribute '}, $self->name, q{'}
    );
    goto &$code;
}

##############################################################################

=head3 build

  $attr->build($class);

This is a protected method, designed to be called only by the Class::Meta
class or a subclass of Class::Meta. It takes a single argument, the
Class::Meta::Class object for the class in which the attribute was defined,
and generates attribute accessors by calling out to the C<make_attr_get()> and
C<make_attr_set()> methods of Class::Meta::Type as appropriate for the
Class::Meta::Attribute object.

Although you should never call this method directly, subclasses of
Class::Meta::Constructor may need to override its behavior.

=cut

sub build {
    my ($self, $class) = @_;

    # Check to make sure that only Class::Meta or a subclass is building
    # attribute accessors.
    my $caller = caller;
    $self->class->handle_error(
        "Package '$caller' cannot call " . ref($self) . "->build"
    ) unless UNIVERSAL::isa($caller, 'Class::Meta')
          || UNIVERSAL::isa($caller, __PACKAGE__);

    # Get the data type object and build any accessors.
    my $type = Class::Meta::Type->new($self->{type});
    $self->{type} = $type->key;
    my $create = delete $self->{create};
    $type->build($class->{package}, $self, $create)
        if $create != Class::Meta::NONE;

    # Create the attribute object get code reference.
    if ($self->{authz} >= Class::Meta::READ) {
        $self->{_get} = $type->make_attr_get($self);
    }

    # Create the attribute object set code reference.
    if ($self->{authz} >= Class::Meta::WRITE) {
        $self->{_set} = $type->make_attr_set($self);
    }

}

1;
__END__

=head1 SUPPORT

This module is stored in an open L<GitHub
repository|http://github.com/theory/class-meta/>. Feel free to fork and
contribute!

Please file bug reports via L<GitHub
Issues|http://github.com/theory/class-meta/issues/> or by sending mail to
L<bug-Class-Meta@rt.cpan.org|mailto:bug-Class-Meta@rt.cpan.org>.

=head1 AUTHOR

David E. Wheeler <david@justatheory.com>

=head1 SEE ALSO

Other classes of interest within the Class::Meta distribution include:

=over 4

=item L<Class::Meta|Class::Meta>

=item L<Class::Meta::Class|Class::Meta::Class>

=item L<Class::Meta::Method|Class::Meta::Method>

=item L<Class::Meta::Constructor|Class::Meta::Constructor>

=item L<Class::Meta::Type|Class::Meta::Type>

=back

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2002-2011, David E. Wheeler. Some Rights Reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut
