borrow_direct 1.1.0 → 1.2.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +5 -13
  2. data/.travis.yml +6 -0
  3. data/Gemfile +2 -0
  4. data/README.md +19 -6
  5. data/bd_metrics/finditem_measure.rb +8 -1
  6. data/borrow_direct.gemspec +1 -1
  7. data/lib/borrow_direct/authentication.rb +16 -8
  8. data/lib/borrow_direct/defaults.rb +4 -1
  9. data/lib/borrow_direct/encryption.rb +34 -0
  10. data/lib/borrow_direct/error.rb +17 -0
  11. data/lib/borrow_direct/find_item.rb +32 -18
  12. data/lib/borrow_direct/pickup_location.rb +32 -0
  13. data/lib/borrow_direct/request.rb +30 -8
  14. data/lib/borrow_direct/request_item.rb +15 -7
  15. data/lib/borrow_direct/request_query.rb +18 -4
  16. data/lib/borrow_direct/version.rb +1 -1
  17. data/test/authentication_test.rb +40 -12
  18. data/test/encryption_test.rb +58 -0
  19. data/test/find_item_test.rb +66 -11
  20. data/test/request_item_test.rb +65 -8
  21. data/test/request_query_test.rb +23 -27
  22. data/test/request_test.rb +40 -27
  23. data/test/test_helper.rb +10 -1
  24. data/test/vcr_cassettes/Authentication/Makes_a_request_succesfully.yml +11 -23
  25. data/test/vcr_cassettes/Authentication/Raises_for_bad_library_symbol.yml +12 -23
  26. data/test/vcr_cassettes/Authentication/Raises_for_bad_patron_barcode.yml +12 -24
  27. data/test/vcr_cassettes/Authentication/get_auth_id/Raises_for_bad_api_key.yml +41 -0
  28. data/test/vcr_cassettes/Authentication/get_auth_id/raises_for_a_bad_library_symbol.yml +12 -23
  29. data/test/vcr_cassettes/Authentication/get_auth_id/raises_for_a_bad_patron_barcode.yml +12 -24
  30. data/test/vcr_cassettes/Authentication/get_auth_id/returns_an_auth_id_for_a_good_request.yml +11 -23
  31. data/test/vcr_cassettes/Authentication/get_auth_id/returns_auth_id_with_API_key_from_defaults.yml +40 -0
  32. data/test/vcr_cassettes/Authentication/raw_request_to_verify_HTTP_api/works.yml +11 -23
  33. data/test/vcr_cassettes/FindItem/_find_item_request/Raises_with_bad_api_key.yml +41 -0
  34. data/test/vcr_cassettes/FindItem/_find_item_request/finds_a_locally_available_item.yml +47 -18
  35. data/test/vcr_cassettes/FindItem/_find_item_request/finds_a_requestable_item.yml +66 -18
  36. data/test/vcr_cassettes/FindItem/_find_item_request/finds_an_item_that_does_not_exist_in_BD.yml +66 -19
  37. data/test/vcr_cassettes/FindItem/_find_item_request/raises_proper_error_on_bad_AID.yml +40 -0
  38. data/test/vcr_cassettes/FindItem/_find_item_request/uses_manually_set_auth_id.yml +97 -0
  39. data/test/vcr_cassettes/FindItem/_find_item_request/with_expected_error_PUBFI002/returns_result.yml +46 -9
  40. data/test/vcr_cassettes/FindItem/_find_item_request/works_with_multiple_values.yml +66 -18
  41. data/test/vcr_cassettes/FindItem/find_with_Response/_pickup_location_data/returns_array_of_PickupLocations.yml +97 -0
  42. data/test/vcr_cassettes/FindItem/find_with_Response/has_an_auth_id.yml +66 -18
  43. data/test/vcr_cassettes/FindItem/find_with_Response/has_nil_pickup_locations_when_BD_doesn_t_want_to_give_us_them.yml +46 -9
  44. data/test/vcr_cassettes/FindItem/find_with_Response/has_pickup_locations.yml +66 -18
  45. data/test/vcr_cassettes/FindItem/find_with_Response/knows_locally_available_.yml +47 -18
  46. data/test/vcr_cassettes/FindItem/find_with_Response/not_requestable_for_item_that_BD_returns_PUBFI002.yml +46 -9
  47. data/test/vcr_cassettes/FindItem/find_with_Response/not_requestable_for_item_that_does_not_exist_in_BD.yml +46 -19
  48. data/test/vcr_cassettes/FindItem/find_with_Response/not_requestable_for_item_that_no_libraries_will_lend.yml +275 -18
  49. data/test/vcr_cassettes/FindItem/find_with_Response/not_requestable_for_locally_available_item.yml +47 -18
  50. data/test/vcr_cassettes/FindItem/find_with_Response/requestable_for_requestable_item.yml +66 -18
  51. data/test/vcr_cassettes/FindItem/find_with_Response/requestable_with_multiple_items_if_at_least_one_is_requestable.yml +66 -18
  52. data/test/vcr_cassettes/Request/authentication_id/automatically_fetches_one_when_needed.yml +47 -22
  53. data/test/vcr_cassettes/Request/authentication_id/can_refetch_when_instructed.yml +47 -22
  54. data/test/vcr_cassettes/Request/authentication_id/manually_set_one_will_be_used_without_fetch.yml +40 -0
  55. data/test/vcr_cassettes/Request/authentication_id/starts_out_nil.yml +40 -0
  56. data/test/vcr_cassettes/Request/authentication_id/takes_with_auth_id.yml +40 -0
  57. data/test/vcr_cassettes/Request/can_make_a_succesful_request_with_AID.yml +97 -0
  58. data/test/vcr_cassettes/Request/gets_BD_error_info.yml +49 -12
  59. data/test/vcr_cassettes/Request/gets_BD_error_info_from_a_bad_AID.yml +77 -0
  60. data/test/vcr_cassettes/Request/raises_exception_on_timeout_live.yml +40 -0
  61. data/test/vcr_cassettes/Request/raises_on_bad_path.yml +61 -24
  62. data/test/vcr_cassettes/Request/raises_on_bad_request_hash.yml +50 -35
  63. data/test/vcr_cassettes/Request/uses_timeout_for_HttpClient.yml +40 -0
  64. data/test/vcr_cassettes/Request/with_expected_errors/still_returns_result.yml +47 -10
  65. data/test/vcr_cassettes/RequestItem/make_request/make_request_for_a_locally_available_item.yml +21 -33
  66. data/test/vcr_cassettes/RequestItem/make_request/make_request_for_a_requestable_item.yml +20 -32
  67. data/test/vcr_cassettes/RequestItem/make_request/make_request_for_an_unrequestable_item.yml +21 -33
  68. data/test/vcr_cassettes/RequestItem/make_request/says_no_for_item_that_BD_returns_PUBRI003.yml +77 -0
  69. data/test/vcr_cassettes/RequestItem/make_request/sets_an_auth_id.yml +77 -0
  70. data/test/vcr_cassettes/RequestItem/make_request_/raises_for_unrequestable.yml +21 -33
  71. data/test/vcr_cassettes/RequestItem/make_request_/returns_number_for_succesful_request.yml +20 -32
  72. data/test/vcr_cassettes/RequestItem/raises_proper_error_on_bad_AID.yml +40 -0
  73. data/test/vcr_cassettes/RequestItem/raw_RequestItem_sanity_check.yml +134 -0
  74. data/test/vcr_cassettes/RequestItem/raw_requests_an_unrequestable_item.yml +21 -33
  75. data/test/vcr_cassettes/RequestItem/uses_manually_set_auth_id.yml +20 -32
  76. data/test/vcr_cassettes/RequestItem/with_pickup_location_and_requestable_item/works_with_String_pickup_location.yml +78 -0
  77. data/test/vcr_cassettes/RequestItem/with_pickup_location_and_requestable_item/works_with_structured_PickupLocation.yml +77 -0
  78. data/test/vcr_cassettes/RequestQuery/raises_proper_error_on_bad_AID.yml +40 -0
  79. data/test/vcr_cassettes/RequestQuery/raw_request_query_request/returns_results.yml +117 -325
  80. data/test/vcr_cassettes/RequestQuery/raw_request_to_verify_the_BD_HTTP_API.yml +30 -325
  81. data/test/vcr_cassettes/RequestQuery/requests/fetches_default_records.yml +117 -328
  82. data/test/vcr_cassettes/RequestQuery/requests/fetches_full_records.yml +148 -425
  83. metadata +67 -38
  84. data/test/vcr_cassettes/Authentication/raw_request_to_verify_HTTP_api/.yml +0 -52
  85. data/test/vcr_cassettes/FindItem/find_with_Response/has_nil_auth_id_when_BD_doesn_t_want_to_give_us_one.yml +0 -40
  86. data/test/vcr_cassettes/Request/can_make_a_succesful_request.yml +0 -49
  87. data/test/vcr_cassettes/RequestItem/make_request/raises_for_unrequestable.yml +0 -91
  88. data/test/vcr_cassettes/RequestItem/make_request/returns_number_for_succesful_request.yml +0 -89
  89. data/test/vcr_cassettes/RequestItem/make_request/says_no_for_item_that_BD_returns_PUBRI004.yml +0 -89
  90. data/test/vcr_cassettes/RequestItem/with_pickup_location_and_requestable_item/still_works.yml +0 -90
  91. data/test/vcr_cassettes/top_level_describe/an_inner_describe/.yml +0 -76
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- Zjk4M2VkNzA1NWRmNjY5YmNkOTYyYTcyMmUxMzA5ZDkzZWQwNWU3OA==
5
- data.tar.gz: !binary |-
6
- ODVhYjgxMjhkZTA1MjhjYTNlOTE1OWEwN2Y5NDVlYmZmM2M3ZjQxOA==
2
+ SHA1:
3
+ metadata.gz: f894588c25613d6e9423de67e272784e910b6c8a
4
+ data.tar.gz: f75996b16e159cf14fe0a7454447fd13bc8a1253
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MTA0MTE0NWFjMTViZjgyMmU1MWI0MGQyZTdiYmNkY2RhNTkxNTE2OWY5ZGM3
10
- ZjU2M2FhYzg3ODQzYmExNzViMTI0Y2UwYjQzYjJlYmNiYmYyMmI3NDBjYTIy
11
- OWRiYzdiM2M5MGFjZDRmOTkxNWQ2ZWNhMjhkN2ZhYjQ4M2VmOTc=
12
- data.tar.gz: !binary |-
13
- YTA4NzMxODE4OTgzMDg2ZmZjN2I0NTE0MmM1NjZlOGQwZjRiMmJkNDhjMzg3
14
- Yjc0ZTg3N2I5OWYwMjQwNGRkNDc1YzIxMjk4YmRmMjcxZjY4YWNjNGM0MzIx
15
- ZmVhYjc2OGU5NGY0NzBjMWY2ZDdhODU5MzM3NDU0NzUyMjBjZDE=
6
+ metadata.gz: b21a6e68e0bd31b355da430b08b0ff0dea9e46bbb660ea8741c0b6a041045261b3f89ce426e0a2377b2a25aefb88edbc2f1056f9658f7d3dcdd3b0bc168899f0
7
+ data.tar.gz: 579a230114fb40f0661d25f02ba15be1d366ad1a7e534abe8e134f1aadf1e7c54c19fbe82710372e3783e047d708f520e38ec58776f11a54838907771a6aaa99
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.2.3
5
+ - 1.9.3
6
+ cache: bundler
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in borrow_direct.gemspec
4
4
  gemspec
5
+
6
+ # gem 'byebug', :platforms => [:mri_20, :mri_21, :mri_22]
data/README.md CHANGED
@@ -14,6 +14,14 @@ May also work with other Relais D2D setups with configuration or changes, no ide
14
14
  Some configuration at boot, perhaps in a Rails initializer:
15
15
 
16
16
  ~~~ruby
17
+ # REQUIRED: Set your BD api_key
18
+ BorrowDirect::Defaults.api_key = "your bd api key"
19
+
20
+ # Or, you likely have a different api key for production and
21
+ # testing/dev, if in Rails this is one way to handle that:
22
+ BorrowDirect::Defaults.api_key = Rails.env.production? ? "production_bd_api" : "dev_bd_api"
23
+
24
+
17
25
  # Uses BD Test system by defualt, if you want to use production system instead
18
26
  BorrowDirect::Defaults.api_base = BorrowDirect::Defaults::PRODUCTION_API_BASE
19
27
 
@@ -71,13 +79,13 @@ BorrowDirect::RequestQuery.new(patron_barcode).requests("open")
71
79
 
72
80
  ### AuthID's
73
81
 
74
- For BD api that requires an AuthorizationID (RequestItem and RequestQuery), our ruby
75
- API still accepts a barcode. The ruby code will make a separate request to retrieve
76
- the AuthorizationID behind the scenes.
82
+ All BD API's will requires an AuthorizationID as of late summer/fall 2015.
83
+ Our ruby API still accepts a barcode/library symbol pair instead, with both values possibly
84
+ coming from configured local deaults. The ruby code will make a separate request to retrieve
85
+ the AuthorizationID behind the scenes, so it can use it.
77
86
 
78
- If you already have an AuthorizationID, you can set it to avoid this, but at the moment
79
- we have no code to rescue from expired authorization ID's (and if we did, depending on
80
- how often they expire, it might be less efficient than simply requesting a new one)
87
+ If you already have an AuthorizationID, you can set pass it in to re-use, and avoid
88
+ the extra call, using #with_auth_id on any BD API request.
81
89
 
82
90
  ~~~ruby
83
91
  response = BorrowDirect::FindItem.new(patron_barcode).find(:isbn => isbn)
@@ -86,6 +94,11 @@ auth_id = response.auth_id
86
94
  BorrowDirect::RequestItem.new(patron_barcode).with_auth_id(auth_id).make_request(pickup_location, :isbn => isbn)
87
95
  ~~~
88
96
 
97
+ If you pass in an expired or bad AID, we should raise a BorrowDirect::InvalidAidError.
98
+ (Some unpredictability and inconsistency in remote system error messages may
99
+ prevent us from catching and classing as an InvalidAidError, if you notice report
100
+ and we'll try to fix or report upstream.)
101
+
89
102
  ### Generate a query into BorrowDirect
90
103
 
91
104
  Sometimes you may want to send the user to specific search results inside the standard BorrowDirect HTML interface. We include a helper class for generating such queries.
@@ -8,6 +8,13 @@ require 'borrow_direct'
8
8
  require 'borrow_direct/find_item'
9
9
  require 'date'
10
10
 
11
+ if ENV["ENV"].upcase == "PRODUCTION"
12
+ BorrowDirect::Defaults.api_base = BorrowDirect::Defaults::PRODUCTION_API_BASE
13
+ puts "BD PRODUCTION: #{BorrowDirect::Defaults::PRODUCTION_API_BASE}"
14
+ end
15
+
16
+ BorrowDirect::Defaults.api_key = ENV['BD_API_KEY']
17
+
11
18
  key = "isbn"
12
19
  sourcefile = ARGV[0] || File.expand_path("../isbn-bd-test-200.txt", __FILE__)
13
20
 
@@ -19,7 +26,7 @@ timeout = 20
19
26
  # wait one to 7 minutes.
20
27
  delay = 60..420
21
28
 
22
- puts "#{ENV['BD_LIBRARY_SYMBOL']}: #{key}: #{sourcefile}: #{Time.now.localtime}"
29
+ puts "#{ENV['BD_LIBRARY_SYMBOL']}: #{key}: #{sourcefile}: #{Time.now.localtime}: Testing at #{BorrowDirect::Defaults.api_base}"
23
30
 
24
31
  identifiers = File.readlines(sourcefile) #.shuffle
25
32
 
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Jonathan Rochkind"]
10
10
  spec.email = ["jonathan@dnil.net"]
11
11
  spec.summary = %q{Ruby tools for interacting with the Borrow Direct consortial services}
12
- spec.homepage = ""
12
+ spec.homepage = "https://github.com/jrochkind/borrow_direct"
13
13
  spec.license = "MIT"
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
@@ -9,16 +9,23 @@ module BorrowDirect
9
9
  class Authentication < Request
10
10
  attr_reader :patron_barcode, :patron_library_symbol
11
11
 
12
- @@api_path = "/portal-service/user/authentication/patron"
12
+ @@api_path = "/portal-service/user/authentication"
13
13
 
14
14
  # BorrowDirect::Authentication.new(barcode)
15
15
  # BorrowDirect::Authentication.new(barcode, library_symbol)
16
16
  def initialize(patron_barcode,
17
- patron_library_symbol = Defaults.library_symbol)
17
+ patron_library_symbol = Defaults.library_symbol,
18
+ api_key = Defaults.api_key)
18
19
  super(@@api_path)
19
20
 
20
21
  @patron_barcode = patron_barcode
21
22
  @patron_library_symbol = patron_library_symbol
23
+
24
+ @api_key = api_key
25
+
26
+ unless @api_key
27
+ raise ArgumentError, "BorrowDirect::Authentication requires an api key as third paramter or set in BorrowDirect::Defaults.api_key"
28
+ end
22
29
  end
23
30
 
24
31
  # Returns raw Hash results of the Authentication request
@@ -34,8 +41,8 @@ module BorrowDirect
34
41
  def get_auth_id
35
42
  response = authentication_request
36
43
 
37
- if response["Authentication"] && response["Authentication"]["AuthnUserInfo"] && response["Authentication"]["AuthnUserInfo"]["AId"]
38
- return response["Authentication"]["AuthnUserInfo"]["AId"]
44
+ if response["AuthorizationId"]
45
+ return response["AuthorizationId"]
39
46
  else
40
47
  raise BorrowDirect::Error.new("Could not obtain AId from Authorization API call: #{response.inspect}")
41
48
  end
@@ -43,10 +50,11 @@ module BorrowDirect
43
50
 
44
51
  def authentication_request_hash(patron_barcode, library_symbol)
45
52
  {
46
- "AuthenticationInformation" => {
47
- "LibrarySymbol" => library_symbol,
48
- "PatronId" => patron_barcode
49
- }
53
+ "ApiKey" => @api_key,
54
+ "PartnershipId" => BorrowDirect::Defaults.partnership_id,
55
+ "UserGroup" => "patron",
56
+ "LibrarySymbol" => library_symbol,
57
+ "PatronId" => patron_barcode
50
58
  }
51
59
  end
52
60
 
@@ -13,7 +13,7 @@ module BorrowDirect
13
13
  # To set a default generic patron barcode to use for FindItem requests
14
14
  # BorrowDirect::Defaults.find_item_patron_barcode = "99999999999"
15
15
  class Defaults
16
- TEST_API_BASE = "https://bdtest.relais-host.com/"
16
+ TEST_API_BASE = "https://rc.relais-host.com/"
17
17
  PRODUCTION_API_BASE = "https://borrow-direct.relais-host.com/"
18
18
 
19
19
  TEST_HTML_BASE = "https://bdtest.relaisd2d.com/service-proxy?command=query"
@@ -22,6 +22,7 @@ module BorrowDirect
22
22
  class << self
23
23
  attr_accessor :api_base, :partnership_id, :find_item_patron_barcode, :library_symbol
24
24
  attr_accessor :html_base_url
25
+ attr_accessor :api_key
25
26
 
26
27
  # used for HTTPClient send, connection, AND receive timeouts, so
27
28
  # theoretically could take 3x this, but unlikely, usually it's just
@@ -34,5 +35,7 @@ module BorrowDirect
34
35
  self.timeout = 30
35
36
  self.html_base_url = TEST_HTML_BASE
36
37
 
38
+ self.api_key = nil
39
+
37
40
  end
38
41
  end
@@ -0,0 +1,34 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module BorrowDirect
5
+ # Implements Relais' encryption protocol
6
+ # https://relais.atlassian.net/wiki/display/ILL/Encryption
7
+ #
8
+ # value = BorrowDirect::Encryption.new(public_key_string).encode_with_ts(api_key_or_other_data)
9
+ class Encryption
10
+ attr_reader :public_key_str
11
+
12
+ def initialize(a_public_key)
13
+ @public_key_str = a_public_key
14
+ end
15
+
16
+ # Will add on timestamp according to Relais protocol, encrypt,
17
+ # and Base64-encode, all per Relais protocol.
18
+ def encode_with_ts(value)
19
+ # Not sure if this object is thread-safe, so we re-create
20
+ # each time.
21
+
22
+ public_key = OpenSSL::PKey::RSA.new(self.public_key_str)
23
+
24
+ payload = "#{value}|#{self.now_timestamp}"
25
+
26
+ return Base64.encode64(public_key.public_encrypt(payload))
27
+ end
28
+
29
+ # Timestamp formatted how Relais wants it, in UTC
30
+ def now_timestamp
31
+ Time.now.getutc.strftime("%Y%m%d %H%M%S")
32
+ end
33
+ end
34
+ end
@@ -10,9 +10,15 @@ module BorrowDirect
10
10
  super(msg)
11
11
  end
12
12
 
13
+ # Different services use different error codes for 'invalid aid'
14
+ # Not sure we can actually catch them all, but we'll try.
15
+ def self.invalid_aid_code?(bd_code)
16
+ ["PUBFI003", "PUBRI002"].include?(bd_code)
17
+ end
13
18
  end
14
19
 
15
20
  class HttpError < Error ; end
21
+
16
22
  class HttpTimeoutError < HttpError
17
23
  attr_reader :timeout
18
24
  def initialize(msg, timeout=nil)
@@ -20,4 +26,15 @@ module BorrowDirect
20
26
  super(msg)
21
27
  end
22
28
  end
29
+
30
+ class InvalidAidError < Error
31
+ attr_reader :aid
32
+ def initialize(msg, bd_code = nil, aid = nil)
33
+ msg += " (aid: #{aid})" if aid
34
+ super(msg, bd_code)
35
+ @aid = aid
36
+ end
37
+ end
38
+
39
+
23
40
  end
@@ -1,5 +1,6 @@
1
1
  require 'borrow_direct'
2
2
  require 'borrow_direct/request'
3
+ require 'borrow_direct/pickup_location'
3
4
 
4
5
  module BorrowDirect
5
6
  # The BorrowDirect FindItem service, for discovering item availability
@@ -10,6 +11,7 @@ module BorrowDirect
10
11
  # # optional and use a default patron barcode
11
12
  #
12
13
  # You can also use #find_item_request to get the raw BD response as a ruby hash
14
+ #
13
15
  class FindItem < Request
14
16
  attr_reader :patron_barcode, :patron_library_symbol
15
17
 
@@ -24,7 +26,7 @@ module BorrowDirect
24
26
  @patron_barcode = patron_barcode
25
27
  @patron_library_symbol = patron_library_symbol
26
28
 
27
- # BD sometimes unpredictably returns this error when it means
29
+ # BD sometimes unpredictably returns one of these errors when it means
28
30
  # "no results", other times it doens't. We don't want to raise on it.
29
31
  self.expected_error_codes << "PUBFI002"
30
32
  end
@@ -56,7 +58,7 @@ module BorrowDirect
56
58
  raise ArgumentError.new("Missing valid search type and value: '#{options}'")
57
59
  end
58
60
 
59
- request exact_search_request_hash(search_type, search_value)
61
+ request exact_search_request_hash(search_type, search_value), need_auth_id(self.patron_barcode, self.patron_library_symbol)
60
62
  end
61
63
 
62
64
  # need to send a key and value for a valid exact_search type
@@ -65,7 +67,7 @@ module BorrowDirect
65
67
  # Returns a BorrowDirect::FindItem::Response object, from which you
66
68
  # can find out requestability, list of pickup locations, etc.
67
69
  def find(options)
68
- BorrowDirect::FindItem::Response.new find_item_request(options)
70
+ BorrowDirect::FindItem::Response.new find_item_request(options), self.auth_id
69
71
  end
70
72
 
71
73
  protected
@@ -79,10 +81,6 @@ module BorrowDirect
79
81
 
80
82
  hash = {
81
83
  "PartnershipId" => Defaults.partnership_id,
82
- "Credentials" => {
83
- "LibrarySymbol" => self.patron_library_symbol,
84
- "Barcode" => self.patron_barcode
85
- },
86
84
  "ExactSearch" => []
87
85
  }
88
86
 
@@ -101,8 +99,9 @@ module BorrowDirect
101
99
 
102
100
  attr_reader :response_hash
103
101
 
104
- def initialize(hash)
102
+ def initialize(hash, auth_id)
105
103
  @response_hash = hash
104
+ @auth_id = auth_id
106
105
  end
107
106
 
108
107
 
@@ -123,7 +122,7 @@ module BorrowDirect
123
122
  return false
124
123
  end
125
124
 
126
- return response_hash["Item"]["Available"].to_s == "true"
125
+ return response_hash["Available"].to_s == "true"
127
126
  end
128
127
 
129
128
  # BD thinks the item is locally available at patron's home library,
@@ -131,22 +130,37 @@ module BorrowDirect
131
130
  # Items that are available locally, and thus not requestable via BD, can
132
131
  # only be found by looking at the RequestMessage, bah
133
132
  def locally_available?
134
- h = response_hash["Item"]["RequestLink"]
135
- return !! (h && h["RequestMessage"] == "This item is available locally")
133
+ h = response_hash["RequestLink"]
134
+ # Message seems to sometimes have period sometimes not.
135
+ return !! (h && h["RequestMessage"] =~ /\AThis item is available locally\.?\Z/)
136
136
  end
137
-
138
- # Returns the AuthorizationID returned by FindItem API call,
139
- # or nil if none is available. Nil _can_ be returned, for
140
- # instance when BD returns a NotFound error instead of a good
141
- # response.
137
+
142
138
  def auth_id
143
- hash_key_path response_hash, "Item", "AuthorizationId"
139
+ @auth_id
144
140
  end
145
141
 
146
142
  # Can be nil in some cases if not requestable?
147
143
  # if requestable?, should be an array of Strings.
144
+ #
145
+ # This just returns BD location labels, see also #pickup_location_data to
146
+ # return labels and codes.
148
147
  def pickup_locations
149
- hash_key_path response_hash, "Item", "PickupLocations", "PickupLocation"
148
+ response_hash["PickupLocation"] && response_hash["PickupLocation"].collect {|h| h["PickupLocationDescription"] }
149
+ end
150
+
151
+ # Can be nil if not requestable, otherwise an array of BorrowDirect::PickupLocation
152
+ #
153
+ # See also #pickup_locations to return simply string location descriptions.
154
+ # It's perhaps more careful code to use the codes too, as in this method,
155
+ # although Relais says just using labels and submitting them to RequestItem
156
+ # as a pickup location should work too.
157
+ def pickup_location_data
158
+ unless defined? @pickup_location_data
159
+ @pickup_location_data = response_hash["PickupLocation"] && response_hash["PickupLocation"].collect do |hash|
160
+ BorrowDirect::PickupLocation.new(hash)
161
+ end
162
+ end
163
+ return @pickup_location_data
150
164
  end
151
165
 
152
166
 
@@ -0,0 +1,32 @@
1
+ module BorrowDirect
2
+ # Returned from FindItem::Response.pickup_locations , contains
3
+ # a #code and a #description . #to_a returns a handy duple
4
+ # suitable for passing to Rails options_for_select
5
+ class PickupLocation
6
+ attr_reader :response_hash
7
+ def initialize(bd_hash)
8
+ if bd_hash["PickupLocationCode"].empty? || bd_hash["PickupLocationDescription"].empty?
9
+ raise ArgumentError, "PickupLocation requires a hash with PickupLocationCode and PickupLocationDescription, not `#{bd_hash}`"
10
+ end
11
+
12
+ @response_hash = bd_hash
13
+ end
14
+
15
+ def code
16
+ @response_hash["PickupLocationCode"]
17
+ end
18
+
19
+ def description
20
+ @response_hash["PickupLocationDescription"]
21
+ end
22
+
23
+ def to_h
24
+ self.response_hash
25
+ end
26
+
27
+ # Handy for passing to Rails options_for_select
28
+ def to_a
29
+ [self.description, self.code]
30
+ end
31
+ end
32
+ end
@@ -55,22 +55,29 @@ module BorrowDirect
55
55
  @http_method = :post
56
56
  end
57
57
 
58
- def request(hash)
58
+ # First param is request hash, will be query param for GET or JSON body for POST
59
+ # Second param is optional AuthenticationID used by BD system -- if given,
60
+ # will be added to URI as "?aid=$AID", even for POST. Yep, that's Relais
61
+ # documented protocol eg https://relais.atlassian.net/wiki/display/ILL/Find+Item
62
+ def request(hash, aid = nil)
59
63
  http = http_client
60
-
61
64
 
62
- # Mostly for debugging, store these
63
- @last_request_uri = @api_uri
65
+ uri = @api_uri
66
+ if aid
67
+ uri += "?aid=#{CGI.escape aid}"
68
+ end
64
69
 
70
+ # Mostly for debugging, store these
71
+ @last_request_uri = uri
65
72
 
66
73
  start_time = Time.now
67
74
 
68
75
  if self.http_method == :post
69
76
  @last_request_json = json_request = JSON.generate(hash)
70
- http_response = http.post @api_uri, json_request, self.request_headers
77
+ http_response = http.post uri, json_request, self.request_headers
71
78
  elsif self.http_method == :get
72
79
  @last_request_query_params = hash
73
- http_response = http.get @api_uri, hash, self.request_headers
80
+ http_response = http.get uri, hash, self.request_headers
74
81
  else
75
82
  raise ArgumentError.new("BorrowDirect::Request only understands http_method :get and :post, not `#{self.http_method}`")
76
83
  end
@@ -83,14 +90,18 @@ module BorrowDirect
83
90
  rescue JSON::ParserError => json_parse_exception
84
91
  nil
85
92
  end
86
-
93
+
87
94
  # will be nil if we have none
88
95
  einfo = error_info(response_hash)
89
96
  expected_error = (einfo && self.expected_error_codes.include?(einfo.number))
90
97
 
91
98
 
92
99
  if einfo && (! expected_error)
93
- raise BorrowDirect::Error.new(einfo.message, einfo.number)
100
+ if BorrowDirect::Error.invalid_aid_code?(einfo.number)
101
+ raise BorrowDirect::InvalidAidError.new(einfo.message, einfo.number, aid)
102
+ else
103
+ raise BorrowDirect::Error.new(einfo.message, einfo.number)
104
+ end
94
105
  elsif http_response.code != 200 && (! expected_error)
95
106
  raise BorrowDirect::HttpError.new("HTTP Error: #{http_response.code}: #{http_response.body}")
96
107
  elsif response_hash.nil?
@@ -179,6 +190,17 @@ module BorrowDirect
179
190
  return OpenStruct.new(:number => e["Problem"]["Code"], :message => e["Problem"]["Message"])
180
191
  end
181
192
 
193
+ # And one more for Auth errors! With no error number, hooray.
194
+ if hash && (e = hash["AuthorizationState"]) && e["State"] == "failed"
195
+ return OpenStruct.new(:number => nil, :message => "AuthorizationState: State: failed")
196
+ end
197
+
198
+ # And yet another way!
199
+ if hash && (e = hash["Problem"])
200
+ # Code/Message appear unpredictably at different keys?
201
+ return OpenStruct.new(:number => e["ErrorCode"] || e["Code"], :message => e["ErrorMessage"] || e["Message"])
202
+ end
203
+
182
204
  return nil
183
205
  end
184
206
  end
@@ -22,9 +22,10 @@ module BorrowDirect
22
22
  @patron_barcode = patron_barcode
23
23
  @patron_library_symbol = patron_library_symbol
24
24
 
25
- # BD sometimes unpredictably returns this error when it means
26
- # "no results", other times it doens't. We don't want to raise on it.
27
- self.expected_error_codes << "PUBRI004"
25
+ # BD sometimes unpredictably returns one of these errors when it means
26
+ # "no results", other times it doesn't. We don't want to raise on it.
27
+ self.expected_error_codes << "PUBRI004"
28
+ self.expected_error_codes << "PUBRI003"
28
29
  end
29
30
 
30
31
  # need to send a key and value for a valid exact_search type
@@ -34,6 +35,11 @@ module BorrowDirect
34
35
  # PickupLocation to BD, which it seems to accept, not sure what it
35
36
  # does with it.
36
37
  #
38
+ # pickup_location can be a BorrowDirect::PickupLocation object,
39
+ # or a string. If a string, BD recommends it be a CODE returned
40
+ # from FindItem, rather than DESCRIPTION as in the past, but we
41
+ # think description still works?
42
+ #
37
43
  # Returns the actual complete BD response hash. You may want
38
44
  # #make_request instead
39
45
  #
@@ -55,7 +61,11 @@ module BorrowDirect
55
61
  raise ArgumentError.new("Missing valid search type and value: '#{options}'")
56
62
  end
57
63
 
58
- request exact_search_request_hash(pickup_location, search_type, search_value)
64
+ if pickup_location.kind_of?(BorrowDirect::PickupLocation)
65
+ pickup_location = pickup_location.code
66
+ end
67
+
68
+ request exact_search_request_hash(pickup_location, search_type, search_value), need_auth_id(self.patron_barcode, self.patron_library_symbol)
59
69
  end
60
70
 
61
71
  # Pass in a BD exact search and pickup location eg
@@ -91,14 +101,12 @@ module BorrowDirect
91
101
  protected
92
102
 
93
103
  def extract_request_number(resp)
94
- return (resp["Request"] && resp["Request"]["RequestNumber"])
104
+ return resp["RequestNumber"]
95
105
  end
96
106
 
97
107
  def exact_search_request_hash(pickup_location, type, value)
98
108
  hash = {
99
109
  "PartnershipId" => Defaults.partnership_id,
100
- "AuthorizationId" => need_auth_id(self.patron_barcode, self.patron_library_symbol),
101
- "PickupLocation" => pickup_location,
102
110
  "ExactSearch" => [
103
111
  {
104
112
  "Type" => type.to_s.upcase,
@@ -55,7 +55,16 @@ module BorrowDirect
55
55
  "fullRecord" => (full_record ? "1" : "0")
56
56
  }
57
57
 
58
- request query_params
58
+ response = request query_params
59
+
60
+ # RequestQuery sometimes returns odd 200 OK response with
61
+ # AuthFailed, at least as of 29 Sep 2015. Catch it and
62
+ # raise it properly.
63
+ if response["AuthorizationState"] && response["AuthorizationState"]["State"] == false
64
+ raise BorrowDirect::InvalidAidError.new("API returned AuthorizationState.State == false", nil, response["AuthorizationState"]["AuthorizationId"])
65
+ end
66
+
67
+ return response
59
68
  end
60
69
 
61
70
  # Returns an array of BorrowDirect::RequestQuery::Item
@@ -68,7 +77,7 @@ module BorrowDirect
68
77
 
69
78
  results = []
70
79
 
71
- response["QueryResult"]["MyRequestRecords"].each do |item_hash|
80
+ response["MyRequestRecords"].each do |item_hash|
72
81
  results << BorrowDirect::RequestQuery::Item.new(item_hash)
73
82
  end
74
83
 
@@ -89,11 +98,16 @@ module BorrowDirect
89
98
  # basic record values
90
99
  @request_number = hash["RequestNumber"]
91
100
  @title = hash["Title"]
92
- @date_submitted = DateTime.iso8601 hash["ISO8601DateSubmitted"]
101
+ if hash["ISO8601DateSubmitted"]
102
+ @date_submitted = DateTime.iso8601 hash["ISO8601DateSubmitted"]
103
+ end
93
104
  @allow_renew = hash["AllowRenew"]
94
105
  @allow_cancel = hash["AllowCancel"]
95
106
  @request_status = hash["RequestStatus"]
96
- @request_status_date = DateTime.iso8601 hash["ISO8601RequestStatusDate"]
107
+
108
+ if hash["ISO8601RequestStatusDate"]
109
+ @request_status_date = DateTime.iso8601 hash["ISO8601RequestStatusDate"]
110
+ end
97
111
 
98
112
  # full record values
99
113
  @publicaition_type = hash["PublicationType"]
@@ -1,3 +1,3 @@
1
1
  module BorrowDirect
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0.pre1"
3
3
  end