f4867c4db8ba72456199ebd8618b7044f56bc461
[gitmo/MooseX-Storage.git] / lib / MooseX / Storage / Engine.pm
1
2 package MooseX::Storage::Engine;
3 use Moose;
4
5 our $VERSION   = '0.06';
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         !$_->does('MooseX::Storage::Meta::Attribute::Trait::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 ($obj) does not have a &pack method, cannot collapse";
200         $obj->pack(%$options);
201     },
202 );
203
204
205 my %TYPES = (
206     # NOTE:
207     # we need to make sure that we properly numify the numbers 
208     # before and after them being futzed with, because some of 
209     # the JSON engines are stupid/annoying/frustrating
210     'Int'      => { expand => sub { $_[0] + 0 }, collapse => sub { $_[0] + 0 } },
211     'Num'      => { expand => sub { $_[0] + 0 }, collapse => sub { $_[0] + 0 } },
212     # These are boring ones, so they use the identity function ...    
213     'Str'      => { expand => sub { shift }, collapse => sub { shift } },
214     'Bool'     => { expand => sub { shift }, collapse => sub { shift } },
215     # These are the trickier ones, (see notes)
216     # NOTE:
217     # Because we are nice guys, we will check 
218     # your ArrayRef and/or HashRef one level 
219     # down and inflate any objects we find. 
220     # But this is where it ends, it is too
221     # expensive to try and do this any more  
222     # recursively, when it is probably not 
223     # nessecary in most of the use cases.
224     # However, if you need more then this, subtype 
225     # and add a custom handler.    
226     'ArrayRef' => { 
227         expand => sub {
228             my ( $array, @args ) = @_;
229             foreach my $i (0 .. $#{$array}) {
230                 next unless ref($array->[$i]) eq 'HASH' 
231                          && exists $array->[$i]->{$CLASS_MARKER};
232                 $array->[$i] = $OBJECT_HANDLERS{expand}->($array->[$i], @args);
233             }
234             $array;
235         }, 
236         collapse => sub {
237             my ( $array, @args ) = @_;
238             # NOTE:         
239             # we need to make a copy cause
240             # otherwise it will affect the 
241             # other real version.
242             [ map {
243                 blessed($_)
244                     ? $OBJECT_HANDLERS{collapse}->($_, @args)
245                     : $_
246             } @$array ] 
247         } 
248     },
249     'HashRef'  => { 
250         expand   => sub {
251             my ( $hash, @args ) = @_;
252             foreach my $k (keys %$hash) {
253                 next unless ref($hash->{$k}) eq 'HASH' 
254                          && exists $hash->{$k}->{$CLASS_MARKER};
255                 $hash->{$k} = $OBJECT_HANDLERS{expand}->($hash->{$k}, @args);
256             }
257             $hash;            
258         }, 
259         collapse => sub {
260             my ( $hash, @args ) = @_;
261             # NOTE:         
262             # we need to make a copy cause
263             # otherwise it will affect the 
264             # other real version.
265             +{ map {
266                 blessed($hash->{$_})
267                     ? ($_ => $OBJECT_HANDLERS{collapse}->($hash->{$_}, @args))
268                     : ($_ => $hash->{$_})
269             } keys %$hash }            
270         } 
271     },
272     'Object'   => \%OBJECT_HANDLERS,
273     # NOTE:
274     # The sanity of enabling this feature by 
275     # default is very questionable.
276     # - SL
277     #'CodeRef' => {
278     #    expand   => sub {}, # use eval ...
279     #    collapse => sub {}, # use B::Deparse ...        
280     #} 
281 );
282
283 sub add_custom_type_handler {
284     my ($class, $type_name, %handlers) = @_;
285     (exists $handlers{expand} && exists $handlers{collapse})
286         || confess "Custom type handlers need an expand *and* a collapse method";
287     $TYPES{$type_name} = \%handlers;
288 }
289
290 sub remove_custom_type_handler {
291     my ($class, $type_name) = @_;
292     delete $TYPES{$type_name} if exists $TYPES{$type_name};
293 }
294
295 sub find_type_handler {
296     my ($self, $type_constraint) = @_;
297     
298     # this should handle most type usages
299     # since they they are usually just 
300     # the standard set of built-ins
301     return $TYPES{$type_constraint->name} 
302         if exists $TYPES{$type_constraint->name};
303       
304     # the next possibility is they are 
305     # a subtype of the built-in types, 
306     # in which case this will DWIM in 
307     # most cases. It is probably not 
308     # 100% ideal though, but until I 
309     # come up with a decent test case 
310     # it will do for now.
311     foreach my $type (keys %TYPES) {
312         return $TYPES{$type} 
313             if $type_constraint->is_subtype_of($type);
314     }
315     
316     # NOTE:
317     # the reason the above will work has to 
318     # do with the fact that custom subtypes
319     # are mostly used for validation of 
320     # the guts of a type, and not for some
321     # weird structural thing which would 
322     # need to be accomidated by the serializer.
323     # Of course, mst or phaylon will probably  
324     # do something to throw this assumption 
325     # totally out the door ;)
326     # - SL
327     
328     # NOTE:
329     # if this method hasnt returned by now
330     # then we have no been able to find a 
331     # type constraint handler to match 
332     confess "Cannot handle type constraint (" . $type_constraint->name . ")";    
333 }
334
335 sub find_type_handler_for {
336     my ($self, $type_handler_name) = @_;
337     $TYPES{$type_handler_name}
338 }
339
340 1;
341
342 __END__
343
344 =pod
345
346 =head1 NAME
347
348 MooseX::Storage::Engine - The meta-engine to handle collapsing and expanding objects
349
350 =head1 DESCRIPTION
351
352 No user serviceable parts inside. If you really want to know, read the source :)
353
354 =head1 METHODS
355
356 =head2 Accessors
357
358 =over 4
359
360 =item B<class>
361
362 =item B<object>
363
364 =item B<storage>
365
366 =item B<seen>
367
368 =back
369
370 =head2 API
371
372 =over 4
373
374 =item B<expand_object>
375
376 =item B<collapse_object>
377
378 =back
379
380 =head2 ...
381
382 =over 4
383
384 =item B<collapse_attribute>
385
386 =item B<collapse_attribute_value>
387
388 =item B<expand_attribute>
389
390 =item B<expand_attribute_value>
391
392 =item B<check_for_cycle_in_collapse>
393
394 =item B<check_for_cycle_in_expansion>
395
396 =item B<map_attributes>
397
398 =back
399
400 =head2 Type Constraint Handlers
401
402 =over 4
403
404 =item B<find_type_handler ($type)>
405
406 =item B<find_type_handler_for ($name)>
407
408 =item B<add_custom_type_handler ($name, %handlers)>
409
410 =item B<remove_custom_type_handler ($name)>
411
412 =back
413
414 =head2 Introspection
415
416 =over 4
417
418 =item B<meta>
419
420 =back
421
422 =head1 BUGS
423
424 All complex software has bugs lurking in it, and this module is no 
425 exception. If you find a bug please either email me, or add the bug
426 to cpan-RT.
427
428 =head1 AUTHOR
429
430 Chris Prather E<lt>chris.prather@iinteractive.comE<gt>
431
432 Stevan Little E<lt>stevan.little@iinteractive.comE<gt>
433
434 =head1 COPYRIGHT AND LICENSE
435
436 Copyright 2007-2008 by Infinity Interactive, Inc.
437
438 L<http://www.iinteractive.com>
439
440 This library is free software; you can redistribute it and/or modify
441 it under the same terms as Perl itself.
442
443 =cut
444
445
446