File Coverage

blib/lib/Document/Transform.pm
Criterion Covered Total %
statement 28 66 42.4
branch 0 16 0.0
condition n/a
subroutine 10 17 58.8
pod 4 4 100.0
total 42 103 40.7


line stmt bran cond sub pod time code
1             package Document::Transform;
2             BEGIN {
3 2     2   2211626 $Document::Transform::VERSION = '1.110530';
4             }
5              
6             #ABSTRACT: Pull and transform documents from a NoSQL backend
7              
8 2     2   10991 use Moose;
  2         565227  
  2         20  
9 2     2   16561 use namespace::autoclean;
  2         2924  
  2         13  
10              
11 2     2   124 use Try::Tiny;
  2         3  
  2         118  
12 2     2   1832 use Throwable::Error;
  2         125267  
  2         65  
13 2     2   1943 use MooseX::Params::Validate;
  2         25320  
  2         17  
14 2     2   2774 use MooseX::Types::Moose(':all');
  2         142149  
  2         21  
15 2     2   26020 use Document::Transform::Transformer;
  2         8  
  2         111  
16 2     2   21 use Moose::Util::TypeConstraints('match_on_type');
  2         4  
  2         20  
17 2     2   3018 use Devel::PartialDump('dump');
  2         22672  
  2         12  
18              
19              
20             has backend =>
21             (
22             is => 'ro',
23             does => 'Document::Transform::Role::Backend',
24             required => 1,
25             handles => 'Document::Transform::Role::Backend',
26             );
27              
28              
29             has transformer =>
30             (
31             is => 'ro',
32             does => 'Document::Transform::Role::Transformer',
33             handles => ['transform'],
34             builder => '_build_transformer',
35             );
36              
37             sub _build_transformer
38             {
39 0     0     my ($self) = @_;
40 0           return Document::Transform::Transformer->new(
41             document_constraint => $self->document_constraint,
42             transform_constraint => $self->transform_constraint,
43             );
44             }
45              
46              
47              
48             has post_fetch_callback =>
49             (
50             is => 'ro',
51             isa => CodeRef,
52             predicate => 'has_post_fetch_callback',
53             );
54              
55              
56             sub fetch
57             {
58 0     0 1   my ($self, $key) = pos_validated_list
59             (
60             \@_,
61             {isa => __PACKAGE__},
62             {isa => Defined}
63             );
64              
65 0           my $transform = $self->fetch_transform_from_key($key);
66 0 0         unless(defined($transform))
67             {
68 0           my $document = $self->fetch_document_from_key($key);
69 0 0         unless(defined($document))
70             {
71 0           Throwable::Error->throw
72             ({
73             message => 'Unable to fetch anything useful with: '.dump($key)
74             });
75             }
76 0 0         if($self->has_post_fetch_callback)
77             {
78 0           $self->post_fetch_callback->($document);
79             }
80              
81 0           return $document;
82             }
83             else
84             {
85 0           my @transforms = ($transform);
86 0           my $doc;
87              
88             do
89 0           {
90 0           $doc = $self->fetch_document_from_transform($transform);
91 0 0         if($self->transform_constraint->check($doc))
92             {
93 0 0         my @check = map
94             {
95 0           $self->is_same_transform($_, $doc)
96             ? $_
97             : ()
98             }
99             @transforms;
100              
101 0 0         if(scalar(@check))
102             {
103 0           Throwable::Error->throw
104             ({
105             message => 'Circular references detected while'.
106             'traversing transform references starting with: '.
107             dump($transform)
108             });
109             }
110 0           push(@transforms, $doc);
111 0           $transform = $doc;
112             }
113             }
114             while($self->transform_constraint->check($doc));
115              
116 0           @transforms = reverse @transforms;
117             # bottom most transform should be executed first
118 0           my $final = $self->transform($doc, \@transforms);
119              
120 0 0         if($self->has_post_fetch_callback)
121             {
122 0           $self->post_fetch_callback->($final);
123             }
124              
125 0 0         if(wantarray)
126             {
127 0           return ($final, @transforms);
128             }
129             else
130             {
131 0           return $final;
132             }
133             }
134             }
135              
136              
137             sub store
138             {
139 0     0 1   my $self = shift;
140 0           my ($item) = pos_validated_list
141             (
142             \@_,
143             {isa =>
144             Moose::Util::TypeConstraints::create_type_constraint_union(
145             $self->document_constraint,
146             $self->transform_constraint
147             )
148             }
149             );
150              
151             match_on_type $item =>
152             (
153 0     0     $self->document_constraint => sub { $self->store_document($item) },
154 0     0     $self->transform_constraint => sub { $self->store_transform($item) },
155 0           );
156             }
157              
158              
159             sub check_fetch_document
160             {
161 0     0 1   my ($self, $key) = pos_validated_list
162             (
163             \@_,
164             {isa => __PACKAGE__},
165             {isa => Defined}
166             );
167              
168 0           return $self->has_document($key);
169             }
170              
171              
172             sub check_fetch_transform
173             {
174 0     0 1   my ($self, $key) = pos_validated_list
175             (
176             \@_,
177             {isa => __PACKAGE__},
178             {isa => Defined}
179             );
180              
181 0           return $self->has_transform($key);
182             }
183              
184             __PACKAGE__->meta->make_immutable();
185             1;
186              
187              
188              
189             =pod
190              
191             =head1 NAME
192              
193             Document::Transform - Pull and transform documents from a NoSQL backend
194              
195             =head1 VERSION
196              
197             version 1.110530
198              
199             =head1 SYNOPSIS
200              
201             use Try::Tiny;
202             use Document::Transform;
203             use Document::Transform::Backend::MongoDB;
204              
205             my $backend = Document::Transform::Backend::MongoDB->new(
206             host => $ENV{MONGOD}
207             database_name => 'foo',
208             transform_collection => 'transforms',
209             document_collection => 'documents');
210              
211             my $transform = Document::Transform->new(backend => $backend);
212              
213             my $result;
214              
215             try
216             {
217             $result = $transform->fetch(
218             MongoDB::OID->new(value => 'SOME_DOCUMENT'));
219             }
220             catch
221             {
222             warn 'Failed to fetch the document';
223             }
224              
225             =head1 DESCRIPTION
226              
227             Ever need to fetch a document from some NoSQL source, and wanted a way to store
228             only the specific changes to that document in a separate document and magically
229             combine the two when you ask for the more specific document? Then this module
230             will help you get that pony you've always wanted.
231              
232             Consider the following JSON document:
233              
234             {
235             "document_id": "QWERTY1",
236             "foo": "bar",
237             "yarg":
238             [
239             "one",
240             "two",
241             "three"
242             ],
243             "blarg": "sock puppets rock"
244             }
245              
246             This is an awesome, typical document stored in something like MongoDB. Now, what
247             if we had hundreds of other documents that were all the same except the "blarg"
248             attribute was slightly different? It would be wasteful to store all of those
249             whole complete documents. And what if we wanted to update them all? That could
250             potentially be expensive. So what is the solution? Store a core document, and
251             store the set of changes to morph it into the more specific or different
252             document separately. Then when you update the core document, everything else
253             continues to work without manually touching all of the other documents.
254              
255             So what does a transform look like? Like this:
256              
257             {
258             "transform_id": "YTREWQ1",
259             "document_id": "QWERTY1",
260             "operations":
261             [
262             {
263             "path": "/yarg/*[0]",
264             "value": "ONE"
265             },
266             {
267             "path": "/foo",
268             "value": "BAR"
269             },
270             {
271             "path": "/qwak/farg",
272             "value": { "yarp": 1, "narp": 0 }
273             }
274             ]
275             }
276              
277             Jumpin' jehosaphat! What is all of that line noise? So, you can see how this
278             transform references the core document via the document_id attribute. The
279             transform_id is what we use to fetch this transform. The operations attribute
280             holds an array of tuples. Each tuple is merely a L<Data::DPath> path
281             specification and a value to be used at that location. What is a Data::DPath
282             path? Well, it is like XPath but for data structures. It is some good stuff.
283              
284             So the first two operations look simple enough. We reference locations that
285             exist and replace those values with all caps versions, but what about the last
286             operation? The original document doesn't have anything that matches that path.
287             Well, you're in luck. If your path is simple enough, the transformer will
288             create that path for you and dump your value there for you. Now, let me stress
289             "simple enough." It needs to be straight hashes, no filters, no array access,
290             etc. So, '/this/path/rocks' will work just fine. '/this/*[4]/path/sucks' will
291             not work. If you would like it to work, you are more than welcome to implement
292             your own transformer. Simply consume the interface role
293             L<Document::Transform::Role::Transformer> and implement the transform method and
294             pass in an instance and you are set.
295              
296             Something else this module does is allow you to have transforms reference other
297             transforms that reference other transforms, and so on until it reaches a source
298             document. Please be careful that the transforms dont ultimately make a giant
299             circular linked list that will never resolve to a document. There are checks in
300             place to throw an exception if a transform has already been seen when
301             attempting to get a document, but the checks are naive and only look at the
302             L<Document::Transform::Role::Backend/transform_id_key>. If this key is not
303             unique in your NoSQL store, then you are screwed. You've been warned.
304              
305             This module ships with one backend and one transformer implemented but you
306             aren't married to either if you don't like MongoDB or think the transformer
307             semantics are subpar. This module and its packages are all very L<Bread::Board>
308             friendly.
309              
310             =head1 PUBLIC_ATTRIBUTES
311              
312             =head2 backend
313              
314             is: ro, does: Document::Transform::Role::Backend, required: 1
315              
316             The backend attribute is required for instantiation of the Document::Transform
317             object. The backend object is what talks to whichever NoSQL resource to fetch
318             and store documents and transforms.
319              
320             You are encouraged to implement your own backends. Simply consume the interface
321             role L<Document::Transform::Role::Backend> and implement the required methods.
322              
323             =head2 transformer
324              
325             is: ro, does: L<Document::Transform::Role::Transformer>
326             builder: '_build_transformer', lazy: 1
327              
328             The transformer is the object to which transformation responsibilities are
329             delegated. By default, the L<Document::Transform::Transformer> class is
330             instantiated when none is provided. Please see its documentation on the
331             expectations of document and transform formats.
332              
333             If you would like to implement your own transformer (to support your own
334             document and transform formats), simply consume the interface role
335             L<Document::Transform::Role::Transformer> and implement the transform method.
336              
337             =head2 post_fetch_callback
338              
339             is: ro, isa: CodeRef
340              
341             post_fetch_callback simply provides a way to do additional processing after
342             the document has been fetched and transform executed. One good use for this is
343             if validation of the result needs to take place. This coderef is called naked
344             with a single argument, the final document. Throw an exception if execution
345             should stop. The return value is discarded.
346              
347             =head1 PUBLIC_METHODS
348              
349             =head2 fetch
350              
351             (Defined)
352              
353             fetch performs a transform lookup using the provided key argument, then a
354             document lookup based information inside the transform(which can recurse as
355             transforms can reference other transforms until a document is reached). Once it
356             has both pieces (all of the transforms and the document), it passes them on to
357             the transformer via the transform method. The result is then passed to the
358             callback L</post_fetch_callback> before finally being returned.
359              
360             If for whatever reason there isn't a transform with that key, but there is a
361             document with that key, the document will be fetched and not transformed. It is
362             still subject to the L</post_fetch_callback> though.
363              
364             In list context, the transformed document along with the transforms executed
365             are returned. Otherwise, just the transformed document is returned.
366              
367             =head2 store
368              
369             (Backend constrained document or transform)
370              
371             store takes a single item as an argument and depending on the the type
372             constraints from the backend it will execute the appropriate store method on
373             the backend. See L<Document::Transform::Role::Backend/document_constraint> and
374             L<Document::Transform::Role::Backend/transform_constraint> for more information
375              
376             =head2 check_fetch_document
377              
378             (Defined)
379              
380             A document fetch is attempted with the provided argument. If successful, it
381             returns true.
382              
383             =head2 check_fetch_transform
384              
385             (Defined)
386              
387             A transform fetch is attempted with the provided argument. If successful, it
388             returns true.
389              
390             =head1 AUTHOR
391              
392             Nicholas R. Perez <nperez@cpan.org>
393              
394             =head1 COPYRIGHT AND LICENSE
395              
396             This software is copyright (c) 2010 by Infinity Interactive.
397              
398             This is free software; you can redistribute it and/or modify it under
399             the same terms as the Perl 5 programming language system itself.
400              
401             =cut
402              
403              
404             __END__
405