File Coverage

blib/lib/CouchDB/Client/Doc.pm
Criterion Covered Total %
statement 18 125 14.4
branch 0 58 0.0
condition 0 19 0.0
subroutine 6 23 26.0
pod 17 17 100.0
total 41 242 16.9


line stmt bran cond sub pod time code
1              
2             package CouchDB::Client::Doc;
3              
4 3     3   37223 use strict;
  3         7  
  3         106  
5 3     3   14 use warnings;
  3         5  
  3         142  
6              
7             our $VERSION = $CouchDB::Client::VERSION;
8              
9 3     3   3236 use HTTP::Request qw();
  3         109420  
  3         154  
10 3     3   22 use URI::Escape qw(uri_escape_utf8);
  3         6  
  3         191  
11 3     3   3393 use MIME::Base64 qw(encode_base64);
  3         2852  
  3         232  
12 3     3   21 use Carp qw(confess);
  3         6  
  3         5781  
13              
14             sub new {
15 0     0 1   my $class = shift;
16 0 0         my %opt = @_ == 1 ? %{$_[0]} : @_;
  0            
17              
18 0 0         confess "Doc needs a database" unless $opt{db};
19              
20 0   0       my %self = (
      0        
      0        
      0        
21             id => $opt{id} || '',
22             rev => $opt{rev} || '',
23             attachments => $opt{attachments} || {},
24             data => $opt{data} || {},
25             db => $opt{db},
26             );
27 0           return bless \%self, $class;
28             }
29              
30 0     0 1   sub id { return $_[0]->{id}; }
31 0     0 1   sub rev { return $_[0]->{rev}; }
32              
33             sub data {
34 0     0 1   my $self = shift;
35 0 0         if (@_) {
36 0           my $data = shift;
37 0   0       $self->{attachments} = delete($data->{_attachments}) || {};
38 0           $self->{data} = $data;
39             }
40             else {
41 0           return $self->{data};
42             }
43             }
44 0 0   0 1   sub attachments { @_ == 2 ? $_[0]->{attachments} = $_[1] : $_[0]->{attachments}; }
45              
46             sub uriName {
47 0     0 1   my $self = shift;
48 0 0         return undef unless $self->{id};
49 0           return $self->{db}->uriName . '/' . uri_escape_utf8($self->{id});
50             }
51              
52             sub create {
53 0     0 1   my $self = shift;
54              
55 0 0         confess("Object already had a revision") if $self->{rev};
56              
57 0           my $content = $self->contentForSubmit;
58 0           my $res;
59 0 0         if ($self->{id}) {
60 0           $res = $self->{db}->{client}->req('PUT', $self->uriName, $content);
61             }
62             else {
63 0           $res = $self->{db}->{client}->req('POST', $self->{db}->uriName, $content);
64             }
65 0 0         confess("Storage error: $res->{msg}") unless $res->{success};
66 0           $self->{rev} = $res->{json}->{rev};
67 0 0         $self->{id} = $res->{json}->{id} unless $self->{id};
68 0           return $self;
69             }
70              
71             sub contentForSubmit {
72 0     0 1   my $self = shift;
73 0           my $content = $self->{data};
74 0 0         $content->{_id} = $self->{id} if $self->{id};
75 0 0         $content->{_rev} = $self->{rev} if $self->{rev};
76 0 0 0       $content->{_attachments} = $self->{attachments} if $self->{attachments} and keys %{$self->{attachments}};
  0            
77 0           return $content;
78             }
79              
80             sub retrieve {
81 0     0 1   my $self = shift;
82              
83 0           my $res = $self->{db}->{client}->req('GET', $self->uriName);
84 0 0         confess("Object not found: $res->{msg}") if $res->{status} == 404;
85 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
86 0           my $data = $res->{json};
87 0           my %private;
88 0           my @keys = keys %$data; # need to two-step this due to delete()
89 0           for my $k (@keys) {
90 0 0         if ($k =~ m/^_(.+)/) {
91 0           $private{$1} = delete $data->{$k};
92             }
93             }
94 0           $self->{data} = $data;
95 0           $self->{id} = $private{id};
96 0           $self->{rev} = $private{rev};
97 0 0         $self->{attachments} = $private{attachments} if exists $private{attachments};
98 0           return $self;
99             }
100              
101             sub retrieveFromRev {
102 0     0 1   my $self = shift;
103 0           my $rev = shift;
104              
105 0           my $res = $self->{db}->{client}->req('GET', $self->uriName . '?rev=' . $rev);
106 0 0         confess("Object not found: $res->{msg}") if $res->{status} == 404;
107 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
108 0           my $data = $res->{json};
109 0           my %private;
110 0           my @keys = keys %$data; # need to two-step this due to delete()
111 0           for my $k (@keys) {
112 0 0         if ($k =~ m/^_(.+)/) {
113 0           $private{$1} = delete $data->{$k};
114             }
115             }
116 0           return ref($self)->new({
117             id => $self->id,
118             rev => $rev,
119             attachments => $private{attachments},
120             data => $data,
121             db => $self->{db},
122             });
123             }
124              
125             sub revisionsInfo {
126 0     0 1   my $self = shift;
127              
128 0           my $res = $self->{db}->{client}->req('GET', $self->uriName . '?revs_info=true');
129 0 0         confess("Object not found: $res->{msg}") if $res->{status} == 404;
130 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
131 0           return $res->{json}->{_revs_info};
132             }
133              
134             sub update {
135 0     0 1   my $self = shift;
136              
137 0 0 0       confess("Object hasn't been retrieved") unless $self->{id} and $self->{rev};
138 0           my $content = $self->contentForSubmit;
139 0           my $res = $self->{db}->{client}->req('PUT', $self->uriName, $content);
140 0 0         confess("Storage error: $res->{msg}") unless $res->{success};
141 0           $self->{rev} = $res->{json}->{rev};
142 0           return $self;
143             }
144              
145             sub delete {
146 0     0 1   my $self = shift;
147              
148 0 0 0       confess("Object hasn't been retrieved") unless $self->{id} and $self->{rev};
149 0           my $res = $self->{db}->{client}->req('DELETE', $self->uriName . "?rev=" . $self->rev);
150 0 0         confess("Object not found: $res->{msg}") if $res->{status} == 404;
151 0 0         confess("Connection error: $res->{msg}") unless $res->{success};
152 0           $self->{deletion_stub_rev} = $res->{json}->{rev};
153 0           $self->{rev} = '';
154 0           $self->{data} = {};
155 0           $self->{attachments} = {};
156 0           return $self;
157             }
158              
159             sub fetchAttachment {
160 0     0 1   my $self = shift;
161 0           my $attName = shift;
162              
163 0 0         confess("No such attachment: '$attName'") unless exists $self->{attachments}->{$attName};
164 0           my $res = $self->{db}->{client}->{ua}->request(
165             HTTP::Request->new('GET', $self->{db}->{client}->uriForPath($self->uriName . '/' . uri_escape_utf8($attName)))
166             );
167 0 0         return $res->content if $res->is_success;
168 0           confess("Object not found: $res->{msg}");
169             }
170              
171             sub addAttachment {
172 0     0 1   my $self = shift;
173 0           my $name = shift;
174 0           my $ctype = shift;
175 0           my $data = shift;
176              
177 0           $self->{attachments}->{$name} = {
178             content_type => $ctype,
179             data => $self->toBase64($data),
180             };
181 0           return $self;
182             }
183              
184             sub deleteAttachment {
185 0     0 1   my $self = shift;
186 0           my $attName = shift;
187              
188 0 0         confess("No such attachment: '$attName'") unless exists $self->{attachments}->{$attName};
189 0           delete $self->{attachments}->{$attName};
190 0           return $self;
191             }
192              
193             sub toBase64 {
194 0     0 1   my $self = shift;
195 0           my $data = shift;
196              
197 0           my $ret = encode_base64 $data;
198 0           $ret =~ s/\n//g;
199 0           return $ret;
200             }
201              
202             1;
203              
204             =pod
205              
206             =head1 NAME
207              
208             CouchDB::Client::Doc - CouchDB::Client document
209              
210             =head1 SYNOPSIS
211              
212             $doc->data->{foo} = 'new bar';
213             $doc->addAttachment('file.xml', 'application/xml', ');
214             $doc->update;
215             $doc->delete;
216              
217             =head1 DESCRIPTION
218              
219             This module represents documents in the CouchDB database.
220              
221             We don't yet deal with a number of options such as retrieving revisions and
222             revision status.
223              
224             =head1 METHODS
225              
226             =over 8
227              
228             =item new
229              
230             Constructor. Takes a hash or hashref of options: C which is the parent
231             C object and is required; the document's C and C
232             if known; a hashref of C being the content; and a hashref of C
233             if present.
234              
235             The C field must be a valid document name (CouchDB accepts anything, but
236             things that are not URI safe have not been tested yet).
237              
238             The C field must be a valid CouchDB revision, it is recommended that you
239             only touch it if you know what you're doing.
240              
241             The C field is a normal Perl hashref that can have nested content. Its
242             keys must not contain fields that being with an underscore (_) as those are
243             reserved for CouchDB.
244              
245             The C field must be structured in the manner that CouchDB expects.
246             It is a hashref with attachment names as its keys and hashrefs as values. The
247             latter have C and C fields which are the MIME media type
248             of the content, and the data in single-line Base64. It is recommended that you
249             manipulate this through the helpers instead.
250              
251             It is not recommended that this constructor be used directly, but rather that
252             C<<newDoc>>> be used instead.
253              
254             =item id
255              
256             Read-only accessor for the ID.
257              
258             =item rev
259              
260             Read-only accessor for the revision.
261              
262             =item data
263              
264             Read-write accessor for the content. See above for the constraints on this hasref.
265             Note that this only changes the data on the client side, you have to create/update
266             the object for it to be stored.
267              
268             =item attachments
269              
270             Read-write accessor for the attachments. See above for the constraints on this hasref.
271             Note that this only changes the attachments on the client side, you have to create/update
272             the object for it to be stored.
273              
274             =item uriName
275              
276             Returns the path part for this object (if it has an ID, otherwise undef).
277              
278             =item create
279              
280             Causes the document to be created in the DB. It will throw an exception if the object already
281             has a revision (since that would indicate that it's already in the DB) or if the actual
282             storage operation fails.
283              
284             If the object has an ID it will PUT it to the URI, otherwise it will POST it and set its ID based
285             on the result. It returns itself, with the C field updated.
286              
287             =item contentForSubmit
288              
289             A helper that returns a data structure matching that of the JSON that will be submitted as part
290             of a create/update operation.
291              
292             =item retrieve
293              
294             Loads the document from the database, initialising all its fields in the process. Will
295             throw an exception if the document cannot be found, or for connection issues. It returns
296             the object.
297              
298             Note that the attachments field if defined will contain stubs and not the full content.
299             Retrieving the actual data is done using C.
300              
301             =item update
302              
303             Same as C but only operates on documents already in the DB.
304              
305             =item delete
306              
307             Deletes the document and resets the object (updating its C). Returns the object (which
308             is still perfectly usable). Throws an exception if the document isn't found, or for
309             connection issues.
310              
311             =item fetchAttachment $NAME
312              
313             Fetches the attachment with the given name and returns its content. Throws an exception if
314             the attachment cannot be retrieved, or if the object had no knowledge of such an attachment.
315              
316             =item addAttachment $NAME, $CONTENT_TYPE, $DATA
317              
318             Adds an attachment to the document with a given name, MIME media type, and data. The
319             data is the original, not the Base64 version which is handled internally. The object
320             is returned.
321              
322             =item deleteAttachment $NAME
323              
324             Deletes an attachment from the document. Note that this only removes the attachment
325             on the client side, you have to update the object for it to be removed from the DB.
326              
327             Throws an exception if the document does not contain an attachment by that name.
328              
329             =item toBase64 $DATA
330              
331             A simple helper that returns data in Base64 of a form acceptable to CouchDB (on a single
332             line).
333              
334             =item retrieveFromRev $REV
335              
336             Fetches a specific revision of a document, and returns it I. This is
337             to avoid destroying your own Doc object. Throws exceptions if it can't connect or find the
338             document.
339              
340             =item revisionsInfo
341              
342             Returns an arrayref or hashresf indicating the C of previous revisions and their
343             C (being C, C, C). Throws exceptions if it can't connect
344             or find the document.
345              
346             =back
347              
348             =head1 TODO
349              
350             Handling of attachments could be improved by not forcing the data into memory at all
351             times. Also, an option to turn the attachments into stubs after they have been saved
352             would be good.
353              
354             =head1 AUTHOR
355              
356             Robin Berjon,
357             Maverick Edwards, (current maintainer)
358              
359             =head1 BUGS
360              
361             Please report any bugs or feature requests to bug-couchdb-client at rt.cpan.org, or through the
362             web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CouchDB-Client.
363              
364             =head1 COPYRIGHT & LICENSE
365              
366             Copyright 2008 Robin Berjon, all rights reserved.
367              
368             This library is free software; you can redistribute it and/or modify it under the same terms as
369             Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may
370             have available.
371              
372             =cut