File Coverage

blib/lib/MooseX/Storage/Engine.pm
Criterion Covered Total %
statement 72 82 87.8
branch 33 44 75.0
condition 14 21 66.6
subroutine 15 18 83.3
pod 13 13 100.0
total 147 178 82.5


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.52';
5              
6 29     29   730057 use Moose;
  29         104128  
  29         585  
7 29     29   135690 use Scalar::Util qw(refaddr blessed);
  29         44  
  29         1881  
8 29     29   126 use Carp 'confess';
  29         33  
  29         1102  
9 29     29   1785 use namespace::autoclean;
  29         19150  
  29         223  
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 88     88 1 174 my ( $self, %options ) = @_;
34              
35             # NOTE:
36             # mark the root object as seen ...
37 88         2475 $self->seen->{refaddr $self->object} = undef;
38              
39 88         409 $self->map_attributes('collapse_attribute', \%options);
40 85         3439 $self->storage->{$CLASS_MARKER} = $self->object->meta->identifier;
41 85         2291 return $self->storage;
42             }
43              
44             sub expand_object {
45 78     78 1 251 my ($self, $data, %options) = @_;
46              
47 78 100       295 $options{check_version} = 1 unless exists $options{check_version};
48 78 100       262 $options{check_authority} = 1 unless exists $options{check_authority};
49              
50             # NOTE:
51             # mark the root object as seen ...
52 78         2231 $self->seen->{refaddr $data} = undef;
53              
54 78         282 $self->map_attributes('expand_attribute', $data, \%options);
55 75         2161 return $self->storage;
56             }
57              
58             ## this is the internal API ...
59              
60             sub collapse_attribute {
61 350     350 1 401 my ($self, $attr, $options) = @_;
62 350         611 my $value = $self->collapse_attribute_value($attr, $options);
63              
64 347 100       9383 return unless $attr->has_value($self->object);
65 210         9872 $self->storage->{$attr->name} = $value;
66             }
67              
68             sub expand_attribute {
69 332     332 1 377 my ($self, $attr, $data, $options) = @_;
70 332 100       1087 return unless exists $data->{$attr->name};
71 192         565 my $value = $self->expand_attribute_value($attr, $data->{$attr->name}, $options);
72 189         5910 $self->storage->{$attr->name} = $value;
73             }
74              
75             sub collapse_attribute_value {
76 350     350 1 324 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 350         9602 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 350 100       31502 $self->check_for_cycle_in_collapse($attr, $value)
86             if ref $value;
87              
88 348 100 100     7718 if (defined $value && $attr->has_type_constraint) {
89 190         6415 my $type_converter = $self->find_type_handler($attr->type_constraint, $value);
90 190 50       8106 (defined $type_converter)
91             || confess "Cannot convert " . $attr->type_constraint->name;
92 190         553 $value = $type_converter->{collapse}->($value, $options);
93             }
94 347         2853 return $value;
95             }
96              
97             sub expand_attribute_value {
98 192     192 1 239 my ($self, $attr, $value, $options) = @_;
99              
100             # NOTE:
101             # (see comment in method above ^^)
102 192 100 66     2809 if( ref $value and not(
      100        
103             $options->{disable_cycle_check} or
104             $self->class->does('MooseX::Storage::Traits::DisableCycleDetection')
105             )) {
106 67         20975 $self->check_for_cycle_in_collapse($attr, $value)
107             }
108              
109 191 100 100     7936 if (defined $value && $attr->has_type_constraint) {
110 178         6291 my $type_converter = $self->find_type_handler($attr->type_constraint, $value);
111 178         8656 $value = $type_converter->{expand}->($value, $options);
112             }
113 189         25233 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 219 my ($self, $attr, $value) = @_;
121 137 100       3879 (!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         3615 $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 166     166 1 367 my ($self, $method_name, @args) = @_;
141             map {
142 683         15811 $self->$method_name($_, @args)
143             } grep {
144             # Skip our special skip attribute :)
145 166 100       5646 !$_->does('MooseX::Storage::Meta::Attribute::Trait::DoNotSerialize')
  693         43069  
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 6061 my ($self, $type_name, %handlers) = @_;
282             (exists $handlers{expand} && exists $handlers{collapse})
283 2 50 33     15 || confess "Custom type handlers need an expand *and* a collapse method";
284 2         6 $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 402     402 1 3508 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 402 100       11133 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       2058 if (my $parent = $type_constraint->parent) {
304 76 100 66     5233 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       1692 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       269 if($type_constraint->can('find_type_for')) {
319 4         16 my $tc = $type_constraint->find_type_for($value);
320 4 50       805 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         50 my %seen;
328 42         139 my @fallback = grep { !$seen{$_}++ } qw/Int Num Str Value/, keys %TYPES;
  506         714  
329 42         84 foreach my $type ( @fallback ) {
330 208 100       75742 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 29     29   53383 no Moose::Role;
  29         9540  
  29         214  
351              
352             1;
353              
354             __END__
355              
356             =pod
357              
358             =encoding UTF-8
359              
360             =head1 NAME
361              
362             MooseX::Storage::Engine - The meta-engine to handle collapsing and expanding objects
363              
364             =head1 VERSION
365              
366             version 0.52
367              
368             =head1 DESCRIPTION
369              
370             There really aren't any major user serviceable parts here. However the typical
371             use case is adding new non-Moose classes to the type registry for
372             serialization. Here is an example of this for L<DateTime> objects. This
373             assumes a C<DateTime> type has been registered.
374              
375             MooseX::Storage::Engine->add_custom_type_handler(
376             'DateTime' => (
377             expand => sub { DateTime::Format::ISO8601->new->parser_datetime(shift) },
378             collapse => sub { (shift)->iso8601 },
379             )
380             );
381              
382             =head1 METHODS
383              
384             =head2 Accessors
385              
386             =over 4
387              
388             =item B<class>
389              
390             =item B<object>
391              
392             =item B<storage>
393              
394             =item B<seen>
395              
396             =back
397              
398             =head2 API
399              
400             =over 4
401              
402             =item B<expand_object>
403              
404             =item B<collapse_object>
405              
406             =back
407              
408             =head2 ...
409              
410             =over 4
411              
412             =item B<collapse_attribute>
413              
414             =item B<collapse_attribute_value>
415              
416             =item B<expand_attribute>
417              
418             =item B<expand_attribute_value>
419              
420             =item B<check_for_cycle_in_collapse>
421              
422             =item B<check_for_cycle_in_expansion>
423              
424             =item B<map_attributes>
425              
426             =back
427              
428             =head2 Type Constraint Handlers
429              
430             =over 4
431              
432             =item B<find_type_handler ($type)>
433              
434             =item B<find_type_handler_for ($name)>
435              
436             =item B<add_custom_type_handler ($name, %handlers)>
437              
438             =item B<remove_custom_type_handler ($name)>
439              
440             =back
441              
442             =head1 SUPPORT
443              
444             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=MooseX-Storage>
445             (or L<bug-MooseX-Storage@rt.cpan.org|mailto:bug-MooseX-Storage@rt.cpan.org>).
446              
447             There is also a mailing list available for users of this distribution, at
448             L<http://lists.perl.org/list/moose.html>.
449              
450             There is also an irc channel available for users of this distribution, at
451             L<C<#moose> on C<irc.perl.org>|irc://irc.perl.org/#moose>.
452              
453             =head1 AUTHORS
454              
455             =over 4
456              
457             =item *
458              
459             Chris Prather <chris.prather@iinteractive.com>
460              
461             =item *
462              
463             Stevan Little <stevan.little@iinteractive.com>
464              
465             =item *
466              
467             יובל קוג'מן (Yuval Kogman) <nothingmuch@woobling.org>
468              
469             =back
470              
471             =head1 COPYRIGHT AND LICENSE
472              
473             This software is copyright (c) 2007 by Infinity Interactive, Inc.
474              
475             This is free software; you can redistribute it and/or modify it under
476             the same terms as the Perl 5 programming language system itself.
477              
478             =cut