File Coverage

blib/lib/WebService/ScormCloud/Service.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package WebService::ScormCloud::Service;
2              
3 1     1   1995 use Moose::Role;
  0            
  0            
4              
5             =head1 NAME
6              
7             WebService::ScormCloud::Service - ScormCloud API base class
8              
9             =head1 VERSION
10              
11             Version 0.03
12              
13             =cut
14              
15             our $VERSION = '0.03';
16              
17             =head1 SYNOPSIS
18              
19             use WebService::ScormCloud;
20              
21             my $ScormCloud = WebService::ScormCloud->new(
22             app_id => '12345678',
23             secret_key => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
24             );
25              
26             print "Service is alive\n" if $ScormCloud->ping;
27              
28             print "Auth is valid\n" if $ScormCloud->authPing;
29              
30             =head1 DESCRIPTION
31              
32             This module defines L<WebService::ScormCloud> shared API methods.
33             See L<WebService::ScormCloud> for more info.
34              
35             =cut
36              
37             use Carp;
38             use Data::Dump 'dump';
39             use Digest::MD5 qw(md5_hex);
40             use HTTP::Request::Common;
41             use POSIX qw(strftime);
42             use Try::Tiny;
43             use XML::Simple;
44              
45             use Readonly;
46             Readonly::Scalar my $DUMP_WIDTH => 40;
47              
48             =head1 METHODS
49              
50             =head2 request_uri ( I<params> )
51              
52             Returns a URI object that would be used to make a ScormCloud API
53             request.
54              
55             Note that you would not typically call this method directly - use
56             the API methods defined in the L</API CLASSES> instead.
57              
58             The params hashref should contain all query params and values used
59             in building the request query string. At minimum it must include a
60             value for "method".
61              
62             =cut
63              
64             sub request_uri
65             {
66             my ($self, $params) = @_;
67              
68             $params ||= {};
69              
70             croak 'No method' unless $params->{method};
71              
72             my $top_level_namespace = $self->top_level_namespace;
73             unless ($params->{method} =~ /^$top_level_namespace[.]/xsm)
74             {
75             $params->{method} = $top_level_namespace . q{.} . $params->{method};
76             }
77              
78             $params->{appid} ||= $self->app_id;
79              
80             $params->{ts} ||= strftime '%Y%m%d%H%M%S', gmtime;
81              
82             my $sig = join q{}, map { $_ . $params->{$_} } sort keys %{$params};
83             $params->{sig} = md5_hex($self->secret_key . $sig);
84              
85             my $uri = $self->service_url->clone;
86             $uri->query_form($params);
87              
88             $self->_dump_data($uri . q{}) if $self->dump_request_url;
89              
90             return $uri;
91             }
92              
93             =head2 request ( I<params> [ , I<args> ] )
94              
95             Make an API request:
96              
97             my $parsed_response_data =
98             $ScormCloud->request(method => 'rustici.debug.authPing');
99              
100             Note that you would not typically call this method directly - use
101             the API methods defined in the L</API CLASSES> instead.
102              
103             =cut
104              
105             sub request
106             {
107             my ($self, $params, $args) = @_;
108              
109             my $uri = $self->request_uri($params);
110              
111             $args ||= {};
112             $args->{request_method} ||= 'GET';
113             $args->{request_headers} ||= {};
114             $args->{xml_parser} ||= {};
115              
116             my %request_args = %{$args->{request_headers}};
117              
118             # If set, "request_content" should be a listref. E.g. for a
119             # file upload:
120             #
121             # $args->{request_content} = [file => ['/path/to/file']];
122             #
123             if ($args->{request_content})
124             {
125             $args->{request_method} = 'POST';
126             $request_args{Content_Type} = 'form-data';
127             $request_args{Content} = $args->{request_content};
128             }
129              
130             my $http_request;
131             {
132             no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
133             $http_request = $args->{request_method}->($uri, %request_args);
134             }
135              
136             return $self->_make_http_request($http_request, $args);
137             }
138              
139             sub _make_http_request
140             {
141             my ($self, $http_request, $args) = @_;
142              
143             my $response = $self->lwp_user_agent->request($http_request);
144              
145             $self->last_error_data([]);
146              
147             my $response_data = undef;
148              
149             if ($response->is_success)
150             {
151             try
152             {
153             $self->_dump_data($response->content) if $self->dump_response_xml;
154              
155             # Add some extra handling in case we get an error response:
156             #
157             my $force_array = delete $args->{xml_parser}->{ForceArray} || [];
158             my $group_tags = delete $args->{xml_parser}->{GroupTags} || {};
159             push @{$force_array}, 'err', 'tracetext';
160             $group_tags->{stacktrace} = 'tracetext';
161              
162             $response_data =
163             XML::Simple->new->XMLin(
164             $response->content,
165             KeyAttr => [],
166             SuppressEmpty => q{},
167             ForceArray => $force_array,
168             GroupTags => $group_tags,
169             %{$args->{xml_parser}}
170             ) || {};
171              
172             # Response data should always include "stat". Make sure it
173             # exists, so callers can safely assume it is always there:
174             #
175             $response_data->{stat} ||= 'fail';
176             }
177             catch
178             {
179             $response_data = {
180             stat => 'fail',
181             err => [
182             {
183             code => 999,
184             msg => 'XML PARSE FAILURE: ' . $_
185             }
186             ]
187             };
188             };
189             }
190             else
191             {
192             $response_data = {
193             stat => 'fail',
194             err => [
195             {
196             code => 999,
197             msg => 'BAD HTTP RESPONSE: ' . $response->status_line
198             }
199             ]
200             };
201             }
202              
203             if ($response_data->{stat} eq 'fail')
204             {
205             $response_data->{err} ||= [{code => 999, msg => 'FAIL BUT NO ERR'}];
206              
207             $self->last_error_data($response_data->{err});
208              
209             croak "Invalid API response data:\n" . dump($response_data)
210             if $self->die_on_bad_response;
211             }
212              
213             $self->_dump_data($response_data) if $self->dump_response_data;
214              
215             return $response_data;
216             }
217              
218             sub _dump_data
219             {
220             my ($self, $data) = @_;
221              
222             print q{=} x $DUMP_WIDTH, "\n", dump($data), "\n", q{=} x $DUMP_WIDTH, "\n";
223              
224             return;
225             }
226              
227             =head2 process_request ( I<params>, I<callback> )
228              
229             Make an API request, and return desired data out of the response.
230              
231             Input arguments are:
232              
233             =over 4
234              
235             =item B<params>
236              
237             A hashref of API request params. At minimum must include "method".
238              
239             =item B<callback>
240              
241             A callback function that extracts and returns the desired data from
242             the response data. The callback should expect a single argument
243             "response" which is the parsed XML response data.
244              
245             =item B<args>
246              
247             An optional hashref of arguments to modify the request.
248              
249             =back
250              
251             =cut
252              
253             sub process_request
254             {
255             my ($self, $params, $callback, $args) = @_;
256              
257             croak 'Missing request params' unless $params;
258             croak 'Missing callback' unless $callback;
259              
260             my $response_data = $self->request($params, $args);
261              
262             my $data = undef;
263              
264             if ($response_data->{stat} eq 'ok')
265             {
266             try
267             {
268             $data = $callback->($response_data);
269             };
270             }
271              
272             unless (defined $data)
273             {
274             croak "Invalid API response data:\n" . dump($response_data)
275             if $self->die_on_bad_response;
276             }
277              
278             $self->_dump_data($data) if $self->dump_api_results;
279              
280             return $data;
281             }
282              
283             1;
284              
285             __END__
286              
287             =head1 SEE ALSO
288              
289             L<WebService::ScormCloud>
290              
291             =head1 AUTHOR
292              
293             Larry Leszczynski, C<< <larryl at cpan.org> >>
294              
295             =head1 BUGS
296              
297             Please report any bugs or feature requests to C<bug-scormcloud at rt.cpan.org>, or through
298             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=WebService-ScormCloud>. I will be notified, and then you'll
299             automatically be notified of progress on your bug as I make changes.
300              
301             Patches more than welcome, especially via GitHub:
302             L<https://github.com/larryl/ScormCloud>
303              
304             =head1 SUPPORT
305              
306             You can find documentation for this module with the perldoc command.
307              
308             perldoc WebService::ScormCloud::Service
309              
310             You can also look for information at:
311              
312             =over 4
313              
314             =item * GitHub
315              
316             L<https://github.com/larryl/ScormCloud>
317              
318             =item * RT: CPAN's request tracker
319              
320             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=WebService-ScormCloud>
321              
322             =item * AnnoCPAN: Annotated CPAN documentation
323              
324             L<http://annocpan.org/dist/WebService-ScormCloud>
325              
326             =item * CPAN Ratings
327              
328             L<http://cpanratings.perl.org/d/WebService-ScormCloud>
329              
330             =item * Search CPAN
331              
332             L<http://search.cpan.org/dist/WebService-ScormCloud/>
333              
334             =back
335              
336             =head1 ACKNOWLEDGEMENTS
337              
338              
339             =head1 COPYRIGHT & LICENSE
340              
341             Copyright 2010 Larry Leszczynski.
342              
343             This program is free software; you can redistribute it and/or modify it
344             under the terms of either: the GNU General Public License as published
345             by the Free Software Foundation; or the Artistic License.
346              
347             See http://dev.perl.org/licenses/ for more information.
348              
349             =cut
350