* add feature to disable cycle checking, eitehr via trait or option
[gitmo/MooseX-Storage.git] / lib / MooseX / Storage / Engine.pm
1
2 package MooseX::Storage::Engine;
3 use Moose;
4 use Scalar::Util qw(refaddr);
5
6 our $VERSION   = '0.18';
7 our $AUTHORITY = 'cpan:STEVAN';
8
9 # the class marker when 
10 # serializing an object. 
11 our $CLASS_MARKER = '__CLASS__';
12
13 has 'storage' => (
14     is      => 'ro',
15     isa     => 'HashRef',
16     default => sub {{}}
17 );
18
19 has 'seen' => (
20     is      => 'ro',
21     isa     => 'HashRef[Int]', # int is the refaddr
22     default => sub {{}}
23 );
24
25 has 'object' => (is => 'rw', isa => 'Object');
26 has 'class'  => (is => 'rw', isa => 'Str');
27
28 ## this is the API used by other modules ...
29
30 sub collapse_object {
31     my ( $self, %options ) = @_;
32
33         # NOTE:
34         # mark the root object as seen ...
35         $self->seen->{refaddr $self->object} = undef;
36         
37     $self->map_attributes('collapse_attribute', \%options);
38     $self->storage->{$CLASS_MARKER} = $self->object->meta->identifier;    
39         return $self->storage;
40 }
41
42 sub expand_object {
43     my ($self, $data, %options) = @_;
44     
45     $options{check_version}       = 1 unless exists $options{check_version};
46     $options{check_authority}     = 1 unless exists $options{check_authority};   
47
48         # NOTE:
49         # mark the root object as seen ...
50         $self->seen->{refaddr $data} = undef;    
51     
52     $self->map_attributes('expand_attribute', $data, \%options);
53         return $self->storage;    
54 }
55
56 ## this is the internal API ...
57
58 sub collapse_attribute {
59     my ($self, $attr, $options)  = @_;
60     my $value = $self->collapse_attribute_value($attr, $options);
61     return if !defined($value);
62     $self->storage->{$attr->name} = $value;
63 }
64
65 sub expand_attribute {
66     my ($self, $attr, $data, $options)  = @_;
67     my $value = $self->expand_attribute_value($attr, $data->{$attr->name}, $options);
68     $self->storage->{$attr->name} = defined $value ? $value : return;
69 }
70
71 sub collapse_attribute_value {
72     my ($self, $attr, $options)  = @_;
73     # Faster, but breaks attributes without readers, do we care?
74         #my $value = $attr->get_read_method_ref->($self->object);
75         my $value = $attr->get_value($self->object);
76
77         # NOTE:
78         # this might not be enough, we might
79         # need to make it possible for the
80         # cycle checker to return the value
81         # Check cycles unless explicitly disabled
82     if( ref $value and not(
83         $options->{disable_cycle_check} or
84         $self->object->does('MooseX::Storage::Traits::DisableCycleDetection')
85     )) {        
86         $self->check_for_cycle_in_collapse($attr, $value)
87     }
88
89     if (defined $value && $attr->has_type_constraint) {
90         my $type_converter = $self->find_type_handler($attr->type_constraint);
91         (defined $type_converter)
92             || confess "Cannot convert " . $attr->type_constraint->name;
93         $value = $type_converter->{collapse}->($value, $options);
94     }
95         return $value;
96 }
97
98 sub expand_attribute_value {
99     my ($self, $attr, $value, $options)  = @_;
100
101         # NOTE:
102         # (see comment in method above ^^)
103     if( ref $value and not(
104         $options->{disable_cycle_check} or
105         $self->class->does('MooseX::Storage::Traits::DisableCycleDetection')
106     )) {        
107         $self->check_for_cycle_in_collapse($attr, $value)
108     }
109     
110     if (defined $value && $attr->has_type_constraint) {
111         my $type_converter = $self->find_type_handler($attr->type_constraint);
112         $value = $type_converter->{expand}->($value, $options);
113     }
114         return $value;
115 }
116
117 # NOTE:
118 # possibly these two methods will 
119 # be used by a cycle supporting 
120 # engine. However, I am not sure 
121 # if I can make a cycle one work 
122 # anyway.
123
124 sub check_for_cycle_in_collapse {
125     my ($self, $attr, $value) = @_;
126     (!exists $self->seen->{refaddr $value})
127         || confess "Basic Engine does not support cycles in class(" 
128                  . ($attr->associated_class->name) . ").attr("
129                  . ($attr->name) . ") with $value";
130     $self->seen->{refaddr $value} = undef;
131 }
132
133 sub check_for_cycle_in_expansion {
134     my ($self, $attr, $value) = @_;
135     (!exists $self->seen->{refaddr $value})
136     || confess "Basic Engine does not support cycles in class(" 
137              . ($attr->associated_class->name) . ").attr("
138              . ($attr->name) . ") with $value";
139     $self->seen->{refaddr $value} = undef;
140 }
141
142 # util methods ...
143
144 sub map_attributes {
145     my ($self, $method_name, @args) = @_;
146  
147     # The $self->object check is here to differentiate a ->pack from a 
148     # ->unpack; ->object is only defined for a ->pack
149  
150     # no checks needed if this is class based (ie, restore)
151     unless( $self->object ) {
152         return map { $self->$method_name($_, @args) }
153             $self->class->meta->get_all_attributes;
154     }
155     
156     # if it's object based, it's a store -- in that case, 
157     # check thoroughly
158     my @rv;
159     my $o = $self->object;
160     for my $attr ( $o->meta->get_all_attributes ) {    
161         
162         # Skip our special skip attribute :)
163         next if $attr->does(
164             'MooseX::Storage::Meta::Attribute::Trait::DoNotSerialize');
165
166         # If we're invoked with the 'OnlyWhenBuilt' trait, we should
167         # only serialize the attribute if it's already built. So, go ahead
168         # and check if the attribute has a predicate. If so, check if it's 
169         # set  and then go ahead and look it up.
170         if( $o->does('MooseX::Storage::Traits::OnlyWhenBuilt') and 
171             my $pred = $attr->predicate 
172         ) { 
173             next unless $self->object->$pred; 
174         }         
175         push @rv, $self->$method_name($attr, @args);
176     } 
177
178     return @rv;
179 }
180
181 ## ------------------------------------------------------------------
182 ## This is all the type handler stuff, it is in a state of flux
183 ## right now, so this may change, or it may just continue to be 
184 ## improved upon. Comments and suggestions are welcomed.
185 ## ------------------------------------------------------------------
186
187 # NOTE:
188 # these are needed by the 
189 # ArrayRef and HashRef handlers
190 # below, so I need easy access 
191 my %OBJECT_HANDLERS = (
192     expand => sub {
193         my ($data, $options) = @_;   
194         (exists $data->{$CLASS_MARKER})
195             || confess "Serialized item has no class marker";
196         # check the class more thoroughly here ...
197         my ($class, $version, $authority) = (split '-' => $data->{$CLASS_MARKER});
198         my $meta = eval { $class->meta };
199         confess "Class ($class) is not loaded, cannot unpack" if $@;     
200         
201         if ($options->{check_version}) {
202             my $meta_version = $meta->version;
203             if (defined $meta_version && $version) {            
204                 if ($options->{check_version} eq 'allow_less_than') {
205                     ($meta_version <= $version)
206                         || confess "Class ($class) versions is not less than currently available." 
207                                  . " got=($version) available=($meta_version)";                
208                 }
209                 elsif ($options->{check_version} eq 'allow_greater_than') {
210                     ($meta->version >= $version)
211                         || confess "Class ($class) versions is not greater than currently available." 
212                                  . " got=($version) available=($meta_version)";                
213                 }            
214                 else {
215                     ($meta->version == $version)
216                         || confess "Class ($class) versions don't match." 
217                                  . " got=($version) available=($meta_version)";
218                 }
219             }
220         }
221         
222         if ($options->{check_authority}) {
223             my $meta_authority = $meta->authority;
224             ($meta->authority eq $authority)
225                 || confess "Class ($class) authorities don't match." 
226                          . " got=($authority) available=($meta_authority)"
227                 if defined $meta_authority && defined $authority;            
228         }
229             
230         # all is well ...
231         $class->unpack($data, %$options);
232     },
233     collapse => sub {
234         my ( $obj, $options ) = @_;
235 #        ($obj->can('does') && $obj->does('MooseX::Storage::Basic'))
236 #            || confess "Bad object ($obj) does not do MooseX::Storage::Basic role";
237         ($obj->can('pack'))
238             || confess "Object ($obj) does not have a &pack method, cannot collapse";
239         $obj->pack(%$options);
240     },
241 );
242
243
244 my %TYPES = (
245     # NOTE:
246     # we need to make sure that we properly numify the numbers 
247     # before and after them being futzed with, because some of 
248     # the JSON engines are stupid/annoying/frustrating
249     'Int'      => { expand => sub { $_[0] + 0 }, collapse => sub { $_[0] + 0 } },
250     'Num'      => { expand => sub { $_[0] + 0 }, collapse => sub { $_[0] + 0 } },
251     # These are boring ones, so they use the identity function ...    
252     'Str'      => { expand => sub { shift }, collapse => sub { shift } },
253     'Bool'     => { expand => sub { shift }, collapse => sub { shift } },
254     # These are the trickier ones, (see notes)
255     # NOTE:
256     # Because we are nice guys, we will check 
257     # your ArrayRef and/or HashRef one level 
258     # down and inflate any objects we find. 
259     # But this is where it ends, it is too
260     # expensive to try and do this any more  
261     # recursively, when it is probably not 
262     # nessecary in most of the use cases.
263     # However, if you need more then this, subtype 
264     # and add a custom handler.    
265     'ArrayRef' => { 
266         expand => sub {
267             my ( $array, @args ) = @_;
268             foreach my $i (0 .. $#{$array}) {
269                 next unless ref($array->[$i]) eq 'HASH' 
270                          && exists $array->[$i]->{$CLASS_MARKER};
271                 $array->[$i] = $OBJECT_HANDLERS{expand}->($array->[$i], @args);
272             }
273             $array;
274         }, 
275         collapse => sub {
276             my ( $array, @args ) = @_;
277             # NOTE:         
278             # we need to make a copy cause
279             # otherwise it will affect the 
280             # other real version.
281             [ map {
282                 blessed($_)
283                     ? $OBJECT_HANDLERS{collapse}->($_, @args)
284                     : $_
285             } @$array ] 
286         } 
287     },
288     'HashRef'  => { 
289         expand   => sub {
290             my ( $hash, @args ) = @_;
291             foreach my $k (keys %$hash) {
292                 next unless ref($hash->{$k}) eq 'HASH' 
293                          && exists $hash->{$k}->{$CLASS_MARKER};
294                 $hash->{$k} = $OBJECT_HANDLERS{expand}->($hash->{$k}, @args);
295             }
296             $hash;            
297         }, 
298         collapse => sub {
299             my ( $hash, @args ) = @_;
300             # NOTE:         
301             # we need to make a copy cause
302             # otherwise it will affect the 
303             # other real version.
304             +{ map {
305                 blessed($hash->{$_})
306                     ? ($_ => $OBJECT_HANDLERS{collapse}->($hash->{$_}, @args))
307                     : ($_ => $hash->{$_})
308             } keys %$hash }            
309         } 
310     },
311     'Object'   => \%OBJECT_HANDLERS,
312     # NOTE:
313     # The sanity of enabling this feature by 
314     # default is very questionable.
315     # - SL
316     #'CodeRef' => {
317     #    expand   => sub {}, # use eval ...
318     #    collapse => sub {}, # use B::Deparse ...        
319     #} 
320 );
321
322 sub add_custom_type_handler {
323     my ($class, $type_name, %handlers) = @_;
324     (exists $handlers{expand} && exists $handlers{collapse})
325         || confess "Custom type handlers need an expand *and* a collapse method";
326     $TYPES{$type_name} = \%handlers;
327 }
328
329 sub remove_custom_type_handler {
330     my ($class, $type_name) = @_;
331     delete $TYPES{$type_name} if exists $TYPES{$type_name};
332 }
333
334 sub find_type_handler {
335     my ($self, $type_constraint) = @_;
336     
337     # check if the type is a Maybe and
338     # if its parent is not parameterized.
339     # If both is true recurse this method
340     # using ->type_parameter.
341     return $self->find_type_handler($type_constraint->type_parameter)
342         if $type_constraint->parent eq 'Maybe'
343           and not $type_constraint->parent->can('type_parameter');
344
345     # this should handle most type usages
346     # since they they are usually just 
347     # the standard set of built-ins
348     return $TYPES{$type_constraint->name} 
349         if exists $TYPES{$type_constraint->name};
350       
351     # the next possibility is they are 
352     # a subtype of the built-in types, 
353     # in which case this will DWIM in 
354     # most cases. It is probably not 
355     # 100% ideal though, but until I 
356     # come up with a decent test case 
357     # it will do for now.
358     foreach my $type (keys %TYPES) {
359         return $TYPES{$type} 
360             if $type_constraint->is_subtype_of($type);
361     }
362     
363     # NOTE:
364     # the reason the above will work has to 
365     # do with the fact that custom subtypes
366     # are mostly used for validation of 
367     # the guts of a type, and not for some
368     # weird structural thing which would 
369     # need to be accomidated by the serializer.
370     # Of course, mst or phaylon will probably  
371     # do something to throw this assumption 
372     # totally out the door ;)
373     # - SL
374     
375     # NOTE:
376     # if this method hasnt returned by now
377     # then we have no been able to find a 
378     # type constraint handler to match 
379     confess "Cannot handle type constraint (" . $type_constraint->name . ")";    
380 }
381
382 sub find_type_handler_for {
383     my ($self, $type_handler_name) = @_;
384     $TYPES{$type_handler_name}
385 }
386
387 1;
388
389 __END__
390
391 =pod
392
393 =head1 NAME
394
395 MooseX::Storage::Engine - The meta-engine to handle collapsing and expanding objects
396
397 =head1 DESCRIPTION
398
399 No user serviceable parts inside. If you really want to know, read the source :)
400
401 =head1 METHODS
402
403 =head2 Accessors
404
405 =over 4
406
407 =item B<class>
408
409 =item B<object>
410
411 =item B<storage>
412
413 =item B<seen>
414
415 =back
416
417 =head2 API
418
419 =over 4
420
421 =item B<expand_object>
422
423 =item B<collapse_object>
424
425 =back
426
427 =head2 ...
428
429 =over 4
430
431 =item B<collapse_attribute>
432
433 =item B<collapse_attribute_value>
434
435 =item B<expand_attribute>
436
437 =item B<expand_attribute_value>
438
439 =item B<check_for_cycle_in_collapse>
440
441 =item B<check_for_cycle_in_expansion>
442
443 =item B<map_attributes>
444
445 =back
446
447 =head2 Type Constraint Handlers
448
449 =over 4
450
451 =item B<find_type_handler ($type)>
452
453 =item B<find_type_handler_for ($name)>
454
455 =item B<add_custom_type_handler ($name, %handlers)>
456
457 =item B<remove_custom_type_handler ($name)>
458
459 =back
460
461 =head2 Introspection
462
463 =over 4
464
465 =item B<meta>
466
467 =back
468
469 =head1 BUGS
470
471 All complex software has bugs lurking in it, and this module is no 
472 exception. If you find a bug please either email me, or add the bug
473 to cpan-RT.
474
475 =head1 AUTHOR
476
477 Chris Prather E<lt>chris.prather@iinteractive.comE<gt>
478
479 Stevan Little E<lt>stevan.little@iinteractive.comE<gt>
480
481 =head1 COPYRIGHT AND LICENSE
482
483 Copyright 2007-2008 by Infinity Interactive, Inc.
484
485 L<http://www.iinteractive.com>
486
487 This library is free software; you can redistribute it and/or modify
488 it under the same terms as Perl itself.
489
490 =cut
491
492
493