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