File Coverage

blib/lib/MooseX/Storage/Engine.pm
Criterion Covered Total %
statement 69 79 87.3
branch 33 44 75.0
condition 14 21 66.6
subroutine 14 17 82.3
pod 13 13 100.0
total 143 174 82.1


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