borrow_direct 1.1.0 → 1.2.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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