alma 0.2.6 → 0.3.3

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +54 -0
  3. data/.circleci/setup-rubygems.sh +3 -0
  4. data/.github/dependabot.yml +7 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +134 -0
  7. data/.ruby-version +1 -1
  8. data/CODE_OF_CONDUCT.md +1 -1
  9. data/Gemfile +4 -3
  10. data/Guardfile +75 -0
  11. data/README.md +136 -26
  12. data/Rakefile +3 -1
  13. data/alma.gemspec +21 -16
  14. data/lib/alma/alma_record.rb +3 -3
  15. data/lib/alma/api_defaults.rb +39 -0
  16. data/lib/alma/availability_response.rb +50 -53
  17. data/lib/alma/bib.rb +26 -42
  18. data/lib/alma/bib_holding.rb +25 -0
  19. data/lib/alma/bib_item.rb +28 -38
  20. data/lib/alma/bib_item_set.rb +72 -12
  21. data/lib/alma/bib_set.rb +7 -21
  22. data/lib/alma/config.rb +10 -4
  23. data/lib/alma/course.rb +47 -0
  24. data/lib/alma/course_set.rb +17 -0
  25. data/lib/alma/electronic/README.md +20 -0
  26. data/lib/alma/electronic/batch_utils.rb +224 -0
  27. data/lib/alma/electronic/business.rb +29 -0
  28. data/lib/alma/electronic.rb +167 -0
  29. data/lib/alma/error.rb +16 -4
  30. data/lib/alma/fine.rb +16 -0
  31. data/lib/alma/fine_set.rb +36 -21
  32. data/lib/alma/item_request_options.rb +23 -0
  33. data/lib/alma/library.rb +29 -0
  34. data/lib/alma/library_set.rb +21 -0
  35. data/lib/alma/loan.rb +31 -2
  36. data/lib/alma/loan_set.rb +62 -15
  37. data/lib/alma/location.rb +29 -0
  38. data/lib/alma/location_set.rb +21 -0
  39. data/lib/alma/renewal_response.rb +19 -11
  40. data/lib/alma/request.rb +167 -0
  41. data/lib/alma/request_options.rb +36 -17
  42. data/lib/alma/request_set.rb +64 -15
  43. data/lib/alma/response.rb +45 -0
  44. data/lib/alma/result_set.rb +27 -35
  45. data/lib/alma/user.rb +111 -92
  46. data/lib/alma/user_request.rb +19 -0
  47. data/lib/alma/user_set.rb +5 -6
  48. data/lib/alma/version.rb +3 -1
  49. data/lib/alma.rb +34 -22
  50. data/log/.gitignore +4 -0
  51. metadata +118 -10
  52. data/.travis.yml +0 -5
  53. data/lib/alma/api.rb +0 -33
data/lib/alma/loan_set.rb CHANGED
@@ -1,29 +1,76 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
- class LoanSet
3
- extend Forwardable
4
- include Enumerable
5
- #include Alma::Error
4
+ class LoanSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ alias :total_records :total_record_count
9
+
10
+
11
+ attr_reader :results, :raw_response
12
+ def_delegators :results, :empty?
13
+
14
+ def initialize(raw_response, search_args = {})
15
+ @raw_response = raw_response
16
+ @response = raw_response.parsed_response
17
+ @search_args = search_args
18
+ validate(raw_response)
19
+ @results = @response.fetch(key, [])
20
+ .map { |item| single_record_class.new(item) }
21
+ # args passed to the search that returned this set
22
+ # such as limit, expand, order_by, etc
23
+ end
24
+
25
+ def loggable
26
+ { search_args: @search_args,
27
+ uri: @raw_response&.request&.uri.to_s
28
+ }.select { |k, v| !(v.nil? || v.empty?) }
29
+ end
6
30
 
7
- attr_reader :response
8
- def_delegators :list, :each, :size
9
- def_delegators :response, :[], :fetch
31
+ def validate(response)
32
+ if response.code != 200
33
+ error = "Could not find loans info."
34
+ log = loggable.merge(response.parsed_response)
35
+ raise ResponseError.new(error, log)
36
+ end
37
+ end
10
38
 
11
- def initialize(response_body_hash)
12
- @response = response_body_hash
39
+ def all
40
+ Enumerator.new do |yielder|
41
+ offset = 0
42
+ loop do
43
+ extra_args = @search_args.merge({ limit: 100, offset: offset })
44
+ r = (offset == 0) ? self : single_record_class.where_user(user_id, extra_args)
45
+ unless r.empty?
46
+ r.map { |item| yielder << item }
47
+ offset += 100
48
+ else
49
+ raise StopIteration
50
+ end
51
+ end
52
+ end
13
53
  end
14
54
 
15
- def list
16
- fetch(key, [])
55
+ def each(&block)
56
+ @results.each(&block)
57
+ end
58
+
59
+ def success?
60
+ raw_response.response.code.to_s == "200"
17
61
  end
18
62
 
19
63
  def key
20
- 'item_loan'
64
+ "item_loan"
21
65
  end
22
66
 
23
- def total_record_count
24
- fetch('total_record_count', 0)
67
+ def single_record_class
68
+ Alma::Loan
25
69
  end
26
- alias :total_records :total_record_count
27
70
 
71
+ private
72
+ def user_id
73
+ @user_id ||= results.first.user_id
74
+ end
28
75
  end
29
76
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class Location < AlmaRecord
5
+ extend Alma::ApiDefaults
6
+
7
+ def self.all(library_code:, args: {})
8
+ response = HTTParty.get("#{configuration_base_path}/libraries/#{library_code}/locations", query: args, headers: headers, timeout: timeout)
9
+ if response.code == 200
10
+ LocationSet.new(response)
11
+ else
12
+ raise StandardError, get_body_from(response)
13
+ end
14
+ end
15
+
16
+ def self.find(library_code:, location_code:, args: {})
17
+ response = HTTParty.get("#{configuration_base_path}/libraries/#{library_code}/locations/#{location_code}", query: args, headers: headers, timeout: timeout)
18
+ if response.code == 200
19
+ AlmaRecord.new(response)
20
+ else
21
+ raise StandardError, get_body_from(response)
22
+ end
23
+ end
24
+
25
+ def self.get_body_from(response)
26
+ JSON.parse(response.body)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class LocationSet < ResultSet
5
+ def_delegators :results, :[], :empty?
6
+
7
+ def each(&block)
8
+ results.each(&block)
9
+ end
10
+
11
+ def results
12
+ @results ||= @response.fetch(key, [])
13
+ .map { |item| single_record_class.new(item) }
14
+ end
15
+
16
+ protected
17
+ def key
18
+ "location"
19
+ end
20
+ end
21
+ end
@@ -1,31 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class RenewalResponse
3
-
4
-
5
-
6
5
  def initialize(response)
7
- @response = response
8
- @success = response.has_key?('loan_id')
6
+ @raw_response = response
7
+ @response = response.parsed_response
8
+ @success = response.has_key?("loan_id")
9
+ end
10
+
11
+ def loggable
12
+ { uri: @raw_response&.request&.uri.to_s
13
+ }.select { |k, v| !(v.nil? || v.empty?) }
9
14
  end
10
15
 
11
16
  def renewed?
12
17
  @success
13
18
  end
14
19
 
20
+ def has_error?
21
+ !renewed?
22
+ end
23
+
15
24
  def due_date
16
- @response.fetch('dueDate', '')
25
+ @response.fetch("due_date", "")
17
26
  end
18
27
 
19
28
 
20
29
  def due_date_pretty
21
- Time.parse(due_date).strftime('%m-%e-%y %H:%M')
30
+ Time.parse(due_date).strftime("%m-%e-%y %H:%M")
22
31
  end
23
32
 
24
33
  def item_title
25
34
  if renewed?
26
- @response['title']
35
+ @response["title"]
27
36
  else
28
- 'This Item'
37
+ "This Item"
29
38
  end
30
39
  end
31
40
 
@@ -38,8 +47,7 @@ module Alma
38
47
  end
39
48
 
40
49
  def error_message
41
- @response unless renewed?
50
+ @response unless renewed?
42
51
  end
43
-
44
52
  end
45
53
  end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alma
4
+ class BibRequest
5
+ class ItemAlreadyExists < Alma::StandardError
6
+ end
7
+
8
+ extend Alma::ApiDefaults
9
+
10
+ REQUEST_TYPES = %w[HOLD DIGITIZATION BOOKING]
11
+
12
+ def self.submit(args)
13
+ request = new(args)
14
+ response = HTTParty.post(
15
+ "#{bibs_base_path}/#{request.mms_id}/requests",
16
+ query: { user_id: request.user_id },
17
+ headers: headers,
18
+ body: request.body.to_json
19
+ )
20
+ Alma::Response.new(response)
21
+ end
22
+
23
+ attr_reader :mms_id, :user_id, :body, :request_type
24
+ def initialize(args)
25
+ @mms_id = args.delete(:mms_id) { raise ArgumentError.new(":mms_id option must be specified to create request") }
26
+ @user_id = args.delete(:user_id) { raise ArgumentError.new(":user_id option must be specified to create request") }
27
+ @request_type = args.fetch(:request_type, "NOT_SPECIFIED")
28
+ validate!(args)
29
+ normalize!(args)
30
+ @body = args
31
+ end
32
+
33
+
34
+ def normalize!(args)
35
+ request_type_normalization!(args)
36
+ additional_normalization!(args)
37
+ end
38
+
39
+ def request_type_normalization!(args)
40
+ method = "#{@request_type.downcase}_normalization".to_sym
41
+ send(method, args) if respond_to? method
42
+ end
43
+
44
+ # Intended to be overridden by subclasses, allowing extra normalization logic to be provided
45
+ def additional_normalization!(args)
46
+ end
47
+
48
+ def validate!(args)
49
+ unless REQUEST_TYPES.include?(request_type)
50
+ raise ArgumentError.new(":request_type option must be specified and one of #{REQUEST_TYPES.join(", ")} to submit a request")
51
+ end
52
+ request_type_validation!(args)
53
+ additional_validation!(args)
54
+ end
55
+
56
+ def request_type_validation!(args)
57
+ method = "#{@request_type.downcase}_validation".to_sym
58
+ send(method, args) if respond_to? method
59
+ end
60
+
61
+ # Intended to be overridden by subclasses, allowing extra validation logic to be provided
62
+ def additional_validation!(args)
63
+ end
64
+
65
+ def digitization_normalization(args)
66
+ if args[:target_destination].is_a? String
67
+ args[:target_destination] = { value: args[:target_destination] }
68
+ end
69
+ end
70
+
71
+ def digitization_validation(args)
72
+ args.fetch(:target_destination) do
73
+ raise ArgumentError.new(
74
+ ":target_destination option must be specified when request_type is DIGITIZATION"
75
+ )
76
+ end
77
+ pd = args.fetch(:partial_digitization) do
78
+ raise ArgumentError.new(
79
+ ":partial_digitization option must be specified when request_type is DIGITIZATION"
80
+ )
81
+ end
82
+ if pd == true
83
+ args.fetch(:comment) do
84
+ raise ArgumentError.new(
85
+ ":comment option must be specified when :request_type is DIGITIZATION and :partial_digitization is true"
86
+ )
87
+ end
88
+ end
89
+ end
90
+
91
+ def booking_normalization(args)
92
+ if args[:material_type].is_a? String
93
+ args[:material_type] = { value: args[:material_type] }
94
+ end
95
+ end
96
+
97
+ def booking_validation(args)
98
+ args.fetch(:booking_start_date) do
99
+ raise ArgumentError.new(
100
+ ":booking_start_date option must be specified when request_type is BOOKING"
101
+ )
102
+ end
103
+ args.fetch(:booking_end_date) do
104
+ raise ArgumentError.new(
105
+ ":booking_end_date option must be specified when request_type is BOOKING"
106
+ )
107
+ end
108
+ args.fetch(:pickup_location_type) do
109
+ raise ArgumentError.new(
110
+ ":pickup_location_type option must be specified when request_type is BOOKING"
111
+ )
112
+ end
113
+ args.fetch(:pickup_location_library) do
114
+ raise ArgumentError.new(
115
+ ":pickup_location_library option must be specified when request_type is BOOKING"
116
+ )
117
+ end
118
+ end
119
+
120
+ def hold_normalization(args)
121
+ # if args[:material_type].is_a? String
122
+ # args[:material_type] = { value: args[:material_type] }
123
+ # end
124
+ end
125
+
126
+ def hold_validation(args)
127
+ args.fetch(:pickup_location_type) do
128
+ raise ArgumentError.new(
129
+ ":pickup_location_type option must be specified when request_type is HOLD"
130
+ )
131
+ end
132
+ args.fetch(:pickup_location_library) do
133
+ raise ArgumentError.new(
134
+ ":pickup_location_library option must be specified when request_type is HOLD"
135
+ )
136
+ end
137
+ end
138
+ end
139
+
140
+ class ItemRequest < BibRequest
141
+ def self.submit(args)
142
+ request = new(args)
143
+ response = HTTParty.post(
144
+ "#{bibs_base_path}/#{request.mms_id}/holdings/#{request.holding_id}/items/#{request.item_pid}/requests",
145
+ query: { user_id: request.user_id },
146
+ headers: headers,
147
+ body: request.body.to_json
148
+ )
149
+ Alma::Response.new(response)
150
+ end
151
+
152
+ attr_reader :holding_id, :item_pid
153
+ def initialize(args)
154
+ super(args)
155
+ @holding_id = args.delete(:holding_id) { raise ArgumentError.new(":holding_id option must be specified to create request") }
156
+ @item_pid = args.delete(:item_pid) { raise ArgumentError.new(":item_pid option must be specified to create request") }
157
+ end
158
+
159
+ def additional_validation!(args)
160
+ args.fetch(:description) do
161
+ raise ArgumentError.new(
162
+ ":description option must be specified when request_type is DIGITIZATION"
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end
@@ -1,6 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
4
  class RequestOptions
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
3
8
  extend Forwardable
9
+ extend Alma::ApiDefaults
4
10
 
5
11
  attr_accessor :request_options, :raw_response
6
12
  def_delegators :raw_response, :response, :request
@@ -9,39 +15,52 @@ module Alma
9
15
 
10
16
  def initialize(response)
11
17
  @raw_response = response
12
- @request_options = JSON.parse(response.body)["request_option"]
18
+ validate(response)
19
+ @request_options = response.parsed_response["request_option"]
13
20
  end
14
21
 
15
- def self.get(mms_id, options={})
22
+
23
+ def self.get(mms_id, options = {})
16
24
  url = "#{bibs_base_path}/#{mms_id}/request-options"
17
- options.select! {|k,_| REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
18
- response = HTTParty.get(url, headers: headers, query: options)
25
+ options.select! { |k, _| REQUEST_OPTIONS_PERMITTED_ARGS.include? k }
26
+ response = HTTParty.get(url, headers: headers, query: options, timeout: timeout)
19
27
  new(response)
20
28
  end
21
29
 
22
- def hold_allowed?
23
- !request_options.select {|option| option["type"]["value"] == "HOLD" }.empty?
30
+ def loggable
31
+ { uri: @raw_response&.request&.uri.to_s
32
+ }.select { |k, v| !(v.nil? || v.empty?) }
24
33
  end
25
34
 
26
- private
35
+ def validate(response)
36
+ if response.code != 200
37
+ raise ResponseError.new("Could not get request options.", loggable.merge(response.parsed_response))
38
+ end
39
+ end
27
40
 
28
- def self.region
29
- Alma.configuration.region
41
+ def hold_allowed?
42
+ !request_options.nil? &&
43
+ !request_options.select { |option| option["type"]["value"] == "HOLD" }.empty?
30
44
  end
31
45
 
32
- def self.bibs_base_path
33
- "#{region}/almaws/v1/bibs"
46
+ def digitization_allowed?
47
+ !request_options.nil? &&
48
+ !request_options.select { |option| option["type"]["value"] == "DIGITIZATION" }.empty?
34
49
  end
35
50
 
36
- def self.headers
37
- { "Authorization": "apikey #{apikey}",
38
- "Accept": "application/json",
39
- "Content-Type": "application/json" }
51
+ def booking_allowed?
52
+ !request_options.nil? &&
53
+ !request_options.select { |option| option["type"]["value"] == "BOOKING" }.empty?
40
54
  end
41
55
 
42
- def self.apikey
43
- Alma.configuration.apikey
56
+ def resource_sharing_broker_allowed?
57
+ !request_options.nil? &&
58
+ !request_options.select { |option| option["type"]["value"] == "RS_BROKER" }.empty?
44
59
  end
45
60
 
61
+ def ez_borrow_link
62
+ broker = request_options.select { |option| option["type"]["value"] == "RS_BROKER" }
63
+ broker.collect { |opt| opt["request_url"] }.first
64
+ end
46
65
  end
47
66
  end
@@ -1,29 +1,78 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Alma
2
- class RequestSet
3
- extend Forwardable
4
- include Enumerable
5
- #include Alma::Error
4
+ class RequestSet < ResultSet
5
+ class ResponseError < Alma::StandardError
6
+ end
7
+
8
+ alias :total_records :total_record_count
6
9
 
7
- attr_reader :response
8
- def_delegators :list, :each, :size
9
- def_delegators :response, :[], :fetch
10
+ attr_reader :results, :raw_response
11
+ def_delegators :results, :empty?
10
12
 
11
- def initialize(response_body_hash)
12
- @response = response_body_hash
13
+ def initialize(raw_response)
14
+ @raw_response = raw_response
15
+ @response = raw_response.parsed_response
16
+ validate(raw_response)
17
+ @results = @response.fetch(key, [])
18
+ .map { |item| single_record_class.new(item) }
13
19
  end
14
20
 
15
- def list
16
- fetch(key, [])
21
+ def loggable
22
+ { uri: @raw_response&.request&.uri.to_s
23
+ }.select { |k, v| !(v.nil? || v.empty?) }
17
24
  end
18
25
 
19
- def total_record_count
20
- fetch('total_record_count', 0)
26
+ def validate(response)
27
+ if response.code != 200
28
+ error = "Could not find requests."
29
+ log = loggable.merge(response.parsed_response)
30
+ raise ResponseError.new(error, log)
31
+ end
32
+ end
33
+
34
+ def all
35
+ Enumerator.new do |yielder|
36
+ offset = 0
37
+ loop do
38
+ r = (offset == 0) ? self : single_record_class.where_user(user_id, { limit: 100, offset: offset })
39
+ unless r.empty?
40
+ r.map { |item| yielder << item }
41
+ offset += 100
42
+ else
43
+ raise StopIteration
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def each(&block)
50
+ @results.each(&block)
51
+ end
52
+
53
+ def success?
54
+ raw_response.response.code.to_s == "200"
21
55
  end
22
- alias :total_records :total_record_count
23
56
 
24
57
  def key
25
- 'user_request'
58
+ "user_request"
26
59
  end
27
60
 
61
+ def single_record_class
62
+ Alma::UserRequest
63
+ end
64
+
65
+ private
66
+ def user_id
67
+ @user_id ||= get_user_id_from_path(raw_response.request.uri.path)
68
+ end
69
+
70
+ def get_user_id_from_path(path)
71
+ # Path in user api calls starts with "/almaws/v1/users/123/maybe_something/else"
72
+ split_path = path.split("/")
73
+ # the part immediately following the "users" is going to be the user_id
74
+ user_id_index = split_path.index("users") + 1
75
+ split_path[user_id_index]
76
+ end
28
77
  end
29
78
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Alma
6
+ class Response
7
+ class StandardError < Alma::StandardError
8
+ end
9
+
10
+ extend ::Forwardable
11
+
12
+ attr_reader :raw_response
13
+ def_delegators :raw_response, :body, :success?, :response, :request
14
+
15
+ def initialize(response)
16
+ @raw_response = response
17
+ # We could validate and throw an error here but currently a
18
+ validate(response)
19
+ end
20
+
21
+ def loggable
22
+ { uri: @raw_response&.request&.uri.to_s
23
+ }.select { |k, v| !(v.nil? || v.empty?) }
24
+ end
25
+
26
+ def validate(response)
27
+ if errors.first&.dig("errorCode") == "401136"
28
+ message = "The requested item already exists."
29
+ log = loggable.merge(response.parsed_response)
30
+
31
+ raise Alma::BibRequest::ItemAlreadyExists.new(message, log)
32
+ end
33
+
34
+ if response.code != 200
35
+ log = loggable.merge(response.parsed_response)
36
+ raise StandardError.new("Invalid Response.", log)
37
+ end
38
+ end
39
+
40
+ # Returns an array of errors
41
+ def errors
42
+ @raw_response.parsed_response&.dig("errorList", "error") || []
43
+ end
44
+ end
45
+ end
@@ -1,50 +1,42 @@
1
- module Alma
2
- class ResultSet
3
- extend Forwardable
1
+ # frozen_string_literal: true
4
2
 
5
- include Enumerable
6
- include Alma::Error
3
+ require "forwardable"
7
4
 
8
- def_delegators :list, :each, :size
5
+ class Alma::ResultSet
6
+ extend ::Forwardable
7
+ include Enumerable
8
+ include Alma::Error
9
9
 
10
- def initialize(ws_response)
11
- @response = ws_response
12
- end
10
+ attr_reader :response
13
11
 
14
- def total_record_count
15
- @response[top_level_key].fetch('total_record_count', 0).to_i
16
- end
12
+ def_delegators :response, :[], :fetch
13
+ def_delegators :each, :each_with_index, :size
17
14
 
18
- def list
19
- @list ||= list_results
20
- end
15
+ def initialize(response_body_hash)
16
+ @response = response_body_hash
17
+ end
21
18
 
19
+ def loggable
20
+ { uri: @response&.request&.uri&.to_s }
21
+ .select { |k, v| !(v.nil? || v.empty?) }
22
+ end
22
23
 
23
- def top_level_key
24
- raise NotImplementedError 'Subclasses of ResultSet Need to define the top level key'
25
- end
24
+ def each
25
+ @results ||= @response.fetch(key, [])
26
+ .map { |item| single_record_class.new(item) }
27
+ end
26
28
 
27
- def response_records_key
28
- raise NotImplementedError 'Subclasses of ResultSet Need to define the key for response records'
29
- end
29
+ def total_record_count
30
+ fetch("total_record_count", 0).to_i
31
+ end
32
+ alias :total_records :total_record_count
30
33
 
31
- private
32
- def response_records
33
- @response[top_level_key].fetch(response_records_key,[])
34
+ protected
35
+ def key
36
+ raise NotImplementedError "Subclasses of ResultSet need to define a response key"
34
37
  end
35
38
 
36
- # Subclasses Can override this to use a Custom Class for single record objects.
37
39
  def single_record_class
38
40
  Alma::AlmaRecord
39
41
  end
40
-
41
- def list_results
42
- #If there is only one record in the response, HTTParty returns as a hash, not
43
- # an array of hashes, so wrap in array to normalize.
44
- response_array = (response_records.is_a? Array) ? response_records : [response_records]
45
- response_array.map do |record|
46
- single_record_class.new(record)
47
- end
48
- end
49
- end
50
42
  end