File Coverage

lib/WorldCat/API.pm
Criterion Covered Total %
statement 44 73 60.2
branch 0 6 0.0
condition 1 11 9.0
subroutine 15 20 75.0
pod 0 1 0.0
total 60 111 54.0


line stmt bran cond sub pod time code
1 2     2   143080 use strict;
  2         20  
  2         49  
2 2     2   9 use warnings;
  2         3  
  2         76  
3             package WorldCat::API;
4             $WorldCat::API::VERSION = '1.002';
5             # ABSTRACT: Moo bindings for the OCLC WorldCat API
6              
7              
8 2     2   15 use feature qw(say);
  2         2  
  2         335  
9              
10 2     2   945 use Moo;
  2         17378  
  2         7  
11 2     2   2430 use Carp qw(croak);
  2         3  
  2         70  
12 2     2   875 use Digest::SHA qw(hmac_sha256_base64);
  2         4921  
  2         119  
13 2     2   1381 use HTTP::Request;
  2         34690  
  2         56  
14 2     2   794 use HTTP::Status qw(:constants);
  2         7133  
  2         590  
15 2     2   1067 use LWP::UserAgent;
  2         30445  
  2         55  
16 2     2   870 use MARC::Record;
  2         11150  
  2         75  
17 2     2   914 use Math::Random::Secure qw(irand);
  2         229645  
  2         120  
18 2     2   931 use Readonly;
  2         6073  
  2         83  
19 2     2   627 use WorldCat::MARC::Record::Monkeypatch;
  2         5  
  2         53  
20 2     2   1355 use XML::Simple qw(XMLin);
  2         14045  
  2         1147  
21              
22             Readonly my $DEFAULT_RETRIES => 5;
23              
24             sub _from_env {
25 1     1   3 my ($attr) = @_;
26 1   50     17 return $ENV{uc "WORLDCAT_API_$attr"} // die "Attribute $attr is required";
27             }
28              
29             has institution_id => (
30             is => 'ro',
31             required => 1,
32             default => sub { _from_env('institution_id') },
33             );
34              
35             has principle_id => (
36             is => 'ro',
37             required => 1,
38             default => sub { _from_env('principle_id') },
39             );
40              
41             has principle_id_namespace => (
42             is => 'ro',
43             required => 1,
44             default => sub { _from_env('principle_id_namespace') },
45             );
46              
47             has secret => (
48             is => 'ro',
49             required => 1,
50             default => sub { _from_env('secret') },
51             );
52              
53             has wskey => (
54             is => 'ro',
55             required => 1,
56             default => sub { _from_env('wskey') },
57             );
58              
59             sub _query_param {
60 0     0     return "$_[0]=\"$_[1]\"";
61             }
62              
63             # OCLC returns encoding=UTF-8, format=MARC21+xml.
64             sub find_by_oclc_number {
65 0     0 0   my ($self, $oclc_number, %opts) = @_;
66              
67 0   0       my $retries = $opts{retries} // $DEFAULT_RETRIES;
68              
69             # Fetch the record with retries and exponential backoff
70 0           my $res;
71 0           my $ua = $self->_new_ua;
72 0           for my $try (0..($retries - 1)) {
73 0           $res = $ua->get("https://worldcat.org/bib/data/$oclc_number");
74 0           say "Got HTTP Response Code: @{[$res->code]}";
  0            
75              
76 0 0         last if not $res->is_server_error; # only retry 5xx errors
77 0           sleep 2 ** $try;
78             }
79              
80             # Return MARC::Record on success
81 0 0         if ($res->is_success) {
82 0           my $xml = XMLin($res->decoded_content)->{entry}{content}{record};
83 0           return MARC::Record->new_from_marc21xml($xml);
84             }
85              
86             # Return nil if record not found
87 0 0         return if $res->code eq HTTP_NOT_FOUND;
88              
89             # An error occurred, throw the response
90 0           croak $res;
91             }
92              
93             # Generate the authorization header. It's complicated; see the docs:
94             #
95             # https://www.oclc.org/developer/develop/authentication/hmac-signature.en.html
96             # https://github.com/geocolumbus/hmac-language-examples/blob/master/perl/hmacAuthenticationExample.pl
97             sub _create_auth_header {
98 0     0     my ($self) = @_;
99              
100 0           my $signature = $self->_create_signature;
101              
102             return 'http://www.worldcat.org/wskey/v2/hmac/v1 ' . join(q{,},
103             _query_param(clientId => $self->wskey),
104             _query_param(principalID => $self->principle_id),
105             _query_param(principalIDNS => $self->principle_id_namespace),
106             _query_param(nonce => $signature->{nonce}),
107             _query_param(signature => $signature->{value}),
108 0           _query_param(timestamp => $signature->{timestamp}),
109             );
110             }
111              
112             sub _create_signature {
113 0     0     my ($self, %opts) = @_;
114              
115 0   0       my $nonce = $opts{nonce} || sprintf q{%x}, irand;
116 0   0       my $timestamp = $opts{timestamp} || time;
117              
118 0           my $signature = hmac_sha256_base64(join(qq{\n},
119             $self->wskey,
120             $timestamp,
121             $nonce,
122             q{}, # Hash of the body; empty because we're just GET-ing
123             "GET", # all-caps HTTP request method
124             "www.oclc.org",
125             "443",
126             "/wskey",
127             q{}, # query params
128             ), $self->secret) . q{=};
129              
130             return {
131 0           value => $signature,
132             nonce => $nonce,
133             timestamp => $timestamp,
134             };
135             }
136              
137             sub _new_ua {
138 0     0     my ($self) = @_;
139              
140 0           my $ua = LWP::UserAgent->new;
141 0           $ua->default_header(Accept => q{application/atom+xml;content="application/vnd.oclc.marc21+xml"});
142 0           $ua->default_header(Authorization => $self->_create_auth_header);
143 0           return $ua;
144             }
145              
146             1;
147              
148             __END__