The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#-----------------------------------------------------------------
# Class::Data::TIN
#-----------------------------------------------------------------
# Copyright Thomas Klausner / ZSI 2001, 2002
# You may use and distribute this module according to the same terms
# that Perl is distributed under.
#
# Thomas Klausner domm@zsi.at http://domm.zsi.at
#
# $Author: domm $
# $Date: 2002/01/29 22:03:35 $
# $Revision: 1.9 $
#-----------------------------------------------------------------
# Class::Data::TIN - T_ranslucent I_nheritable N_onpolluting
#-----------------------------------------------------------------
package Class::Data::TIN;

use 5.006;
use strict;
use warnings;

require Exporter;

use Carp;
use Data::Dumper;

our @ISA = qw(Exporter);
our @EXPORT_OK = qw(get get_classdata set set_classdata append append_classdata);
our $VERSION = '0.02';

# not exported, has to be called explicitly with Class::Data::TIN->new()
sub new {
   shift;  # remove own ClassName 'Class::Data::TIN'
   my $org_package=shift;  # get name of package to store vars in
   my $data;

   if (@_ == 1) {  # one param passed
     my $param=shift;
     if (ref($param) eq 'HASH') {  # is it a HASH ref ?
       $data=$param;
     } elsif (-e $param) {  # or is it a file ?
       $data=do $param; # TODO some error checking
     } else {  # then something is wrong
       croak("param is neither HASH REF nor file ...");
     }
   } else {  # more params passed, treat as HASH
     $data={@_};
   }

   croak("data structure must be a hashref") if ($data && ref($data) ne "HASH");

   my $tin_package=__PACKAGE__."::".$org_package;

   ### put data into TIN
   # start eval-string
   my $install="package $tin_package;";

   # add ISA's
   my @isa=eval "@".$org_package."::ISA";
   my @isa_tin;
   foreach (@isa) {
      push(@isa_tin,__PACKAGE__."::".$_);
   }
   $install.='our @ISA=(qw ('."@isa_tin".'));' if @isa_tin;

   $install.='our $_tin;';
   $install.='$_tin=$data;' if $data;
   eval $install;
   croak $@ if $@;

   # generate accessor methods in $tin_package
   for my $key (keys %$data) {
      _make_accessor($tin_package,$key);
   }

   # return empty fake pseudo obj, to make calling get/set/append easier
   # this is /not/ blessed, in fact, its just an alias to __PACKAGE__
   return $org_package;
}

# not exported
sub _make_accessor {
   my ($pkg,$key)=@_;

   # to enable black symbol table magic
   no strict "refs";

   my $accessor=$pkg."::".$key;
   return if *$accessor{CODE}; # there is allready an accessor

   my $r_tin=eval '$'."$pkg".'::_tin';

   *$accessor = sub {
      my $self=shift;
      $r_tin->{$key} = shift if @_;
      return $r_tin->{$key};
   }
}

# exported, has to be called on object or class, NOT on Class::Data::TIN
sub get_classdata {
   my ($self,$key)=@_;

   my $package=ref($self) || $self;
   my $tin=__PACKAGE__."::".$package;
   if ($tin->can($key)) {
      return $tin->$key();
   }
   return;
}

# alias
*get=*get_classdata;

# exported, has to be called on object or class, NOT on Class::Data::TIN
sub set_classdata {
   my $self=shift;
   my $package=ref($self) || $self;

   croak "object not allowed to modify class data" if (ref($self));

   my $tin=__PACKAGE__."::".$package;
   my ($key,$val)=@_;

   # copy on write:
   _make_accessor($tin,$key);

   return $tin->$key($val);
}

# alias
*set=*set_classdata;


# exported, has to be called on object or class, NOT on Class::Data::TIN
sub append_classdata {
   my $self=shift;
   my $package=ref($self) || $self;

   croak "object not allowed to modify class data" if (ref($self));

   my $tin=__PACKAGE__."::".$package;

   my $key=shift;

   # if this key is not here, there's no use appending, so use set()
   unless ($tin->can($key)) {
      return set($self,$key,@_);
   }

   # get old value
   my $val=$tin->$key;

   if (!ref($val)) {
      $val.=shift;
   } elsif (ref($val) eq "HASH") {
      eval Data::Dumper->Dump([$val],['val']);
      $val={%$val,@_};
   } elsif (ref($val) eq "ARRAY") {
      eval Data::Dumper->Dump([$val],['val']);
      push(@$val,@_);
   } elsif (ref($val) eq "CODE") {
      croak("cannot modify code ref");
   }

   # copy on write:
   _make_accessor($tin,$key);

   $tin->$key($val);
}

# alias
*append=*append_classdata;




1;
__END__

=head1 NAME

Class::Data::TIN - Translucent Inheritable Nonpolluting Class Data

=head1 SYNOPSIS

  use Class::Data::TIN qw(get_classdata set_classdata append_classdata);
  # or
  # use Class::Data::TIN qw(get set append);
  # but I prefer the first option, because of a less likly
  # namespace clashing

  # generate class data in your PACAKGE
  package My::Stuff;
  use Class::Data::TIN;

  our @ISA=(qw (Our::Stuff));

  my $tin=Class::Data::TIN->new(__PACKAGE__,
		      {
		       string=>"a string",
		       string2=>"another string",
		       array=>['foo','bar'],
		       hash=>{
			      foo=>'bar',
			      jaja=>'neinein',
			     },
		       code=>sub{return "bla"}
		      });


   print $tin->get_classdata('string');
   # or
   # print My::Stuff->get_classdata('string');
   # prints "a string"

   print $tin->get_classdata('newstring');
   # prints nothing, as newstring is not defined

   $tin->set_classdata('newstring','now I am here');
   print $self->get_classdata('newstring');
   # prints "now I am here"

   $tin->append_classdata('newstring',', or am I?');
   print $tin->get_classdata('newstring');
   # prints "now I am here, or am I?"


=head1 DESCRIPTION

Class::Data::TIN implements Translucent Inheritable Nonpolluting Class Data.

The thing I don't like with Class::Data::Inheritable or the implementations suggested in perltootc is that you end up with lots of accessor routines in your namespace.

Class::Data::TIN works around this "problem" by storing the Class Data in its own namespace (mirroring the namespace and @ISA hierarchies of the modules using it) and supplying the using packages with (at this time) three meta-accessors called C<get_classdata> (or just C<get>), C<set_classdata> (C<set>) and C<append_classdata> (C<append>). It achieves this with some black magic (namespace munging & evaling).

=head2 new ($package,$datastruct)

new takes the package name of the package needing ClassData, and a data structrure passed as a hashref, a hash or a path to a file returning a hashref if called with C<do>. It then installs a new package by appending "Class::Data::TIN::" to C<$package>, copying C<$package>s @ISA to the new package and saving C<$data> in the var C<$_tin>

Then for every key in C<$data> accessor methods are generated in the new namespace.

new() returns the name of the original package as a string (B<not> as a blessed reference!), so that the calling package may use the return value to modifiy the Class Data. This is done because I have to discern between B<object> invocation and B<class> invocation of the Class Data manipulating methods. Ideally, if an object modifies the Class Data, this changes are only visible to this object. B<NOTE:> But this is not implemented yet. You can only modify Class Data when calling directly with ClassName->set, or with the return value of new() (which is, for example, nothing but the string "ClassName").

B<Example:>

  package My::Stuff;
  use Class::Data::TIN;
  our @ISA=('Other::Stuff');
  my $tin=Class::Data::TIN->new(__PACKAGE__,
		      {
		       string=>"a string",
                      });

In new(), the following code is eval'ed:

  package Class::Data::TIN::My::Stuff;
  our @ISA=(qw (Class::Data::TIN::Other::Stuff));
  our $_tin;
  $_tin=$data;

and accesors are generated, that look sort of like this:

  sub string {
      my $self=shift;
      $_tin->{'string'} = shift if @_;
      return $_tin->{'string'};
   }

The point is that C<string> and all other accessors are generate in a Namespace in Class::Data::TIN::My::Stuff, and B<not> in My::Stuff, thus keeping My::Stuff neat and tidy.

look at the test script (test.pl) for a more complex example.

=head2 get_classdata ($key)

returns the value of the given key.

=head2 set_classdata ($key,$val)

set the key to the given value.

Translucency is implemented here by making a new accessor in the pseudo-class. (copy on write)

=head2 append_classdata ($key,$value [,$value2,..])

appends some values to a key. sets a new key if the key wasn't there. Does copy on write. You can also use append to override the value of a HASH in a parent class (simply append the value you'd like to override to the HASH)

=head2 _make_accessor

internal method, don't call it!

_make_accessor checks if there allready exists an accessor for the given key. If not, it dumps one into the appropriate symbol table.

=head2 TODO

A Lot:

=over 4

=item * implement object translucency

=item * test different kinds to call new

=item * let user decide wheter object is allowed to modify class data

=back 4

=head2 EXPORT

None by default.

get get_classdata set set_classdata append append_classdata, if you ask for it

=head1 SEE ALSO

perltootc, Class::Data::Inheritable

=head1 AUTHOR

Thomas Klausner, domm@zsi.at, http://domm.zsi.at

=head1 COPYRIGHT

Class::Data::TIN is Copyright (c) 2002 Thomas Klausner, ZSI.
All rights reserved.

You may use and distribute this module according to the same terms
that Perl is distributed under

=cut