File Coverage

blib/lib/MooseX/Storage/Engine.pm
Criterion Covered Total %
statement 70 78 89.7
branch 31 38 81.5
condition 16 21 76.1
subroutine 15 18 83.3
pod 13 13 100.0
total 145 168 86.3


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