File Coverage

blib/lib/TiddlyWeb/Resting.pm
Criterion Covered Total %
statement 30 173 17.3
branch 0 80 0.0
condition 0 20 0.0
subroutine 10 24 41.6
pod 7 9 77.7
total 47 306 15.3


line stmt bran cond sub pod time code
1             package TiddlyWeb::Resting;
2              
3 1     1   1325 use strict;
  1         2  
  1         32  
4 1     1   6 use warnings;
  1         2  
  1         51  
5              
6 1     1   866 use URI::Escape;
  1         1551  
  1         68  
7 1     1   457690 use LWP::UserAgent;
  1         181118  
  1         48  
8 1     1   16 use HTTP::Request;
  1         2  
  1         84  
9 1     1   1325 use Class::Field 'field';
  1         25578  
  1         136  
10 1     1   1700 use JSON::XS;
  1         9654  
  1         87  
11              
12 1     1   1131 use Readonly;
  1         5133  
  1         3080  
13              
14             our $VERSION = '0.1';
15              
16             Readonly my $BASE_URI => '';
17             Readonly my %ROUTES => (
18             page => $BASE_URI . '/:type/:ws/tiddlers/:pname',
19             pages => $BASE_URI . '/:type/:ws/tiddlers',
20             revisions => $BASE_URI . '/:type/:ws/pages/:pname/revisions',
21             recipe => $BASE_URI . '/recipes/:ws',
22             recipes => $BASE_URI . '/recipes',
23             bag => $BASE_URI . '/bags/:ws',
24             bags => $BASE_URI . '/bags',
25             search => $BASE_URI . '/search',
26             );
27              
28             field 'workspace';
29             field 'username';
30             field 'password';
31             field 'user_cookie';
32             field 'server';
33             field 'verbose';
34             field 'accept';
35             field 'filter';
36             field 'count';
37             field 'order';
38             field 'query';
39             field 'etag_cache' => {};
40             field 'http_header_debug';
41             field 'response';
42             field 'json_verbose';
43             field 'cookie';
44             field 'agent_string';
45              
46             sub new {
47 0     0 1   my $invocant = shift;
48 0   0       my $class = ref($invocant) || $invocant;
49 0           my $self = {@_};
50             #open($self->{log}, ">wiklog"); # handy with debugging
51 0           return bless $self, $class;
52             }
53              
54             sub get_page {
55 0     0 1   my $self = shift;
56 0           my $pname = shift;
57 0           my $paccept;
58              
59 0 0         if (ref $pname){
60 0           $paccept = $pname->{accept};
61             }
62             else {
63 0           $paccept = $self->accept;
64             }
65              
66 0           $pname = name_to_id($pname);
67 0   0       my $accept = $paccept || 'text/plain';
68              
69 0           my $workspace = $self->workspace;
70 0           my $uri = $self->_make_uri(
71             'page',
72             { pname => $pname, ws => $workspace }
73             );
74 0 0         $uri .= '?verbose=1' if $self->json_verbose;
75              
76 0 0         $accept = 'application/json' if $accept eq 'perl_hash';
77 0           my ( $status, $content, $response ) = $self->_request(
78             uri => $uri,
79             method => 'GET',
80             accept => $accept,
81             );
82              
83 0 0 0       if ( $status == 200 || $status == 404 ) {
84 0           $self->{etag_cache}{$workspace}{$pname} = $response->header('etag');
85 0 0 0       if (($self->accept || '') eq 'perl_hash') {
86 0 0         if ($status == 200) {
87 0           return decode_json($content);
88             } else {
89             # send an empty page
90             return +{
91 0           text => 'Not found',
92             tags => [],
93             modifier => '',
94             modified => '',
95             bag => '',
96             };
97             }
98             }
99 0           return $content;
100             }
101             else {
102 0           die "$status: $content\n";
103             }
104             }
105              
106             sub put_page {
107 0     0 1   my $self = shift;
108 0           my $pname = shift;
109 0           my $page_content = shift;
110              
111 0           my $bag;
112 0           my $type = 'text/plain';
113 0 0         if ( ref $page_content ) {
114 0           $type = 'application/json';
115 0           my $dict = {
116             'text' => $page_content->{text},
117             'tags' => $page_content->{tags},
118             'fields' => $page_content->{fields},
119             };
120 0           $bag = $page_content->{bag};
121 0           $page_content = encode_json($dict);
122             }
123              
124 0           my $workspace = $self->workspace;
125 0           my $uri;
126 0 0         if ($bag) {
127 0           $uri = $self->_make_uri(
128             'page',
129             { pname => $pname, ws => $bag, type => 'bags' }
130             );
131             } else {
132 0           $uri = $self->_make_uri(
133             'page',
134             { pname => $pname, ws => $workspace }
135             );
136             }
137              
138 0           my %extra_opts;
139 0           my $page_id = name_to_id($pname);
140 0 0         if ($bag) {
    0          
141 0 0         if (my $prev_etag = $self->{etag_cache}{"bag:$bag"}{$page_id}) {
142 0           $extra_opts{if_match} = $prev_etag;
143             }
144             } elsif (my $prev_etag = $self->{etag_cache}{"recipe:$workspace"}{$page_id}) {
145 0           $extra_opts{if_match} = $prev_etag;
146             }
147              
148 0           my ( $status, $content ) = $self->_request(
149             uri => $uri,
150             method => 'PUT',
151             type => $type,
152             content => $page_content,
153             %extra_opts,
154             );
155              
156 0 0 0       if ( $status == 204 || $status == 201 ) {
157 0           return $content;
158             }
159             else {
160 0           die "$status: $content\n";
161             }
162             }
163              
164 0     0     sub _name_to_id { name_to_id(@_) }
165 0     0 0   sub name_to_id { return shift; }
166              
167             sub _make_uri {
168 0     0     my $self = shift;
169 0           my $thing = shift;
170 0           my $replacements = shift;
171              
172 0 0         unless ($replacements->{type}) {
173 0           $replacements->{type} = 'recipes';
174             }
175              
176 0           my $uri = $ROUTES{$thing};
177              
178             # REVIEW: tried to do this in on /g go but had issues where
179             # syntax errors were happening...
180 0           foreach my $stub ( keys(%$replacements) ) {
181 0           my $replacement
182             = URI::Escape::uri_escape_utf8( $replacements->{$stub} );
183 0           $uri =~ s{/:$stub\b}{/$replacement};
184             }
185              
186 0           return $uri;
187             }
188              
189             sub get_pages {
190 0     0 1   my $self = shift;
191              
192 0           return $self->_get_things('pages');
193             }
194              
195              
196             sub get_revisions {
197 0     0 1   my $self = shift;
198 0           my $pname = shift;
199              
200 0           return $self->_get_things( 'revisions', pname => $pname );
201             }
202              
203             sub get_search {
204 0     0 0   my $self = shift;
205              
206 0           return $self->_get_things( 'search' );
207             }
208              
209             sub _extend_uri {
210 0     0     my $self = shift;
211 0           my $uri = shift;
212 0           my @extend;
213              
214 0 0         if ( $self->filter ) {
215 0           push (@extend, "select=" . $self->filter);
216             }
217 0 0         if ( $self->query ) {
218 0           push (@extend, "q=" . $self->query);
219             }
220 0 0         if ( $self->order ) {
221 0           push (@extend, "sort=" . $self->order);
222             }
223 0 0         if ( $self->count ) {
224 0           push (@extend, "limit=" . $self->count);
225             }
226 0 0         if (@extend) {
227 0           $uri .= "?" . join(';', @extend);
228             }
229 0           return $uri;
230              
231             }
232              
233             sub _get_things {
234 0     0     my $self = shift;
235 0           my $things = shift;
236 0           my %replacements = @_;
237 0   0       my $accept = $self->accept || 'text/plain';
238              
239 0           my $uri = $self->_make_uri(
240             $things,
241             { ws => $self->workspace, %replacements }
242             );
243 0           $uri = $self->_extend_uri($uri);
244              
245             # Add query parameters from a
246 0 0         if ( exists $replacements{_query} ) {
247 0           my @params;
248 0           for my $q ( keys %{ $replacements{_query} } ) {
  0            
249 0           push @params, "$q=" . $replacements{_query}->{$q};
250             }
251 0 0         if (my $query = join( ';', @params )) {
252 0 0         if ( $uri =~ /\?/ ) {
253 0           $uri .= ";$query";
254             }
255             else {
256 0           $uri .= "?$query";
257             }
258             }
259             }
260              
261 0 0         $accept = 'application/json' if $accept eq 'perl_hash';
262 0           my ( $status, $content ) = $self->_request(
263             uri => $uri,
264             method => 'GET',
265             accept => $accept,
266             );
267              
268 0 0 0       if ( $status == 200 and wantarray ) {
    0          
    0          
    0          
269 0           return ( grep defined, ( split "\n", $content ) );
270             }
271             elsif ( $status == 200 ) {
272 0 0 0       return decode_json($content)
273             if (($self->accept || '') eq 'perl_hash');
274 0           return $content;
275             }
276             elsif ( $status == 404 ) {
277 0           return ();
278             }
279             elsif ( $status == 302 ) {
280 0           return $self->response->header('Location');
281             }
282             else {
283 0           die "$status: $content\n";
284             }
285             }
286              
287             sub get_workspace {
288 0     0 1   my $self = shift;
289 0           my $wksp = shift;
290              
291 0           my $prev_wksp = $self->workspace();
292 0 0         $self->workspace($wksp) if $wksp;
293 0           my $result = $self->_get_things('workspace');
294 0 0         $self->workspace($prev_wksp) if $wksp;
295 0           return $result;
296             }
297              
298             sub get_workspaces {
299 0     0 1   my $self = shift;
300              
301 0           return $self->_get_things('workspaces');
302             }
303              
304             sub _request {
305 0     0     my $self = shift;
306 0           my %p = @_;
307 0           my $ua = LWP::UserAgent->new(agent => $self->agent_string);
308 0           my $server = $self->server;
309 0 0         die "No server defined!\n" unless $server;
310 0           $server =~ s/\/$//;
311 0           my $uri = "$server$p{uri}";
312 0 0         warn "uri: $uri\n" if $self->verbose;
313              
314 0           my $request = HTTP::Request->new( $p{method}, $uri );
315 0 0         if ( $self->user_cookie ) {
316 0           $request->header( 'Cookie' => 'tiddlyweb_user=' . $self->user_cookie );
317             } else {
318 0           $request->authorization_basic( $self->username, $self->password );
319             }
320 0 0         $request->header( 'Accept' => $p{accept} ) if $p{accept};
321 0 0         $request->header( 'Content-Type' => $p{type} ) if $p{type};
322 0 0         $request->header( 'If-Match' => $p{if_match} ) if $p{if_match};
323 0 0         if ($p{method} eq 'PUT') {
324 0           my $content_len = 0;
325 1 0   1   16 $content_len = do { use bytes; length $p{content} } if $p{content};
  1         2  
  1         12  
  0            
  0            
326 0           $request->header( 'Content-Length' => $content_len );
327             }
328              
329 0 0         if (my $cookie = $self->cookie) {
330 0           $request->header('cookie' => $cookie);
331             }
332 0 0         $request->content( $p{content} ) if $p{content};
333 0           $self->response( $ua->simple_request($request) );
334              
335 0 0         if ( $self->http_header_debug ) {
336 1     1   1991 use Data::Dumper;
  1         30842  
  1         1416  
337 0           warn "Code: "
338             . $self->response->code . "\n"
339             . Dumper $self->response->headers;
340             }
341              
342             # We should refactor to not return these response things
343 0           return ( $self->response->code, $self->response->content,
344             $self->response );
345             }
346              
347             =head1 NAME
348              
349             TiddlyWeb::Resting - module for accessing TiddlyWeb HTTP API
350              
351             =head1 SYNOPSIS
352              
353             use TiddlyWeb::Resting;
354             my $Rester = TiddlyWeb::Resting->new(
355             username => $opts{username},
356             password => $opts{password},
357             server => $opts{server},
358             );
359             $Rester->workspace('wikiname');
360             $Rester->get_page('my_page');
361             }
362              
363             =head1 DESCRIPTION
364              
365             C is a module designed to allow remote access
366             to the TiddlyWeb API for use in Perl programs. It is a work in
367             progress, adapting C. It maintains the
368             terms, from Socialtext, of workspace and page, which are translated
369             to recipe and tiddler.
370              
371             =head1 METHODS
372              
373             =head2 new
374              
375             my $Rester = TiddlyWeb::Resting->new(
376             username => $opts{username},
377             password => $opts{password},
378             server => $opts{server},
379             );
380              
381             or
382              
383             my $Rester = TiddlyWeb::Resting->new(
384             user_cookie => $opts{user_cookie},
385             server => $opts{server},
386             );
387              
388             Creates a TiddlyWeb::Resting object for the specified
389             server/user/password, or server/cookie combination.
390              
391             =head2 accept
392              
393             $Rester->accept($mime_type);
394              
395             Sets the HTTP Accept header to ask the server for a specific
396             representation in future requests.
397              
398             Common representations:
399              
400             =over 4
401              
402             =item text/plain
403              
404             =item text/html
405              
406             =item application/json
407              
408             =item text/x-tiddlywiki
409              
410             =back
411              
412             =head2 get_page
413              
414             $Rester->workspace('wikiname');
415             $Rester->get_page('page_name');
416              
417             Retrieves the content of the specified page. Note that
418             the workspace method needs to be called first to specify
419             which workspace to operate on.
420              
421             =head2 put_page
422              
423             $Rester->workspace('wikiname');
424             $Rester->put_page('page_name',$content);
425              
426             Save the content as a page in the wiki. $content can either be a string,
427             which is treated as wikitext, or a hash with the following keys:
428              
429             =over
430              
431             =item text
432              
433             A string which is the page's wiki content or a hash of content
434             plus other stuff.
435              
436             =item tags
437              
438             A list of tags.
439              
440             =item fields
441              
442             A hash of arbitrary key value pairs.
443              
444             =back
445              
446             =head2 get_pages
447              
448             $Rester->workspace('wikiname');
449             $Rester->get_pages();
450              
451             List all pages in the wiki.
452              
453             =head2 get_revisions
454              
455             $Rester->get_revisions($page)
456              
457             List all the revisions of a page.
458              
459             =head2 get_workspace
460              
461             $Rester->get_workspace();
462              
463             Return the metadata about a particular workspace.
464              
465             =head2 get_workspaces
466              
467             $Rester->get_workspaces();
468              
469             List all workspaces on the server
470              
471             =head2 response
472              
473             my $resp = $Rester->response;
474              
475             Return the HTTP::Response object from the last request.
476              
477             =head1 AUTHORS / MAINTAINERS
478              
479             Chris Dent C<< >>
480              
481             Based on work by:
482              
483             Luke Closs C<< >>
484              
485             Shawn Devlin C<< >>
486              
487             Jeremy Stashewsky C<< >>
488              
489             =head2 CONTRIBUTORS
490              
491             Chris Dent
492              
493             Kirsten Jones
494              
495             Michele Berg - get_revisions()
496              
497             =cut
498              
499             1;