Skip Menu |

This queue is for tickets about the Class-Std CPAN distribution.

Report information
The Basics
Id: 14231
Status: resolved
Priority: 0/
Queue: Class-Std

People
Owner: Nobody in particular
Requestors: luke [...] daeron.com
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: (no value)
Fixed in: (no value)



Date: Thu, 18 Aug 2005 21:26:54 -0400
From: Luke Meyer <luke [...] daeron.com>
To: bug-Class-Std [...] rt.cpan.org
CC: damian [...] conway.org
Subject: patch to add serialization to Class::Std
Attached is a patch that adds methods to Class::Std for Storable to freeze and thaw an inside-out class, as well as a few test cases and the full class code. Discussion: This also provides a side door for users to create inside-out objects without going through your constructors and accessors, which is unfortunate for those who really value encapsulation. However, it's still just about impossible to fool with an object once it's been created. Nevertheless, it might be a better approach to do one of: 1) Make serializability optional somehow. 2) Make the methods check that caller is 'Storable' (slim security, though) 3) Move serializing to a separate class Class::Std::Storable -- and then anything you want to be Storable has to be of this type, including base classes. Sucks because it must either be forked code or a "subclass" that can parse and keep a copy of the attributes for the serialization. Also, the serialization of course maintains the same caveat as for any other object: if it's not an object attribute, it won't get stored with the object, and you can't serialize an attribute that's a closure or otherwise not serializable.
--- /Library/Perl/5.8.6/Class/Std.pm 2005-08-07 00:19:22.000000000 -0400 +++ Class/Std.pm 2005-08-18 20:11:41.000000000 -0400 @@ -1,6 +1,6 @@ package Class::Std; -use version; $VERSION = qv('0.0.4'); +use version; $VERSION = qv('0.0.5'); use strict; use warnings; use Carp; @@ -19,6 +19,8 @@ MODIFY_CODE_ATTRIBUTES AUTOLOAD _DUMP + STORABLE_freeze + STORABLE_thaw ); sub import { @@ -130,6 +132,49 @@ return $dump; } +sub STORABLE_freeze { + my($self, $cloning) = @_; + #we'll ignore $cloning; it's not going to make sense to + #serialize and deserialize without "cloning". + my $id = ID($self); + require Storable; + my $serialized = Storable::freeze( \(my $anon_scalar) ); + + my %freeze; #stolen from _DUMP above + for my $package (keys %attribute) { + my $attr_list_ref = $attribute{$package}; + for my $attr_ref ( @{$attr_list_ref} ) { + next if !exists $attr_ref->{ref}{$id}; + $freeze{$package}{$attr_ref->{name}} = $attr_ref->{ref}{$id}; + } + } + + return ($serialized, \%freeze); +} + +sub STORABLE_thaw { + my($self, $cloning, $serialized, $frozen_attr_ref) = @_; + #we'll ignore $cloning; it's not going to make sense to + #serialize and deserialize without "cloning". + #we can ignore $serialized too, as we know it's an anon_scalar. + my $id = ID($self); + while( my ($package, $pkg_attr_ref) = each %$frozen_attr_ref ) { + my $attr_list_ref = $attribute{$package}; + for my $attr_ref ( @{$attr_list_ref} ) { + next if !exists $pkg_attr_ref->{ $attr_ref->{name} }; + $attr_ref->{ref}{$id} + = delete $pkg_attr_ref->{ $attr_ref->{name} }; + } + if( my @extra_keys = keys %$pkg_attr_ref ) { + #this is probably serious enough to throw an exception. + #however, TODO: it would be nice if the class could somehow + #indicate to ignore this problem. + croak "unknown attribute(s) seen during deserialization" + ." for package $package: " . join(q{, }, @extra_keys); + } + } +} + my $STD_OVERLOADER = q{ package %%s; use overload (
use strict; package TestClass; use Class::Std; { my %name_of :ATTR( :get<name> :set<name> ); my %flavor_of :ATTR( :get<flavor> :set<flavor> ); } package LinkedList; use Class::Std; { my %info_of :ATTR( :get<info> :set<info> ); my %next_node_for :ATTR( :get<next_node> :set<next_node> ); } package TestMISubClass; use Class::Std; use base qw( TestClass LinkedList ); { my %ref_copy_for :ATTR( :get<ref_copy> ); sub set_next_node { my $self = shift; my $id = ident $self; die "no param provided" unless @_; my $next_node = shift; $ref_copy_for{$id} = $next_node; $self->SUPER::set_next_node($next_node); return; } } package main; use Test::More tests => 24; use Class::Std; use Storable; use Carp; ########################################################## # very basic testing of a single object my $object = TestClass->new; $object->set_name("Vanilla Bean"); $object->set_flavor("vanilla"); my $clone = Storable::dclone($object); is( $clone->get_name, "Vanilla Bean", "properties successfully cloned"); is( $clone->get_flavor, "vanilla", "properties successfully cloned"); ########################################################## # testing a nested structure my $first_node = LinkedList->new; $first_node->set_info(1); for my $i (2..10) { my $next_node = LinkedList->new; $next_node->set_info($i); $next_node->set_next_node($first_node); $first_node = $next_node; } my $id = ident($first_node); $first_node = Storable::dclone($first_node); isnt($id, ident($first_node), "should in fact be a different object"); for my $i (reverse 1..10) { is($first_node->get_info, $i, "values in the nodes all match"); $first_node = $first_node->get_next_node; } ########################################################## # testing MI and structural integrity my @flavors = qw( vanilla chocolate strawberry mango peach grape ); my $obj; for my $flavor ( @flavors ) { my $next = TestMISubClass->new; $next->set_flavor($flavor); $next->set_info($flavor); $next->set_next_node($obj); $obj = $next; } $clone = Storable::dclone($obj); for my $flavor ( reverse @flavors ) { is($flavor, $clone->get_flavor, "flavor cloned the same"); my $next = $clone->get_next_node; my $copy = $clone->get_ref_copy; last unless $next; is(ident($next), ident($copy), "clone of same object should be the same"); $clone = $next; }

Message body is not shown because it is too large.

Show quoted text
> This also provides a side door for users to create inside-out objects > without going through your constructors and accessors, which is > unfortunate for those who really value encapsulation. However, it's > still just about impossible to fool with an object once it's been > created.
Silly me; of course this also opens the other side door to get the object attributes in a handy little hash as well, and combined with the first side door, pretty much lets you do whatever you want with the attributes directly, if you're so inclined. I can't at the moment think of a good way to put the shotgun (for keeping intruders out) back in the hands of the class, other than maybe marking individual attributes as serializable or not. Perhaps the cap-gun of "check caller()" would be enough for some people. In order to get serialization, you're going to have to let an external class pry somehow.
Date: Fri, 19 Aug 2005 14:17:27 -0400
From: Luke Meyer <luke [...] daeron.com>
To: bug-Class-Std [...] rt.cpan.org
Subject: Re: [cpan #14231] AutoReply: patch to add serialization to Class::Std
RT-Send-Cc:
So I went ahead and created a sister class Class::Std::Storable that allows the class to be serialized. It doesn't suck as much as I thought. Still, thinking about it, it wouldn't be hard to put this in Class::Std as an import option instead. Also, there doesn't seem to be any way to restrict the serialization interface to Storable. The rest of the world can also poke around in the class. Attached are the class and some test cases.
package Class::Std::Storable; use version; $VERSION = qv('0.0.1'); use strict; use warnings; use Class::Std; #get subs from parent to export use Carp; use Scalar::Util; use overload; *ID = \&Scalar::Util::refaddr; my (%attribute); my @exported_subs = qw( new ident DESTROY MODIFY_HASH_ATTRIBUTES MODIFY_CODE_ATTRIBUTES AUTOLOAD _DUMP STORABLE_freeze STORABLE_thaw ); sub import { no strict 'refs'; for my $sub ( @exported_subs ) { *{ caller() . '::' . $sub } = \&{$sub}; } } *_extract_default = Class::Std::_extractor_for_pair_named('default'); *_extract_init_arg = Class::Std::_extractor_for_pair_named('init_arg'); *_extract_get = Class::Std::_extractor_for_pair_named('get'); *_extract_set = Class::Std::_extractor_for_pair_named('set'); #NOTE: this subroutine should override the one that's imported #by the "use Class::Std" above. { my $old_sub = \&MODIFY_HASH_ATTRIBUTES; my %positional_arg_of; my $new_sub = sub { my ($package, $referent, @attrs) = @_; my @return_attrs = $old_sub->(@_); for my $attr (@attrs) { next if $attr !~ m/\A ATTRS? \s* (?:[(] (.*) [)] )? \z/xms; my ($init_arg, $getter, $setter); #we would prefer to know the argument as the class does. if (my $config = $1) { $init_arg = _extract_init_arg($config); $getter = _extract_get($config); $setter = _extract_set($config); } #but we have a backup if no name was given. $positional_arg_of{$package} ||= "__Positional_0001"; push @{$attribute{$package}}, { ref => $referent, name => $init_arg || $getter || $setter || $positional_arg_of{$package}++, }; } return @return_attrs; }; no warnings; #or this complains about redefining sub *MODIFY_HASH_ATTRIBUTES = $new_sub; }; sub STORABLE_freeze { #croak "must be called from Storable" unless caller eq 'Storable'; #unfortunately, Storable never appears on the call stack. my($self, $cloning) = @_; #we'll ignore $cloning; it's not going to make sense to #serialize and deserialize without "cloning". my $id = ID($self); require Storable; my $serialized = Storable::freeze( \(my $anon_scalar) ); my %frozen_attr; my @package_list = ref $self; my %package_seen; PACKAGE: while( my $package = shift @package_list) { #make sure we add any base classes to the list of #packages to examine for attributes. { no strict 'refs'; for my $base_class ( @{"${package}::ISA"} ) { push @package_list, $base_class if !$package_seen{$base_class}++; } } #don't examine attributes from unrelated packages next PACKAGE unless my $attr_list_ref = $attribute{$package}; #look for any attributes of this object for this package for my $attr_ref ( @{$attr_list_ref} ) { next if !exists $attr_ref->{ref}{$id}; $frozen_attr{$package}{$attr_ref->{name}} = $attr_ref->{ref}{$id}; } } return ($serialized, \%frozen_attr ); } sub STORABLE_thaw { #croak "must be called from Storable" unless caller eq 'Storable'; #unfortunately, Storable never appears on the call stack. my($self, $cloning, $serialized, $frozen_attr_ref) = @_; #we'll ignore $cloning; it's not going to make sense to #serialize and deserialize without "cloning". #we can ignore $serialized too, as we know it's an anon_scalar. my $id = ID($self); while( my ($package, $pkg_attr_ref) = each %$frozen_attr_ref ) { my $attr_list_ref = $attribute{$package}; for my $attr_ref ( @{$attr_list_ref} ) { next if !exists $pkg_attr_ref->{ $attr_ref->{name} }; $attr_ref->{ref}{$id} = delete $pkg_attr_ref->{ $attr_ref->{name} }; } if( my @extra_keys = keys %$pkg_attr_ref ) { #this is probably serious enough to throw an exception. #however, TODO: it would be nice if the class could somehow #indicate to ignore this problem. croak "unknown attribute(s) seen during deserialization" ." for package $package: " . join(q{, }, @extra_keys); } } } 1; # Magic true value required at end of module __END__ =head1 NAME Class::Std::Storable - Support for creating serializable "inside-out" classes =head1 VERSION This document describes Class::Std::Storable version 0.0.1 =head2 SYNOPSIS Use this exactly as you would Class::Std. The only difference is that all declared attributes of this class and any Class::Std::Storable superclasses will be serialized with this object when frozen and thawed with Storable. However, in order to let Storable save attributes and construct the object, it is necessary to expose the attributes of the class to the world. Thus, any code could use the same interface that Storable does to access your class's attributes directly. It's not quite as easy as $object->{attribute} but it won't stop anyone determined to do this. As true encapsulation was one of the major features of Class::Std, this would be a good reason NOT to use this class. But this sacrifice is necessary to provide serialization with an inside-out class. =cut
use strict; package TestClass; use Class::Std::Storable; { my %name_of :ATTR( :get<name> :set<name> ); my %flavor_of :ATTR( :get<flavor> :set<flavor> ); } package LinkedList; use Class::Std::Storable; { my %info_of :ATTR( :get<info> :set<info> ); my %next_node_for :ATTR( :get<next_node> :set<next_node> ); } package TestMISubClass; use Class::Std::Storable; use base qw( TestClass LinkedList ); { my %ref_copy_for :ATTR( :get<ref_copy> ); my %unknown1 :ATTR; #for testing with no attr name given my %unknown2 :ATTR; #for testing with no attr name given sub set_next_node { my $self = shift; my $id = ident $self; die "no param provided" unless @_; my $next_node = shift; $ref_copy_for{$id} = $next_node; $self->SUPER::set_next_node($next_node); return; } sub set_unknown1 { my $id = ident shift; $unknown1{$id} = shift; } sub get_unknown1 { return $unknown1{ident shift}; } sub set_unknown2 { my $id = ident shift; $unknown2{$id} = shift; } sub get_unknown2 { return $unknown2{ident shift}; } } package main; use Test::More tests => 36; use Class::Std::Storable; use Storable; use Carp; use Data::Dumper; ########################################################## # very basic testing of a single object my $object = TestClass->new; $object->set_name("Vanilla Bean"); $object->set_flavor("vanilla"); my $clone = Storable::dclone($object); is( $clone->get_name, "Vanilla Bean", "properties successfully cloned"); is( $clone->get_flavor, "vanilla", "properties successfully cloned"); ########################################################## # testing a nested structure my $first_node = LinkedList->new; $first_node->set_info(1); for my $i (2..10) { my $next_node = LinkedList->new; $next_node->set_info($i); $next_node->set_next_node($first_node); $first_node = $next_node; } my $id = ident($first_node); $first_node = Storable::dclone($first_node); isnt($id, ident($first_node), "should in fact be a different object"); for my $i (reverse 1..10) { is($first_node->get_info, $i, "values in the nodes all match"); $first_node = $first_node->get_next_node; } ########################################################## # testing MI and structural integrity my @flavors = qw( vanilla chocolate strawberry mango peach grape ); my $obj; for my $flavor ( @flavors ) { my $next = TestMISubClass->new; $next->set_flavor($flavor); $next->set_info($flavor); $next->set_unknown1("1_$flavor"); $next->set_unknown2("2_$flavor"); $next->set_next_node($obj); $obj = $next; } $clone = Storable::freeze($obj); undef $obj; #should destroy the whole list $clone = Storable::thaw($clone); for my $flavor ( reverse @flavors ) { is($flavor, $clone->get_flavor, "flavor cloned the same"); is("1_$flavor", $clone->get_unknown1, "unknown1 cloned the same"); is("2_$flavor", $clone->get_unknown2, "unknown2 cloned the same"); my $next = $clone->get_next_node; my $copy = $clone->get_ref_copy; last unless $next; is(ident($next), ident($copy), "clone of same object should be the same"); $clone = $next; }
Date: Thu, 25 Aug 2005 07:42:20 +1000
From: Damian Conway <damian [...] conway.org>
To: bug-Class-Std [...] rt.cpan.org
Subject: Re: [cpan #14231] patch to add serialization to Class::Std
RT-Send-Cc:
Hi Luke, You wrote: Show quoted text
> So I went ahead and created a sister class Class::Std::Storable that > allows the class to be serialized. It doesn't suck as much as I > thought. Still, thinking about it, it wouldn't be hard to put this in > Class::Std as an import option instead. > > Also, there doesn't seem to be any way to restrict the serialization > interface to Storable. The rest of the world can also poke around in > the class.
For that reason, I'd much rather see Class::Std::Storable remain a separate class, with a separate set of documented caveats (and a separate maintainer ;-). I really appreciate the effort you've put in on this, Luke. I know several people have been asking about just this kind of facility, and I think your module will be seen as a very welcome addition to CPAN. Thanks again, Damian