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