File Coverage

blib/lib/WebService/Beeminder.pm
Criterion Covered Total %
statement 35 56 62.5
branch 1 8 12.5
condition n/a
subroutine 14 18 77.7
pod 0 1 0.0
total 50 83 60.2


line stmt bran cond sub pod time code
1             package WebService::Beeminder;
2              
3             # ABSTRACT: Access the Beeminder API
4              
5              
6 4     4   109475 use 5.010;
  4         15  
  4         177  
7 4     4   22 use strict;
  4         8  
  4         141  
8 4     4   31 use warnings;
  4         8  
  4         141  
9 4     4   4911 use MooseX::Method::Signatures;
  4         10424451  
  4         28  
10 4     4   865 use Moose;
  4         9  
  4         36  
11 4     4   34690 use WebService::Beeminder::Types qw(BeeBool Goal User);
  4         19  
  4         41  
12 4     4   12163 use JSON::Any;
  4         23846  
  4         33  
13 4     4   35389 use LWP::UserAgent;
  4         283400  
  4         190  
14 4     4   49 use Carp qw(croak);
  4         7  
  4         1189  
15              
16             our $VERSION = '0.003'; # VERSION: Generated by DZP::OurPkg:Version
17              
18             has 'token' => (isa => 'Str', is => 'ro', required => 1);
19             has 'username'=> (isa => User, is => 'ro', default => 'me');
20             has 'agent' => ( is => 'rw'); # Must act like LWP::UserAgent
21             has 'dryrun' => (isa => 'Bool',is => 'ro', default => 0);
22             has 'apibase' => (isa => 'Str', is => 'ro', default => 'https://www.beeminder.com/api/v1');
23              
24             # Everything needs to be able to read/write JSON.
25             my $json = JSON::Any->new;
26              
27             sub BUILD {
28 1     1 0 4005 my ($self) = @_;
29              
30             # Make sure we have a user-agent, if none provided.
31 1 50       44 if (not $self->agent) {
32 1         33 $self->agent(LWP::UserAgent->new(agent => "perl/$], WebService::Beeminder/" . $self->VERSION));
33             }
34              
35 1         13 return;
36             }
37              
38              
39             # TODO: The 'associations' parameter seems to result in EVERYTHING
40             # being returned, even if it's set to 'false'. As such, the handling
41             # of this is presently disabled.
42              
43 4     4   2356978 method user(
44             User :$user = "me",
45             Str :$goals_filter = "all" where { /^(?:all|frontburner|backburner)$/ },
46             Int :$diff_since,
47             # BeeBool :$associations = 'false' does coerce,
48             BeeBool :$skinny = 'false' does coerce
49             ) {
50              
51             # AFAIK, the $user here is irrelevant, since we can only query
52             # the user we're logged in as. Still, we'll respect it, in
53             # case that changes in the future.
54              
55             return $self->_get(['users',"$user.json"],{
56             # associations => $associations,
57             goals_filter => $goals_filter,
58             diff_since => $diff_since // 'null',
59             skinny => $skinny,
60             });
61 4     4   246332  
62             }
63              
64              
65             # Gets the datapoints for a goal
66 4     4   1313117 # DONE: 2011-11-25. This takes no parameters.
67             method datapoints(Goal $goal) {
68             return $self->_userget( ['goals', $goal, 'datapoints.json']);
69             }
70              
71              
72             method add_datapoint(
73             Goal :$goal!,
74             Int :$timestamp, # TODO: Change to a proper timestamp type.
75             Num :$value!,
76 4     4   663450 Str :$comment = "",
77             BeeBool :$sendmail = 'false' does coerce
78             ) {
79             $timestamp //= time();
80              
81             return $self->_userpost(
82             { timestamp => $timestamp, value => $value, comment => $comment, sendmail => $sendmail },
83 0     0     [ 'goals', $goal, 'datapoints.json' ]
84             );
85 0           }
86              
87 0            
88             method goal(
89 0 0         Goal $goal,
90 0           BeeBool :$datapoints = 'false' does coerce
91             ) {
92             return $self->_userget( [ 'goals', "$goal.json" ], { datapoints => $datapoints });
93 0           }
94              
95             # Posts to the API. Takes a hashref of parameters. Remaining arguments
96             # are interpreted as a path.
97             sub _userpost {
98             my ($self, $params, $path, $options) = @_;
99 0     0      
100             my $url = $self->_path(['users', $self->username, @$path], $options);
101 0            
102             my $resp = $self->agent->post( $url, $params );
103 0 0          
104 0           unless ($resp->is_success) {
105             croak "Failed to fetch $url - ".$resp->status_line;
106             }
107 0            
108 0           return $json->decode($resp->content);
109              
110             };
111 0            
112             # Builds a path, and adds appropriate auth tokens, etc.
113             sub _path {
114             my ($self, $path, $options) = @_;
115              
116             my $url = join('/', $self->apibase, @$path) . "?auth_token=" . $self->token;
117              
118 0     0     if ($self->dryrun) {
119             $url .= "&dryrun=1";
120 0           }
121 0            
122             foreach my $opt (keys %$options) {
123 0 0         $url .= "&$opt=$options->{$opt}"; # TODO: Escape params!
124 0           }
125            
126             return $url;
127 0           }
128              
129             # Fetches something from the API. Automatically prepends the API path,
130             # adds the token to the end, and decodes the JSON.
131              
132 0     0     sub _get {
133             my ($self, $path, $options) = @_;
134 0            
135             my $url = $self->_path($path, $options);
136             my $resp = $self->agent->get( $url );
137              
138             unless ($resp->is_success) {
139             croak "Failed to fetch $url - ".$resp->status_line;
140             }
141              
142             return $json->decode($resp->content);
143             }
144              
145             # As for _get, but prepends 'users' and the current user.
146             sub _userget {
147             my ($self, $args, $options) = @_;
148              
149             return $self->_get([ 'users', $self->username, @$args], $options);
150             }
151              
152             1;
153              
154             __END__
155              
156             =pod
157              
158             =head1 NAME
159              
160             WebService::Beeminder - Access the Beeminder API
161              
162             =head1 VERSION
163              
164             version 0.003
165              
166             =head1 SYNOPSIS
167              
168             my $bee = WebService::Beeminder->new( token => $token );
169              
170             # I flossed my teeth today.
171             $bee->add_datapoint( goal => 'floss', value => 1 );
172              
173             # When did I last take dance lessons?
174             my $result = $bee->datapoints('dance');
175              
176             say "I last went dancing on $result->[0]{timestamp} with a comment of " .
177             $result->[0]{comment};
178              
179             =head1 DESCRIPTION
180              
181             This is a I<thin-ish> wrapper around the Beeminder API. All results are
182             exactly what's returned by the underlying API, with the JSON being
183             converted into Perl data structures.
184              
185             You need a Beeminder API token to use this module. The easiest way
186             to get a personal token is just to login to L<Beeminder|http://beeminder.com/>
187             and then go to L<https://www.beeminder.com/api/v1/auth_token.json>.
188             Copy'n'paste the token into your code (or a config file your code uses),
189             and you're good to go!
190              
191             More information on tokens is available in the
192             L<Beeminder API documentation|http://beeminder.com/api>.
193              
194             =head1 METHODS
195              
196             =head2 user
197              
198             my $result = $bee->user(
199             goals_filter => 'frontburner', # or 'backburner', or 'all' (default)
200             diff_since => $last_check, # Seconds from the epoch. Default: 'null'
201             skinny => 1, # Return slimmed info. Default: 'false'
202             );
203              
204             All arguments are optional.
205              
206             Obtains information about the current user. This returns a user resource
207             (as defined by the Beeminder API), which looks like this:
208              
209             {
210             username => "alice",
211             timezone => "America/Los_Angeles",
212             updated_at => 1343449880,
213             goals => ['gmailzero', 'weight']
214             }
215              
216             If C<diff_since> is specified, then the goals will be a list of hashes,
217             rather than just a simple list of goals.
218              
219             Note that the C<associations> parameter specified in the API is currently
220             not supported as it results in excessively slow server responses, even
221             when set to 'false'.
222              
223             =head2 datapoints
224              
225             my $results = $bee->datapoints($goal);
226              
227             This method returns an array reference of data points for the given goal.
228             At the time of writing, the Beeminder API returns the most recent data
229             point in the first position in the array.
230              
231             [
232             {
233             id => 'abc123'
234             timestamp => 1234567890,
235             value => 1.1,
236             comment => "Frobnicated a widget",
237             updated_at => 1234567890
238             },
239             {
240             id => 'abc124'
241             timestamp => 1234567891,
242             value => 1.2,
243             comment => "Straightened my doohickies",
244             updated_at => 1234567891
245             },
246             ]
247              
248             =head2 add_datapoint
249              
250             my $point = $bee->add_datapoint(
251             goal => 'floss',
252             timestamp => time(), # Optional, defaults to now
253             value => 1,
254             comment => 'Floss every tooth for great justice!',
255             sendmail => 0, # Optional, defaults to false
256             );
257              
258             Adds a data-point to the given goal. Mail will be sent to the user if
259             the C<sendmail> parameter is true.
260              
261             Returns the data-point that was created:
262              
263             {
264             id => 'abc125'
265             timestamp => 1234567892,
266             value => 1,
267             comment => 'Floss every tooth for great justice!'
268             updated_at => 1234567892
269             }
270              
271             =head2 goal
272              
273             my $results = $bee->goal('floss', datapoints => 0);
274              
275             Returns information about a goal. The optional C<datapoints> parameter can be
276             supplied with a true value to also fetch datapoints for that goal.
277              
278             Goal objects are complex data structures, and are described in the
279             L<Beeminder API documentation|https://www.beeminder.com/api#goal>.
280              
281             =head1 INSTALLATION
282              
283             This module presently uses L<MooseX::Method::Signatures>. If you're
284             not experienced in installing module dependencies, it's recommend you
285             use L<App::cpanminus>, which doesn't require any special privileges
286             or software.
287              
288             Perl v5.10.0 or later is required to use this module.
289              
290             =head1 SEE ALSO
291              
292             =over
293              
294             =item *
295              
296             L<The Beeminder API|http://beeminder.com/api>
297              
298             =back
299              
300             =for Pod::Coverage BUILD
301              
302             =head1 AUTHOR
303              
304             Paul Fenwick <pjf@cpan.org>
305              
306             =head1 COPYRIGHT AND LICENSE
307              
308             This software is copyright (c) 2012 by Paul Fenwick.
309              
310             This is free software; you can redistribute it and/or modify it under
311             the same terms as the Perl 5 programming language system itself.
312              
313             =cut