Category: Object Persistence
Author/Contact Info Sam Vilain <sam@vilain.net>
Description: Class::Tangram is a base class that lets you specify the allowable attributes of an object in a package exported variable, then magically gives you constructors and accessor/updating methods for those attributes. Basic type checking is thrown in for free. This is most useful in combination with Tangram (available in CPAN), because it shares exactly the same format for the object specification, which itself is inspired by UML. See the POD for more information. Note: This module is now on CPAN. The version here on Perlmonks might not be up to date.
package Class::Tangram;

# Copyright (c) 2001 Sam Vilain. All rights reserved. This program is
# free software; you can redistribute it and/or modify it under the
# same terms as Perl itself.

# Some modifications
# $Id: Tangram.pm,v 1.5 2001/10/10 17:41:00 sv Exp $
# Copyright © 2001 Micro Sharp Technologies, Inc., Vancouver, WA, USA
# Author: Karl M. Hegbloom <karlheg@microsharp.com>
# Perl Artistic Licence.

=head1 NAME

Class::Tangram - create constructors, accessor and update methods for
objects from a Tangram-compatible object specification.

=head1 SYNOPSIS

 package Orange;

 use base qw(Class::Tangram);
 use vars qw($schema);
 use Tangram::Ref;

 # define the schema (ie, allowed attributes) of this object.  See the
 # Tangram::Schema man page for more information on the syntax here.
 $schema = {
     table => "oranges",

     fields => {
         int => {
             juiciness => undef,
             segments => {
                 # here is a new one - this code reference is called
                 # when this attribute is set; it should die() on
                 # error, as it is wrapped in an eval { } block
                 check_func => sub {
                     die "too many segments"
                         if ($ {$_[0]} > 30);
                 },
                 # the default for this attribute.
                 init_default => 7,
             },
         },
         ref => {
             grower => undef,
         },
     },
 };
 Class::Tangram::import_schema("Orange");

 package Project;
 # here's where we build the individual object schemas into a
 # Tangram::Schema object, which the Tangram::Storage class uses to
 # know which tables and columns to find objects.
 use Tangram::Schema;

 my $dbschema = Tangram::Schema->new
     ({ classes => [ 'Orange' => $Orange::schema ]});

 sub schema { $dbschema };

 package main;

 # See Tangram::Relational for instructions on using "deploy" to
 # create the database this connects to.  You only have to do this if
 # you want to write the objects to a database.
 use Tangram::Relational;
 my ($dsn, $u, $p);
 my $storage = Tangram::Relational->connect(Project->schema,
                                            $dsn, $u, $p);

 # OK
 my $orange = Orange->new(juiciness => 8);
 my $juiciness = $orange->juiciness;   # returns 8

 # a "ref" must be set to a blessed object
 my $grower = bless { name => "Joe" }, "Farmer";
 $orange->set_grower ($grower);

 # these are all illegal
 eval { $orange->set_juiciness ("Yum"); }; print $@;
 eval { $orange->set_segments (31); }; print $@;
 eval { $orange->set_grower ("Mr. Nice"); }; print $@;

 # if you prefer
 $orange->get( "juiciness" );
 $orange->set( juiciness => 123 );

=head1 DESCRIPTION

Class::Tangram is a base class originally intended for use with
Tangram objects, that gives you free constructors, access methods,
update methods, and a destructor that should help in breaking circular
references for you. Type checking is achieved by parsing the schema
for the object, which is contained within the object class in an
exported variable C<$schema>.

After writing this I found that it was useful for merely adding type
checking and validation to arbitrary objects.  There are several
modules on CPAN to do that already, but many don't have finely grained
type checking, and none of them integrated with Tangram.

=cut

use strict;
use Carp qw(croak cluck);

use vars qw($AUTOLOAD $VERSION);
$VERSION = "1.04";

local $AUTOLOAD;

# $types{$class}->{$attribute} is the tangram type of each attribute
my (%types);

# $check{$class}->{$attribute}->($value) is a function that will die
# if $value is not alright, see check_X functions
my (%check);

# Destructors for each attribute.  They are called as
# $cleaners{$class}->{$attribute}->($self, $attribute);
my (%cleaners);

# init_default values for each attribute.  These could be hash refs,
# array refs, code refs, or simple scalars.  They will be stored as
# $init_defaults{$class}->{$attribute}
my (%init_defaults);

# if a class is abstract, complain if one is constructed.
my (%abstract);

=head1 METHODS

=over 4

=item Class-E<gt>new (attribute1 =E<gt> value, attribute2 =E<gt> value
+)

sets up a new object of type Class, with attributes set to the values
supplied.

Can also be used as an object method, in which case it returns a
B<copy> of the object, without any deep copying.

=cut

sub new ($@)
{
    my $invocant = shift;
    my $class = ref $invocant || $invocant;

    my @values = @_;

    # Setup the object
    my $self = { };
    bless $self, $class;

    exists $check{$class} or import_schema($class);

    croak "Attempt to instantiate an abstract type"
 if ($abstract{$class});

    if ($invocant ne $class)
    {
 # The copy constructor; this could be better :)
 # this has the side effect of much auto-vivification.
 %$self = %$invocant;
 $self->set (@values); # override with @values
    }
    else
    {
 $self->set (@values); # start with @values

 # now fill in fields that have defaults
 for my $attribute (keys %{$init_defaults{$class}}) {

     next if (exists $self->{$attribute});

     my $default = $init_defaults{$class}->{$attribute}
  unless tied $init_defaults{$class}->{$attribute};

     if (ref $default eq "CODE") {
  # sub { }, attribute gets return value
  $self->{$attribute}
      = $init_defaults{$class}->{$attribute}->();

     } elsif (ref $default eq "HASH") {
  # hash ref, copy hash
  $self->{$attribute}
      = { %{ $init_defaults{$class}->{$attribute} } };

     } elsif (ref $default eq "ARRAY") {
  # array ref, copy array
  $self->{$attribute}
      = [ @{ $init_defaults{$class}->{$attribute} } ];

     } else {
  # something else, an object or a scalar
  $self->{$attribute}
      = $init_defaults{$class}->{$attribute};
     }
 }
    }
    return $self;
}

=item $instance->set(attribute => $value, ...)

Sets the attributes of the given instance to the given values.  croaks
if there is a problem with the values.

=cut

sub set($@) {
    my ($self, @values) = (@_);

    # yes, this is a lot to do.  yes, it's slow.  But I'm fairly
    # certain that this could be handled efficiently if it were to be
    # moved inside the Perl interpreter or an XS module
    $self->isa("Class::Tangram") or croak "type mismatch";
    my $class = ref $self;
    exists $check{$class} or import_schema($class);

    while (my ($name, $value) = splice @values, 0, 2) {
 croak "attempt to set an illegal field $name in a $class"
     if (!defined $check{$class}->{$name});

 #local $@;

 # these handlers die on failure
 eval { $check{$class}->{$name}->(\$value) };
 $@ && croak ("value failed type check - ${class}->{$name}, "
       ."\"$value\" ($@)");

 #should be ok now
 $self->{$name} = $value;
    }
}

=item $instance->get($attribute)

Gets the value of $attribute.  If the attribute in question is a set,
and this method is called in list context, then it returns the MEMBERS
of the set (if called in scalar context, it returns the Set::Object
container).

=cut

sub get($$) {
    my ($self, $field) = (@_);
    $self->isa("Class::Tangram") or croak "type mismatch";
    my $class = ref $self;
    exists $check{$class} or import_schema($class);
    croak "attempt to read an illegal field $field in a $class"
 if (!defined $check{$class}->{$field});

    if ($types{$class}->{$field} =~ m/^i?set$/o) {
 if (!defined $self->{$field}) {
     $self->{$field} = Set::Object->new();
 }
 if (wantarray) {
     return $self->{$field}->members;
 }
    }

    return $self->{$field};
}

=item $instance->attribute($value)

If $value is not given, then
this is equivalent to $instance->get("attribute")

If $value is given, then this is equivalent to
$instance->set("attribute", $value).  This usage issues a warning; you
should change your code to use the set_attribute syntax for better
readability.

=item $instance->get_attribute

=item $instance->set_attribute($value)

Equivalent to $instance->get("attribute") and $instance->set(attribute
=> $value), respectively.

=item $instance->attribute_includes(@objects)

=item $instance->attribute_insert(@objects)

=item $instance->attribute_size

=item $instance->attribute_clear

=item $instance->attribute_remove(@objects)

Equivalent to calling $instance->attribute->includes(@objects), etc.
This only works if the attribute in question is a Set::Object.

=cut

sub AUTOLOAD ($;$) {
    my ($self, $value) = (@_);
    $self->isa("Class::Tangram") or croak "type mismatch";

    my $class = ref $self;
    $AUTOLOAD =~ s/.*://;
    if ($AUTOLOAD =~ m/^(set_|get_)?([^:]+)$/
 and defined $types{$class}->{$2}) {

 # perl sucks at this type of test
 if ((defined $1 and $1 eq "set_")
     or (!defined $1 and defined $value)) {

     if ($^W && !defined $1) {
  cluck("The OO police say change your call to "
        ."\$obj->set_$2");
     }
     return set($self, $2, $value);
 } else {
     return get($self, $2);
 }
    } elsif (my ($attr, $method) =
      ($AUTOLOAD =~ m/^(.*)_(includes|insert|
        size|clear|remove)$/x)
      and $types{$class}->{$1} =~ m/^i?set$/) {
 return get($self, $attr)->$method(@_[1..$#_]);
    } else {
 croak("unknown method/attribute ${class}->$AUTOLOAD called");
    }
}

=item $instance->getset($attribute, $value)

If you're replacing the AUTOLOAD function in your Class::Tangram
derived class, but would still like to use the behaviour for one or
two fields, then you can define functions for them to fall through to
the Class::Tangram method, like so:

 sub attribute { $_[0]->SUPER::getset("attribute", $_[1]) }

=cut

sub getset($$;$) {
    my ($self, $attr, $value) = (@_);
    $self->isa("Class::Tangram") or croak "type mismatch";

    if (defined $value) {
 return set($self, $attr, $value);
    } else {
 return get($self, $attr);
    }

}

=item check_X (\$value)

This series of functions checks that $value is of the type X, and
within applicable bounds.  If there is a problem, then it will croak()
the error.  These functions are not called from the code, but by the
set() method on a particular attribute.

Available functions are:

  check_string - checks that the supplied value is less
                 than 255 characters long.

=cut

sub check_string {
    croak "string ${$_[0]} too long"
 if (length ${$_[0]} > 255);
}

=pod

  check_int    - checks that the value is a (possibly
                 signed) integer

=cut

my $int_re = qr/^-?\d+$/;
sub check_int {
    croak "not an int"
 if (${$_[0]} !~ m/$int_re/ms);
}

=pod

  check_real   - checks that the value is a real number
                 (m/^\d*(\.\d*)?(e\d*)?$/)

=cut

my $real_re = qr/^-?\d*(\.\d*)?(e-?\d*)?$/;
sub check_real {
    croak "not a real"
 if (${$_[0]} !~ m/$real_re/ms);
}

=pod

  check_obj    - checks that the supplied variable is a
                 reference to a blessed object

=cut

# this pattern matches a regular reference
my $obj_re = qr/^(?:HASH|ARRAY|SCALAR)?$/;
sub check_obj {
    croak "not an object reference"
 if ((ref ${$_[0]}) =~ m/$obj_re/);
}

=pod

  check_flat_array
               - checks that $value is a ref ARRAY

=cut

sub check_flat_array {
    croak "not a flat array"
 if (ref ${$_[0]} ne "ARRAY");
}

=pod

  check_array  - checks that $value is a ref ARRAY, and that
                 each element in the array is a reference to
                 a blessed object.

=cut

sub check_array {
    croak "array attribute not passed an array ref"
 if (ref ${$_[0]} ne "ARRAY");
    for my $a (@{${$_[0]}}) {
 croak "member in array not an object reference"
     if ((ref $a) =~ m/$obj_re/);
    }
}

=pod

  check_set    - checks that $value->isa("Set::Object")

=cut

sub check_set {
    croak "set type not passed a Set::Object"
 unless (ref ${$_[0]} and ${$_[0]}->isa("Set::Object"));
}

=pod

  check_rawdatetime 
               - checks that $value is of the form
                 YYYY-MM-DD HH:MM:SS

=cut

# YYYY-MM-DD HH:MM:SS
my $rawdatetime_re = qr/^\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}:\d{2}$/;
sub check_rawdatetime {
    croak "invalid SQL rawdatetime"
 unless (${$_[0]} =~ m/$rawdatetime_re/);
}

=pod

  check_time
              - checks that $value is of the form
                HH:MM(:SS)?

=cut

my $time_re = qr/^\d{1,2}:\d{2}(?::\d{2})?$/;
sub check_time {
    croak "invalid SQL time"
 unless (${$_[0]} =~ m/$time_re/);
}

=pod

  check_timestamp
              - checks that $value is of the form
                (YYYY-MM-DD )?HH:MM(:SS)?

=cut

my $timestamp_re = qr/^(?:\d{4}-\d{2}-\d{2}\s+)?\d{1,2}:\d{2}(?::\d{2}
+)?$/;
sub check_timestamp {
    croak "invalid SQL timestamp"
 unless (${$_[0]} =~ m/$timestamp_re/);
}

=pod

  check_flat_hash
               - checks that $value is a ref HASH

=cut

sub check_flat_hash {
    croak "not a hash"
 unless (ref ${$_[0]} eq "HASH");
    while (my ($k, $v) = each %${$_[0]}) {
 croak "hash not flat"
     if (ref $k or ref $v);
    }
}

=pod

  check_hash   - checks that $value is a ref HASH, that
                 every key in the hash is a scalar, and that
                 every value is a blessed object.

=cut

sub check_hash {
    croak "not a hash"
 unless (ref ${$_[0]} eq "HASH");
    while (my ($k, $v) = each %${$_[0]}) {
 croak "hash key not flat"
     if (ref $k);
 croak "hash value not an object"
     if (ref $v !~ m/$obj_re/);
    }
}

=pod

  check_nothing - checks whether Australians like sport

=cut

sub check_nothing { }

=item destroy_X ($instance, $attr)

Similar story with the check_X series of functions, these are called
during object destruction on every attribute that has a reference that
might need breaking.  Note: B<these functions all assume that
attributes belonging to an object that is being destroyed may be
destroyed also>.  In other words, do not allow distinct objects to
share Set::Object containers or hash references in their attributes,
otherwise when one gets destroyed the others will lose their data.

Available functions are:

  destroy_array - empties an array

=cut

sub destroy_array {
    my ($self, $attr) = (@_);
    my $t = tied $self->{$attr};
    @{$self->{$attr}} = () unless ($t =~ m,Tangram::CollOnDemand,);
    delete $self->{$attr};
}

=pod

  destroy_set   - calls Set::Object::clear to clear the set

=cut

sub destroy_set {
    my ($self, $attr) = (@_);

    # warnings suck sometimes
    local $^W = 0;

    my $t = tied $self->{$attr};
    return if ($t =~ m,Tangram::CollOnDemand,);
    if (ref $self->{$attr} eq "Set::Object") {
 $self->{$attr}->clear;
    }
    delete $self->{$attr};
}

=pod

  destroy_hash  - empties a hash

=cut

sub destroy_hash {
    my ($self, $attr) = (@_);
    my $t = tied $self->{$attr};
    %{$self->{$attr}} = () unless ($t =~ m,Tangram::CollOnDemand,);
    delete $self->{$attr};
}

=pod

  destroy_ref   - destroys a reference.  Contains a hack for
                  Tangram so that if this ref is not loaded,
                  it will not be autoloaded when it is
                  attempted to be accessed.

=cut

sub destroy_ref {
    my ($self, $attr) = (@_);

    # warnings suck sometimes
    local $^W = 0;

    # the only reason I bother with all of this is that I experienced
    # Perl did not always call an object's destructor if you just used
    # delete.
    my $t = tied $self->{$attr};
    if (defined $t and $t =~ m/OnDemand/) {
 delete $self->{$attr};
    } else {
 my $ref = delete $self->{$attr};
    }
}

=item parse_X ($attribute, { schema option })

Parses the schema option field, and returns one or two closures that
act as a check_X and a destroy_X function for the attribute.

This is currently a very ugly hack, parsing the SQL type definition of
an object.  But it was bloody handy in my case for hacking this in
quickly.  This is unmanagably unportable across databases.  This
should be replaced by primitives that go the other way, building the
SQL type definition from a more abstract definition of the type.

Available functions:

  parse_string  - parses SQL types of:

=cut

sub parse_string {

    my ($attribute, $option) = (@_);

    # simple case; return the check_string function.  We don't
    # need a destructor for a string so don't return one.
    if (!$option->{sql}) {
 return \&check_string;
    }

=pod

      CHAR(N), VARCHAR(N)
          closure checks length of string is less
          than N characters

=cut

    if ($option->{sql} =~ m/^\s*(?:var)?char\s*\(\s*(\d+)\s*\)/ix) {
 my $max_length = $1;
 return sub {
     die "string too long for $attribute"
  if (length ${$_[0]} > $max_length);
 };

=pod

      TINYBLOB, BLOB, LONGBLOB
      TINYTEXT, TEXT, LONGTEXT
          checks max. length of string to be 255,
          65535 or 16777215 chars respectively

=cut

    } elsif ($option->{sql} =~ m/^\s*(tiny|long|medium)?
     (blob|text)/ix) {
 my $max_length = ($1 ? ($1 eq "tiny"?255:2**24 - 1)
     : 2**16 - 1);
 return sub {
     die "string too long for $attribute"
  if (length ${$_[0]} > $max_length);
 };

=pod

      SET("members", "of", "set")
          checks that the value passed is valid as
          a SQL set type, and that all of the 
          passed values are allowed to be a member
          of that set

=cut

    } elsif ($option->{sql} =~
      m/^\s*set\s*\(
        (\"[^\"]+\" (?:\s*,\s*\"[^\"]+\")* \s* )
        \)\s*$/xi) {
 my $members = $1;
 my ($member, %members);
 while (($member, $members) =
        ($members =~ m/^[,\s]*\"([^\"]+)\"(.*)$/)) {
     $members{lc($member)} = 1;
 }
 return sub {
     for my $x (split /,/, ${$_[0]}) {
  croak ("SQL set badly formed or invalid member $x "
         ." (SET" . join(",", keys %members). ")")
      if (not exists $members{lc($x)});
     }
 };

=pod

      ENUM("possible", "values")
          checks that the value passed is one of
          the allowed values.

=cut

    } elsif ($option->{sql} =~
      m/^\s*enum\s*\(
        (\"[^\"]+\" (?:\s*,\s*\"[^\"]+\")* \s* )
        \)\s*/xi) {
 my $values = $1;
 my ($value, %values);
 while (($value, $values) =
        ($values =~ m/^[,\s]*\"([^\"]+)\"(.*)$/)) {
     $values{lc($value)} = 1;
 }
 return sub {
     croak ("invalid enum value ${$_[0]} must be ("
     . join(",", keys %values). ")")
  if (not exists $values{lc(${$_[0]})});
 }

    } else {
 die ("Please build support for your string SQL type in "
      ."Class::Tangram (".$option->{sql}.")");
    }
}

# Here is where I map Tangram::Type types to functions.
# Format:
#  type => {
#      check => \&check_X
#      parse => \&parse_type
#      destroy => \&destroy_X
#  }
#
my %defaults =
    (
     int         => { check => \&check_int },
     real        => { check => \&check_real },
     string      => {             parse => \&parse_string },
     ref         => { check => \&check_obj,     destroy => \&destroy_r
+ef },
     array       => { check => \&check_array,   destroy => \&destroy_a
+rray },
     iarray      => { check => \&check_array,   destroy => \&destroy_a
+rray },
     flat_array  => { check => \&check_flat_array },
     set         => { check => \&check_set,     destroy => \&destroy_s
+et },
     iset        => { check => \&check_set,     destroy => \&destroy_s
+et },
     rawdatetime => { check => \&check_rawdatetime },
     time        => { check => \&check_time },
     timestamp   => { check => \&check_timestamp },
     flat_hash   => { check => \&check_flat_hash },
     hash        => { check => \&check_hash,    destroy => \&destroy_h
+ash },
     perl_dump   => { check => \&check_nothing }
    );

=item import_schema($class)

Parses a tangram object schema, in "\$${class}::schema" to the
internal representation used to check types values by set().  Called
automatically on the first get(), set(), or new() for an object of a
given class.

This function updates Tangram schema option hashes, with the following
keys:

  check_func   - supply/override the check_X function for
                 this attribute.

  destroy_func - supply/override the destroy_X function for
                 this attribute

See the SYNOPSIS section for an example of supplying a check_func in
an object schema.

=cut

sub import_schema($) {
    my ($class) = (@_);

    eval {
 my ($fields, $bases, $abstract);
 {
     no strict 'refs';
     $fields = ${"${class}::schema"}->{fields};
     $bases = ${"${class}::schema"}->{bases};
     $abstract = ${"${class}::schema"}->{abstract};
 }

 my $check_class = { };
 my $cleaners_class = { };
 my $init_defaults_class = { };
 my $types_class = { };

 # if this is an abstract type, do not allow it to be
 # instantiated
 if ($abstract) {
     $abstract{$class} = 1;
 }

 # If there are any base classes, import them first so that the
 # check, cleaners and init_defaults can be inherited
 if (defined $bases) {
     (ref $bases eq "ARRAY")
  or die "bases not an array ref for $class";

     # Note that the order of your bases is significant, that
     # is if you are using multiple iheritance then the later
     # classes override the earlier ones.
     #for my $super ( Class::ISA::super_path($class) ) {
     for my $super ( @$bases ) {
  import_schema $super unless (exists $check{$super});

  # copy each of the per-class configuration hashes to
  # this class as defaults.
  my ($k, $v);
  $types_class->{$k} = $v
      while (($k, $v) = each %{ $types{$super} } );
  $check_class->{$k} = $v
      while (($k, $v) = each %{ $check{$super} } );
  $cleaners_class->{$k} = $v
      while (($k, $v) = each %{ $cleaners{$super} } );
  $init_defaults_class->{$k} = $v
      while (($k, $v) = each %{ $init_defaults{$super} } );
     }
 }

 # iterate over each of the *types* of fields (string, int, ref, etc.)
 while (my ($type, $v) = each %$fields) {
     if (ref $v eq "ARRAY") {
  $v = { map { $_, undef } @$v };
     }
     my $def = $defaults{$type};

     # iterate each of the *attributes* of a particular type
     while (my ($attribute, $option) = each %$v) {
  $types_class->{$attribute} = $type;

  # ----- check_X functions ----
  if (ref $option eq "HASH" and $option->{check_func}) {
      # user-supplied check_X function
      $check_class->{$attribute} =
   $option->{check_func};

  } else {
      if (not defined $def) {
   die "No default check function for type $type";
      }

      # check for a type-specific option hash parser
      if ($def->{parse}) {
   my ($check, $destroy) =
       $def->{parse}->($attribute, $option);

   $check_class->{$attribute} = $check;
   $cleaners_class->{$attribute} = $destroy
       if (defined $destroy);

      } else {
   # use the default for this type
   $check_class->{$attribute} = $def->{check};
      }
  }

  # ----- destroy_X functions
  if (ref $option eq "HASH" and $option->{destroy_func}) {
      # user-supplied destroy_X function
      $cleaners_class->{$attribute} =
   $option->{destroy_func};
  } else {
      if ($def->{destroy}) {
   # use the default for this type
   $cleaners_class->{$attribute} =
       $def->{destroy};
      }
  }

  # ----- init_default functions
  # create empty Set::Object containers as necessary
  if ($type =~ m/^i?set$/) {
      $init_defaults_class->{$attribute} =
   sub { Set::Object->new() };
  }
  if (ref $option eq "HASH" and $option->{init_default}) {
      $init_defaults_class->{$attribute} =
   $option->{init_default};
  }
     }
 }
 $types{$class} = $types_class;
 $check{$class} = $check_class;
 $cleaners{$class} = $cleaners_class;
 $init_defaults{$class} = $init_defaults_class;
    };

    $@ && die "$@ while trying to import schema for $class";
}


=item $instance->quickdump

Quickly show the blessed hash of an object, without descending into
it.  Primarily useful when you have a large interconnected graph of
objects so don't want to use the B<x> command within the debugger.
It also doesn't have the side effect of auto-vivifying members.

This function returns a string, suitable for print()ing.  It does not
currently escape unprintable characters.

=cut

sub quickdump($) {
    my ($self) = (@_);

    my $r = "REF ". (ref $self). "\n";
    for my $k (sort keys %$self) {
 $r .= ("   $k => "
        . (
    tied $self->{$k}
    || ( ref $self->{$k}
         ? $self->{$k}
         : "'".$self->{$k}."'" )
   )
        . "\n");
    }
    return $r;
}


=item $instance->DESTROY

This function ensures that all of your attributes have their
destructors called.  It calls the destroy_X function for attributes
that have it defined, if that attribute exists in the instance that we
are destroying.  It calls the destroy_X functions as destroy_X($self,
$k)

=cut

sub DESTROY($) {
    my ($self) = (@_);

    my $class = ref $self;

    # if no cleaners are known for this class, it hasn't been imported
    # yet.  Don't call import_schema, that would be a bad idea in a
    # destructor.
    exists $cleaners{$class} or return;

    # for every attribute that is defined, and has a cleaner function,
    # call the cleaner function.
    for my $k (keys %$self) {
 if (defined $cleaners{$class}->{$k} and exists $self->{$k}) {
     $cleaners{$class}->{$k}->($self, $k);
 }
    }
    $self->{_DESTROYED} = 1;
}

=item $instance->clear_refs

This clears all references from this object, ie exactly what DESTROY
normally does, but calling an object's destructor method directly is
bad form.  Also, this function has no qualms with loading the class'
schema with import_schema() as needed.

This is useful for breaking circular references, if you know you are
no longer going to be using an object then you can call this method,
which in many cases will end up cleaning up most of the objects you
want to get rid of.

However, it still won't do anything about Tangram's internal reference
to the object, which must still be explicitly unlinked with the
Tangram::Storage->unload method.

=cut

sub clear_refs($) {
    my ($self) = (@_);

    my $class = ref $self;

    exists $cleaners{$class} or import_schema($class);

    # break all ref's, sets, arrays
    for my $k (keys %$self) {
 if (defined $cleaners{$class}->{$k} and exists $self->{$k}) {
     $cleaners{$class}->{$k}->($self, $k);
 }
    }
    $self->{_NOREFS} = 1;
}


=back

=head1 SEE ALSO

L<Tangram::Schema>

B<A guided tour of Tangram, by Sound Object Logic.>

 http://www.soundobjectlogic.com/tangram/guided_tour/fs.html

=head1 BUGS/TODO

More datetime types.  I originally avoided the DMDateTime type because
Date::Manip is self-admittedly the most bloated module on CPAN, and I
didn't want to be seen encouraging it.  Then I found out about
autosplit :-}.

More AUTOLOAD methods, in particular for container types such as
array, hash, etc.

This documentation should be easy enough for a fool to understand.

There should be more functions for breaking loops; in particular, a
standard function called drop_refs($obj), which replaces references to
$obj with the appropriate Tangram::RefOnDemand so that an object can
be unloaded via Tangram::Storage->unload() and actually have a hope of
being reclaimed.  Another function that would be handy would be a
deep "mark" operation for mark & sweep garbage collection.

=head1 AUTHOR

Sam Vilain, <sam@vilain.net>

=cut

69;