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