File Coverage

blib/lib/WebService/SlimTimer.pm
Criterion Covered Total %
statement 56 59 94.9
branch n/a
condition n/a
subroutine 32 33 96.9
pod n/a
total 88 92 95.6


line stmt bran cond sub pod time code
1 2     2   347193 use strict;
  2         5  
  2         80  
2 2     2   10 use warnings;
  2         5  
  2         81  
3              
4             package WebService::SlimTimer;
5              
6             # ABSTRACT: Provides interface to SlimTimer web service.
7              
8              
9 2     2   9688 use Moose;
  2         1029793  
  2         19  
10 2     2   18778 use MooseX::Method::Signatures;
  2         3189034  
  2         15  
11 2     2   450 use Moose::Util::TypeConstraints;
  2         4  
  2         27  
12 2     2   4887 use MooseX::Types::Moose qw(Int Str);
  2         5  
  2         25  
13              
14 2     2   17621 use LWP::UserAgent;
  2         150000  
  2         76  
15 2     2   1803 use YAML::XS;
  2         6304  
  2         155  
16              
17 2     2   1277 use WebService::SlimTimer::Task;
  2         960  
  2         75  
18 2     2   1630 use WebService::SlimTimer::TimeEntry;
  2         765  
  2         95  
19 2     2   29 use WebService::SlimTimer::Types qw(TimeStamp OptionalTimeStamp);
  2         11  
  2         23  
20              
21             our $VERSION = '0.005'; # VERSION
22             our $DEBUG = 0;
23              
24             has api_key => ( is => 'ro', isa => Str, required => 1 );
25              
26             has user_id => ( is => 'ro', isa => Int, writer => '_set_user_id' );
27             has access_token => ( is => 'ro', isa => Str, writer => '_set_access_token',
28             predicate => 'is_logged_in'
29             );
30              
31             has _user_agent => ( is => 'ro', builder => '_create_ua', lazy => 1 );
32              
33             # Returns just the first line of possibly multiline string passed as argument.
34             sub _first_line
35             {
36 0     0     my $text = shift;
37 0           $text =~ s/\n.*//s;
38 0           return $text;
39             }
40              
41             # Return a string representation of a TimeStamp.
42 2     2   116331 method _format_time(TimeStamp $timestamp) {
43 2     2   199 use DateTime::Format::RFC3339;
  2         5  
  2         110  
44             return DateTime::Format::RFC3339->format_datetime($timestamp)
45             }
46              
47             # Create the LWP object that we use. This is currently trivial but provides a
48             # central point for customizing its creation later.
49 2     2   55717 method _create_ua() {
50             my $ua = LWP::UserAgent->new;
51             return $ua;
52             }
53              
54             # Common part of _request() and _post(): submit the request and check that it
55             # didn't fail.
56 2     2   148856 method _submit($req, Str $error) {
57             my $res = $self->_user_agent->request($req);
58              
59             print DateTime->now() . ": received reply.\n" if $DEBUG;
60             print "Reply contents:\n" . $res->content . "\n" if $DEBUG >= 2;
61              
62             if ( !$res->is_success ) {
63             die "$error: " . $res->status_line
64             }
65              
66             return Load($res->content)
67             }
68              
69             # A helper method for creating and submitting an HTTP request without
70             # any body parameters, e.g. a GET or DELETE.
71 2     2   403818 method _request(Str $method, Str $url, Str :$error!, HashRef :$params) {
72             my $uri = URI->new($url);
73             $uri->query_form(
74             api_key => $self->api_key,
75             access_token => $self->access_token,
76             %$params
77             );
78             my $req = HTTP::Request->new($method, $uri);
79              
80             print DateTime->now() . ": " . _first_line($req->as_string) . ".\n" if $DEBUG;
81              
82             $req->header(Accept => 'application/x-yaml');
83              
84             return $self->_submit($req, $error)
85             }
86              
87             # Another helper for POST and PUT requests.
88 2     2   544545 method _post(Str $method, Str $url, HashRef $params, Str :$error!) {
89             my $req = HTTP::Request->new($method, $url);
90              
91             $params->{'api_key'} = $self->api_key;
92              
93             # POST request is used to log in so we can be called before we have the
94             # access token and need to check for this explicitly.
95             if ( $self->is_logged_in ) {
96             $params->{'access_token'} = $self->access_token;
97             }
98              
99             print DateTime->now() . ": " . _first_line($req->as_string) . ".\n" if $DEBUG;
100              
101             $req->content(Dump($params));
102              
103             print "Parameters:\n" . $req->as_string . "\n" if $DEBUG >= 2;
104              
105             $req->header(Accept => 'application/x-yaml');
106             $req->content_type('application/x-yaml');
107              
108             return $self->_submit($req, $error)
109             }
110              
111              
112             # Provide a simple single-argument ctor instead of default Moose one taking a
113             # hash with all attributes values.
114             around BUILDARGS => sub
115             {
116             die "A single API key argument is required" unless @_ == 3;
117              
118             my ($orig, $class, $api_key) = @_;
119              
120             $class->$orig(api_key => $api_key)
121             };
122              
123              
124 2     2   234760 method login(Str $login, Str $password) {
125             my $res = $self->_post(POST => 'http://slimtimer.com/users/token',
126             { user => { email => $login, password => $password } },
127             error => "Failed to login as \"$login\""
128             );
129              
130             $self->_set_user_id($res->{user_id});
131             $self->_set_access_token($res->{access_token})
132             }
133              
134              
135             # Helper for task-related methods: returns either the root tasks URI or the
136             # URI for the given task if the task id is specified.
137 2     2   129325 method _get_tasks_uri(Int $task_id?) {
138             my $uri = "http://slimtimer.com/users/$self->{user_id}/tasks";
139             if ( defined $task_id ) {
140             $uri .= "/$task_id"
141             }
142              
143             return $uri
144             }
145              
146              
147 2     2   107884 method list_tasks(Bool $include_completed = 1) {
148             my $tasks_entries = $self->_request(GET => $self->_get_tasks_uri,
149             params => {
150             show_completed => $include_completed ? 'yes' : 'no'
151             },
152             error => "Failed to get the tasks list"
153             );
154              
155             # The expected reply structure is an array of hashes corresponding to each
156             # task.
157             my @tasks;
158             for (@$tasks_entries) {
159             push @tasks, WebService::SlimTimer::Task->new(%$_);
160             }
161              
162             return @tasks;
163             }
164              
165              
166 2     2   146567 method create_task(Str $name) {
167             my $res = $self->_post(POST => $self->_get_tasks_uri,
168             { task => { name => $name } },
169             error => "Failed to create task \"$name\""
170             );
171              
172             return WebService::SlimTimer::Task->new($res);
173             }
174              
175              
176 2     2   195514 method delete_task(Int $task_id) {
177             $self->_request(DELETE => $self->_get_tasks_uri($task_id),
178             error => "Failed to delete the task $task_id"
179             );
180             }
181              
182              
183 2     2   208591 method get_task(Int $task_id) {
184             my $res = $self->_request(GET => $self->_get_tasks_uri($task_id),
185             error => "Failed to find the task $task_id"
186             );
187              
188             return WebService::SlimTimer::Task->new($res);
189             }
190              
191              
192 2     2   266739 method complete_task(Int $task_id, TimeStamp $completed_on) {
193             $self->_post(PUT => $self->_get_tasks_uri($task_id),
194             { task => { completed_on => $self->_format_time($completed_on) } },
195             error => "Failed to mark the task $task_id as completed"
196             );
197             }
198              
199              
200              
201             # Helper for time-entry-related methods: returns either the root time entries
202             # URI or the URI for the given entry if the time entry id is specified.
203 2     2   189110 method _get_entries_uri(Int $entry_id?) {
204             my $uri = "http://slimtimer.com/users/$self->{user_id}/time_entries";
205             if ( defined $entry_id ) {
206             $uri .= "/$entry_id"
207             }
208              
209             return $uri
210             }
211              
212             # Common part of list_entries() and list_task_entries()
213 2     2   343303 method _list_entries(
214             Maybe[Int] $taskId,
215             OptionalTimeStamp $start,
216             OptionalTimeStamp $end) {
217             my $uri = defined $taskId
218             ? $self->_get_tasks_uri($taskId) . "/time_entries"
219             : $self->_get_entries_uri;
220              
221             my %params;
222             $params{'range_start'} = $self->_format_time($start) if defined $start;
223             $params{'range_end'} = $self->_format_time($end) if defined $end;
224              
225             my $entries = $self->_request(GET => $uri,
226             params => \%params,
227             error => "Failed to get the entries list"
228             );
229              
230             my @time_entries;
231             for (@$entries) {
232             push @time_entries, WebService::SlimTimer::TimeEntry->new($_);
233             }
234              
235             return @time_entries;
236 2     2   304975 }
237              
238              
239             method list_entries(TimeStamp :$start, TimeStamp :$end) {
240             return $self->_list_entries(undef, $start, $end);
241 2     2   445812 }
242              
243              
244             method list_task_entries(Int $taskId, TimeStamp :$start, TimeStamp :$end) {
245             return $self->_list_entries($taskId, $start, $end);
246 2     2   160646 }
247              
248              
249             method get_entry(Int $entryId) {
250             my $res = $self->_request(GET => $self->_get_entries_uri($entryId),
251             error => "Failed to get the entry $entryId"
252             );
253              
254             return WebService::SlimTimer::TimeEntry->new($res);
255 2     2   395565 }
256              
257              
258             method create_entry(Int $taskId, TimeStamp $start, TimeStamp $end?) {
259             $end = DateTime->now if !defined $end;
260              
261             my $res = $self->_post(POST => $self->_get_entries_uri, {
262             time_entry => {
263             task_id => $taskId,
264             start_time => $self->_format_time($start),
265             end_time => $self->_format_time($end),
266             duration_in_seconds => $end->epoch() - $start->epoch(),
267             }
268             },
269             error => "Failed to create new entry for task $taskId"
270             );
271              
272             return WebService::SlimTimer::TimeEntry->new($res);
273 2     2   344299 }
274              
275              
276             method update_entry(
277             Int $entry_id,
278             Int $taskId,
279             TimeStamp $start,
280             TimeStamp $end) {
281             $self->_post(PUT => $self->_get_entries_uri($entry_id), {
282             time_entry => {
283             task_id => $taskId,
284             start_time => $self->_format_time($start),
285             end_time => $self->_format_time($end),
286             duration_in_seconds => $end->epoch() - $start->epoch(),
287 2     2   178253 }
288             },
289             error => "Failed to update the entry $entry_id"
290             );
291             }
292              
293              
294             method delete_entry(Int $entry_id) {
295             $self->_request(DELETE => $self->_get_entries_uri($entry_id),
296             error => "Failed to delete the entry $entry_id"
297             );
298             }
299              
300             1;
301              
302             __END__
303             =pod
304              
305             =head1 NAME
306              
307             WebService::SlimTimer - Provides interface to SlimTimer web service.
308              
309             =head1 VERSION
310              
311             version 0.005
312              
313             =head1 SYNOPSIS
314              
315             This module provides interface to L<http://www.slimtimer.com/> functionality.
316              
317             Notice that to use it you must obtain an API key by creating an account at
318             SlimTimer web site and then visit L<http://slimtimer.com/help/api>.
319              
320             my $st = WebService::SlimTimer->new($api_key);
321             $st->login('your@email.address', 'secret-password');
322              
323             # Create a brand new task.
324             my $task = $st->create_task('Testing SlimTimer');
325              
326             # Spend 10 minutes on testing.
327             $st->create_entry($task->id, DateTime->now, DateTime->from_epoch(time() + 600));
328              
329             # Mark the task as completed.
330             $st->complete_task($task->id, DateTime->now());
331              
332             # Or maybe even get rid of it now.
333             $st->delete_task($task->id);
334              
335             =head1 METHODS
336              
337             =head2 CONSTRUCTOR
338              
339             The single required constructor argument is the API key required to connect to
340             SlimTimer:
341              
342             my $st = WebService::SlimTimer->new('123456789abcdef123456789abcdef');
343              
344             The validity of the API key is not checked here but using an invalid key will
345             result in a failure to C<login()> later.
346              
347             =head2 login
348              
349             Logs in to SlimTimer using the provided login and password.
350              
351             $st->login('your@email.address', 'secret-password');
352              
353             This method must be called before doing anything else with this object.
354              
355             =head2 list_tasks
356              
357             Returns the list of all tasks involving the logged in user:
358              
359             my @tasks = $st->list_tasks();
360             say join("\n", map { $_->name } @tasks); # Output the names of all tasks
361              
362             By default all tasks are returned, even the completed ones. Passing a false
363             value as parameter excludes the completed tasks:
364              
365             my @active_tasks = $st->list_tasks(0);
366              
367             See L<WebService::SlimTimer::Task> for the details of the returned objects.
368              
369             =head2 create_task
370              
371             Create a new task with the given name and returns the new
372             L<WebService::SlimTimer::Task> object on success.
373              
374             my $task = $st->create_task('Test Task');
375             ... Use $task->id with the other methods ...
376              
377             =head2 delete_task
378              
379             Delete the task with the given id (presumably previously obtained from
380             L<list_tasks>).
381              
382             $st->delete_task($task->id);
383              
384             =head2 get_task
385              
386             Find the given task by its id.
387              
388             my $task = $st->get_task(task_id);
389              
390             While there is no direct way to obtain a task id using this module, it could
391             be cached locally from a previous program execution, for example.
392              
393             =head2 complete_task
394              
395             Mark the task with the given id as being completed.
396              
397             $st->complete_task($task->id, DateTime->now);
398              
399             =head2 list_entries
400              
401             Return all the time entries.
402              
403             If the optional C<start> and/or C<end> parameters are specified, returns only
404             the entries that begin after the start date and/or before the end one.
405              
406             # List all entries: potentially very time-consuming.
407             my @entries = $st->list_entries;
408              
409             # List entries started today.
410             my $today = DateTime->now;
411             $today->set(hour => 0, minute => 0, second => 0);
412             my @today_entries = $st->list_entries(start => $today);
413              
414             # List entries started in 2010.
415             my @entries_2010 = $st->list_entries(
416             start => DateTime->new(year => 2010),
417             end => DateTime->new(year => 2011)
418             );
419              
420             =head2 list_task_entries
421              
422             Return all the time entries for the given task.
423              
424             Just as L<list_entries>, this method accepts optional C<start> and C<end>
425             parameters to restrict the dates of the entries retrieved.
426              
427             my @today_work_on_task = $st->list_entries($task->id, start => $today);
428              
429             =head2 get_entry
430              
431             Find the given time entry by its id.
432              
433             my $entry = $st->get_entry($entry_id);
434              
435             As with C<get_task()>, it only makes sense to use this method if the id comes
436             from a local cache.
437              
438             =head2 create_entry
439              
440             Create a new time entry.
441              
442             my $day_of_work = $st->create_entry($task->id,
443             DateTime->now->set(hour => 9, minute => 0, second = 0),
444             DateTime->now->set(hour => 17, minute => 0, second = 0)
445             );
446              
447             Notice that the time stamps should normally be in UTC and not local time or
448             another time zone.
449              
450             If the C<end> parameter is not specified, it defaults to now.
451              
452             Returns the entry that was created.
453              
454             =head2 update_entry
455              
456             Changes an existing time entry.
457              
458             # Use more realistic schedule.
459             $st->update_entry($day_of_work->id, $task->id,
460             DateTime->now->set(hour => 11, minute => 0, second = 0)
461             DateTime->now->set(hour => 23, minute => 0, second = 0)
462             );
463              
464             =head2 delete_entry
465              
466             Deletes a time entry.
467              
468             $st->delete_entry($day_of_work->id);
469              
470             =head1 VARIABLES
471              
472             This module define C<VERSION> and C<DEBUG> package variables. The first one is
473             self-explanatory, the second one is 0 by default but can be set to 1 to trace
474             all network requests done by this module. Setting it to 2 will also dump the
475             requests (and replies) contents.
476              
477             =head1 SEE ALSO
478              
479             L<WebService::SlimTimer::Task>, L<WebService::SlimTimer::TimeEntry>
480              
481             =head1 BUGS
482              
483             Currently the C<offset> parameter is not used by C<list_tasks> and
484             C<list_entries> and C<list_task_entries> methods, so they are limited to 50
485             tasks for the first one and 5000 entries for the latter two and accessing the
486             subsequent results is impossible.
487              
488             Access to the comments and tags of the tasks and time entries objects is not
489             implemented yet.
490              
491             =head1 AUTHOR
492              
493             Vadim Zeitlin <vz-cpan@zeitlins.org>
494              
495             =head1 COPYRIGHT AND LICENSE
496              
497             This software is copyright (c) 2011 by Vadim Zeitlin.
498              
499             This is free software; you can redistribute it and/or modify it under
500             the same terms as the Perl 5 programming language system itself.
501              
502             =cut
503