berkeley_library-alma 0.0.1

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +18 -0
  3. data/.gitignore +388 -0
  4. data/.idea/.gitignore +8 -0
  5. data/.idea/alma.iml +55 -0
  6. data/.idea/codeStyles/codeStyleConfig.xml +5 -0
  7. data/.idea/inspectionProfiles/Project_Default.xml +26 -0
  8. data/.idea/misc.xml +4 -0
  9. data/.idea/modules.xml +8 -0
  10. data/.idea/vcs.xml +6 -0
  11. data/.rubocop.yml +334 -0
  12. data/.ruby-version +1 -0
  13. data/.simplecov +8 -0
  14. data/.yardopts +2 -0
  15. data/CHANGES.md +3 -0
  16. data/Dockerfile +54 -0
  17. data/Gemfile +3 -0
  18. data/Jenkinsfile +18 -0
  19. data/LICENSE.md +21 -0
  20. data/README.md +210 -0
  21. data/Rakefile +20 -0
  22. data/berkeley_library-alma.gemspec +42 -0
  23. data/bin/alma-mms-lookup +64 -0
  24. data/docker-compose.yml +15 -0
  25. data/lib/berkeley_library/alma/bib_number.rb +90 -0
  26. data/lib/berkeley_library/alma/config.rb +178 -0
  27. data/lib/berkeley_library/alma/constants.rb +16 -0
  28. data/lib/berkeley_library/alma/mms_id.rb +100 -0
  29. data/lib/berkeley_library/alma/module_info.rb +14 -0
  30. data/lib/berkeley_library/alma/record_id.rb +113 -0
  31. data/lib/berkeley_library/alma.rb +1 -0
  32. data/rakelib/bundle.rake +8 -0
  33. data/rakelib/coverage.rake +11 -0
  34. data/rakelib/gem.rake +54 -0
  35. data/rakelib/rubocop.rake +18 -0
  36. data/rakelib/spec.rake +2 -0
  37. data/spec/.rubocop.yml +37 -0
  38. data/spec/data/991054360089706532-sru.xml +186 -0
  39. data/spec/data/b11082434-sru.xml +165 -0
  40. data/spec/data/bibs_with_check_digits.txt +151 -0
  41. data/spec/lib/berkeley_library/alma/bib_number_spec.rb +95 -0
  42. data/spec/lib/berkeley_library/alma/config_spec.rb +94 -0
  43. data/spec/lib/berkeley_library/alma/mms_id_spec.rb +111 -0
  44. data/spec/lib/berkeley_library/alma/record_id_spec.rb +41 -0
  45. data/spec/spec_helper.rb +56 -0
  46. metadata +325 -0
@@ -0,0 +1,42 @@
1
+ File.expand_path('lib', __dir__).tap do |lib|
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ end
4
+
5
+ ruby_version = '>= 2.7'
6
+
7
+ require 'berkeley_library/alma/module_info'
8
+
9
+ Gem::Specification.new do |spec|
10
+ spec.name = BerkeleyLibrary::Alma::ModuleInfo::NAME
11
+ spec.author = BerkeleyLibrary::Alma::ModuleInfo::AUTHOR
12
+ spec.email = BerkeleyLibrary::Alma::ModuleInfo::AUTHOR_EMAIL
13
+ spec.summary = BerkeleyLibrary::Alma::ModuleInfo::SUMMARY
14
+ spec.description = BerkeleyLibrary::Alma::ModuleInfo::DESCRIPTION
15
+ spec.license = BerkeleyLibrary::Alma::ModuleInfo::LICENSE
16
+ spec.version = BerkeleyLibrary::Alma::ModuleInfo::VERSION
17
+ spec.homepage = BerkeleyLibrary::Alma::ModuleInfo::HOMEPAGE
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.required_ruby_version = ruby_version
24
+
25
+ spec.add_dependency 'berkeley_library-logging', '~> 0.2'
26
+ spec.add_dependency 'berkeley_library-marc', '~> 0.2.1'
27
+ spec.add_dependency 'berkeley_library-util', '~> 0.1', '>= 0.1.1'
28
+ spec.add_dependency 'nokogiri', '~> 1.12'
29
+
30
+ spec.add_development_dependency 'bundle-audit', '~> 0.1'
31
+ spec.add_development_dependency 'ci_reporter_rspec', '~> 1.0'
32
+ spec.add_development_dependency 'colorize', '~> 0.8'
33
+ spec.add_development_dependency 'rake', '~> 13.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.10'
35
+ spec.add_development_dependency 'rubocop', '= 1.11'
36
+ spec.add_development_dependency 'rubocop-rake', '= 0.6.0'
37
+ spec.add_development_dependency 'rubocop-rspec', '= 2.4.0'
38
+ spec.add_development_dependency 'simplecov', '~> 0.21'
39
+ spec.add_development_dependency 'simplecov-rcov', '~> 0.2'
40
+ spec.add_development_dependency 'webmock', '~> 3.12'
41
+ spec.add_development_dependency 'yard', '~> 0.9.27'
42
+ end
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # ------------------------------------------------------------
4
+ # Global setup
5
+
6
+ # Don't buffer stdout or stderr
7
+ $stdout.sync = true
8
+ $stderr.sync = true
9
+
10
+ # ------------------------------------------------------------
11
+ # Dependencies
12
+
13
+ # Require gems
14
+ require 'bundler/setup'
15
+
16
+ # Require lib directory
17
+ unless $LOAD_PATH.include?((lib_path = File.expand_path('../lib', __dir__)))
18
+ puts "Adding #{lib_path} to $LOAD_PATH"
19
+ $LOAD_PATH.unshift(lib_path)
20
+ end
21
+
22
+ require 'berkeley_library/alma'
23
+
24
+ # ------------------------------------------------------------
25
+ # Configuration
26
+
27
+ # Configure Alma URLs etc.
28
+ BerkeleyLibrary::Alma.configure do
29
+ Config.default!
30
+ end
31
+
32
+ # Set log level
33
+ BerkeleyLibrary::Logging.logger.level = Logger::Severity::WARN
34
+
35
+ # ------------------------------------------------------------
36
+ # Utility methods
37
+
38
+ # Read raw IDs from STDIN
39
+ def raw_ids
40
+ @raw_ids ||= Enumerator.new do |y|
41
+ $stdin.each_line(chomp: true) do |ln|
42
+ y << ln.strip
43
+ end
44
+ end
45
+ end
46
+
47
+ # Read control field 001 from MARC record for specified record ID
48
+ def id_001_for(record_id)
49
+ return unless record_id
50
+ return unless (marc_record = record_id.get_marc_record)
51
+ return unless (cf_001 = marc_record['001'])
52
+
53
+ cf_001.value
54
+ end
55
+
56
+ # ------------------------------------------------------------
57
+ # Main program
58
+
59
+ raw_ids.each do |raw_id|
60
+ record_id = BerkeleyLibrary::Alma::RecordId.parse(raw_id)
61
+ canonical_id = id_001_for(record_id)
62
+
63
+ puts [raw_id, record_id, canonical_id].join("\t")
64
+ end
@@ -0,0 +1,15 @@
1
+ services:
2
+ gem:
3
+ build:
4
+ context: .
5
+ target: development
6
+ ports:
7
+ - target: 3000
8
+ published: 3000
9
+ restart: always
10
+ volumes:
11
+ # Note that this mounts the *entire* repo directory (including
12
+ # files ignored in .dockerignore when building the image)
13
+ - ./:/opt/app
14
+
15
+ version: "3.8"
@@ -0,0 +1,90 @@
1
+ require 'berkeley_library/alma/record_id'
2
+
3
+ module BerkeleyLibrary
4
+ module Alma
5
+ # {RecordId} subclass representing a Millennium bib number.
6
+ class BibNumber
7
+ include RecordId
8
+
9
+ # ------------------------------------------------------------
10
+ # Accessors
11
+
12
+ # @return [String] the numeric part of the bib number, excluding check digit, as a string
13
+ attr_reader :digit_str
14
+
15
+ # @return [String] the check digit of the bib number, as a string
16
+ attr_reader :check_str
17
+
18
+ # ------------------------------------------------------------
19
+ # Initializer
20
+
21
+ # Initializes a new {BibNumber} from the specified string.
22
+ #
23
+ # @param [String] bib_number The bib number, with or without check digit
24
+ # @raise [ArgumentError] if the specified string is not an 8- or 9-digit bib number,
25
+ # or if a 9-digit bib number has an incorrect check digit
26
+ def initialize(bib_number)
27
+ @digit_str, @check_str = split_bib(bib_number)
28
+ end
29
+
30
+ # ------------------------------------------------------------
31
+ # Instance methods
32
+
33
+ # Returns the full bib number, including the correct check digit, as a string.
34
+ #
35
+ # @return [String] the bib number, as a string
36
+ def full_bib
37
+ "b#{digit_str}#{check_str}"
38
+ end
39
+
40
+ # Returns the full bib number, including the correct check digit, as a string.
41
+ #
42
+ # @return [String] the bib number, as a string
43
+ def to_s
44
+ full_bib
45
+ end
46
+
47
+ # Returns the SRU query value for this MMS ID.
48
+ #
49
+ # Note that currently only UC Berkeley bib numbers (encoded `UCB-bXXXXXXXXX`)
50
+ # are supported.
51
+ #
52
+ # @return [String] the SRU query value
53
+ def sru_query_value
54
+ # TODO: stop hard-coding `UCB-`
55
+ other_system_number = "UCB-#{self}-#{Config.alma_institution_code.downcase}"
56
+ "alma.other_system_number=#{other_system_number}"
57
+ end
58
+
59
+ # ------------------------------------------------------------
60
+ # Private methods
61
+
62
+ private
63
+
64
+ def split_bib(bib_number)
65
+ raise ArgumentError, "Not a Millennium bib number: #{bib_number.inspect}" unless (md = MILLENNIUM_RECORD_RE.match(bib_number.to_s))
66
+
67
+ digit_str, check_str_orig = %i[digits check].map { |part| md[part] }
68
+ check_str = ensure_check_digit(digit_str, check_str_orig)
69
+
70
+ [digit_str, check_str]
71
+ end
72
+
73
+ def ensure_check_digit(digit_str, check_str_orig)
74
+ digits = digit_str.chars.map(&:to_i)
75
+ check_digit = calculate_check_digit(digits)
76
+ return check_digit if [nil, check_digit, 'a'].include?(check_str_orig)
77
+
78
+ raise ArgumentError, "#{digit_str}#{check_str_orig} check digit invalid: expected #{check_digit}, got #{check_str_orig}"
79
+ end
80
+
81
+ def calculate_check_digit(digits)
82
+ raise ArgumentError, "Not an 8-digit array : #{digits.inspect}" unless digits.is_a?(Array) && digits.size == 8
83
+
84
+ # From: http://liwong.blogspot.com/2018/04/recipe-computing-millennium-checkdigit.html
85
+ mod = digits.reverse.each_with_index.inject(0) { |sum, (v, i)| sum + (v * (i + 2)) } % 11
86
+ mod == 10 ? 'x' : mod.to_s
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,178 @@
1
+ require 'berkeley_library/util/uris'
2
+
3
+ module BerkeleyLibrary
4
+ module Alma
5
+ class Config
6
+ ALL_SETTINGS = %i[
7
+ alma_sru_host
8
+ alma_primo_host
9
+ alma_institution_code
10
+ alma_permalink_key
11
+ ].freeze
12
+
13
+ class << self
14
+ include BerkeleyLibrary::Util
15
+
16
+ # Alma SRU hostname, e.g. UC Berkeley = berkeley.alma.exlibrisgroup.com
17
+ def alma_sru_host
18
+ @alma_sru_host ||= value_from_rails_config(:alma_sru_host)
19
+ end
20
+
21
+ # Alma institution code, e.g. UC Berkeley = 01UCS_BER
22
+ def alma_institution_code
23
+ @alma_institution_code ||= value_from_rails_config(:alma_institution_code)
24
+ end
25
+
26
+ # Alma Primo host, e.g. UC Berkeley = search.library.berkeley.edu
27
+ def alma_primo_host
28
+ @alma_primo_host ||= value_from_rails_config(:alma_primo_host)
29
+ end
30
+
31
+ # View state key to use when generating Alma permalinks, e.g. `iqob43`; see
32
+ # [What is the key in short permalinks?](https://knowledge.exlibrisgroup.com/Primo/Knowledge_Articles/What_is_the_key_in_short_permalinks%3F)
33
+ # in the documentation
34
+ def alma_permalink_key
35
+ @alma_permalink_key ||= value_from_rails_config(:alma_permalink_key)
36
+ end
37
+
38
+ def alma_sru_base_uri
39
+ ensure_configured(:alma_sru_host, :alma_institution_code)
40
+
41
+ sru_base_uri_for(alma_sru_host, alma_institution_code)
42
+ end
43
+
44
+ def alma_permalink_base_uri
45
+ ensure_configured(:alma_primo_host, :alma_institution_code, :alma_permalink_key)
46
+
47
+ primo_permalink_base_uri_for(alma_primo_host, alma_institution_code, alma_permalink_key)
48
+ end
49
+
50
+ # Sets the Alma SRU hostname
51
+ #
52
+ # @param [String] hostname the hostname
53
+ # @return [String] the hostname
54
+ # @raise ArgumentError if the hostname is nil or empty
55
+ # @raise URI::InvalidURIError if the resulting SRU URI cannot be parsed
56
+ def alma_sru_host=(hostname)
57
+ raise ArgumentError, "Invalid hostname: #{hostname.inspect}" if hostname.nil? || hostname.empty?
58
+
59
+ sru_uri = sru_base_uri_for(hostname, '') # Catch bad URIs early
60
+ @alma_sru_host = sru_uri.host
61
+ end
62
+
63
+ # Sets the Alma Primo hostname
64
+ #
65
+ # @param [String] hostname the hostname
66
+ # @return [String] the hostname
67
+ # @raise ArgumentError if the hostname is nil or empty
68
+ # @raise URI::InvalidURIError if the resulting Primo permalink URI cannot be parsed
69
+ def alma_primo_host=(hostname)
70
+ raise ArgumentError, "Invalid hostname: #{hostname.inspect}" if hostname.nil? || hostname.empty?
71
+
72
+ primo_uri = primo_permalink_base_uri_for(hostname, 'XXX', 'abc123') # Catch bad URIs early
73
+ @alma_primo_host = primo_uri.host
74
+ end
75
+
76
+ # Sets the Alma SRU institution code
77
+ #
78
+ # @param [String] inst_code the institution code
79
+ # @return [String] the institution code
80
+ # @raise ArgumentError if the institution code is nil or empty
81
+ # @raise URI::InvalidURIError if the resulting SRU URI cannot be parsed
82
+ def alma_institution_code=(inst_code)
83
+ raise ArgumentError, "Invalid institution code: #{inst_code.inspect}" if inst_code.nil? || inst_code.empty?
84
+
85
+ sru_uri = sru_base_uri_for('example.org', inst_code) # Catch bad URIs early
86
+ @alma_institution_code = sru_uri.path.split('/').last
87
+ end
88
+
89
+ # Sets the Alma permalink key
90
+ #
91
+ # @param [String] permalink_key the permalink key
92
+ # @return [String] the permalink key
93
+ # @raise ArgumentError if the permalink key is nil or empty
94
+ # @raise URI::InvalidURIError if the resulting Primo permalink URI cannot be parsed
95
+ def alma_permalink_key=(permalink_key)
96
+ raise ArgumentError, "Invalid permalink key: #{permalink_key.inspect}" if permalink_key.nil? || permalink_key.empty?
97
+
98
+ sru_uri = primo_permalink_base_uri_for('example.org', 'XXX', permalink_key) # Catch bad URIs early
99
+ @alma_permalink_key = sru_uri.path.split('/').last
100
+ end
101
+
102
+ # Returns the list of missing settings.
103
+ # @return [Array<Symbol>] the missing settings.
104
+ def missing(*settings)
105
+ settings = ALL_SETTINGS if settings.empty?
106
+ [].tap do |unset|
107
+ settings.each do |setting|
108
+ unset << setting unless set?(setting)
109
+ end
110
+ end
111
+ end
112
+
113
+ def ensure_configured(*settings)
114
+ return if (missing_settings = missing(*settings)).empty?
115
+
116
+ raise ArgumentError, "Missing #{self} configuration settings: #{missing_settings.join(', ')}"
117
+ end
118
+
119
+ def default!
120
+ BerkeleyLibrary::Alma.configure do
121
+ config.alma_sru_host = ENV.fetch('LIT_ALMA_SRU_HOST', 'berkeley.alma.exlibrisgroup.com')
122
+ config.alma_institution_code = ENV.fetch('LIT_ALMA_INSTITUTION_CODE', '01UCS_BER')
123
+ config.alma_primo_host = ENV.fetch('LIT_ALMA_PRIMO_HOST', 'search.library.berkeley.edu')
124
+ config.alma_permalink_key = ENV.fetch('LIT_ALMA_PERMALINK_KEY', 'iqob43')
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def sru_base_uri_for(domain, inst_code)
131
+ URIs.append("https://#{domain}/view/sru/", inst_code)
132
+ end
133
+
134
+ def primo_permalink_base_uri_for(alma_primo_host, inst_code, key)
135
+ URIs.append("https://#{alma_primo_host}/", 'permalink', inst_code, key)
136
+ end
137
+
138
+ # Gets the specified value from the Rails configuraiton
139
+ # @return [Object, nil] the value, or nil if there is no Rails configuration or the value is not set
140
+ def value_from_rails_config(sym)
141
+ return unless (config = rails_config)
142
+
143
+ config.send(sym)
144
+ end
145
+
146
+ def rails_config
147
+ return unless defined?(Rails)
148
+ return unless (application = Rails.application)
149
+
150
+ application.config
151
+ end
152
+
153
+ def set?(setting)
154
+ !Config.send(setting).nil?
155
+ end
156
+
157
+ def clear!
158
+ ALL_SETTINGS.each do |attr|
159
+ ivar_name = "@#{attr}"
160
+ next unless instance_variable_defined?(ivar_name)
161
+
162
+ send(:remove_instance_variable, ivar_name)
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ class << self
169
+ def config
170
+ BerkeleyLibrary::Alma::Config
171
+ end
172
+
173
+ def configure(&block)
174
+ class_eval(&block)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,16 @@
1
+ require 'berkeley_library/alma/module_info'
2
+
3
+ module BerkeleyLibrary
4
+ module Alma
5
+ module Constants
6
+ # 'x' represents a calculated check digit of 10; 'a' is a wildcard
7
+ MILLENNIUM_RECORD_RE = /^b(?<digits>[0-9]{8})(?<check>[0-9ax])?$/.freeze
8
+
9
+ # '99' is the Alma prefix for a Metadata Management System ID
10
+ # see https://knowledge.exlibrisgroup.com/Alma/Product_Documentation/010Alma_Online_Help_(English)/Metadata_Management/005Introduction_to_Metadata_Management/020Record_Numbers
11
+ ALMA_RECORD_RE = /^(?<type>99)(?<unique_part>[0-9]{9,12})(?<institution>[0-9]{4})$/.freeze
12
+
13
+ DEFAULT_USER_AGENT = "#{ModuleInfo::NAME} #{ModuleInfo::VERSION} (#{ModuleInfo::HOMEPAGE})".freeze
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,100 @@
1
+ require 'berkeley_library/util/uris'
2
+ require 'berkeley_library/alma/record_id'
3
+
4
+ module BerkeleyLibrary
5
+ module Alma
6
+ # {RecordId} subclass representing an Alma MMS ID. Note that only
7
+ # bibliographic records (prefix `99`) are supported.
8
+ #
9
+ # See [Record Numbers](https://knowledge.exlibrisgroup.com/Alma/Product_Documentation/010Alma_Online_Help_(English)/Metadata_Management/005Introduction_to_Metadata_Management/020Record_Numbers)
10
+ # in the Alma documentation.
11
+ class MMSID
12
+ include BerkeleyLibrary::Util
13
+ include RecordId
14
+
15
+ # ------------------------------------------------------------
16
+ # Constants
17
+
18
+ # The UC Berkeley prefix to the unique part of the MMS ID
19
+ UNIQ_PREFIX_UCB = '10'.freeze
20
+
21
+ # The four-digit institition code for UC berkeley
22
+ INST_CODE_UCB = '6532'.freeze
23
+
24
+ # ------------------------------------------------------------
25
+ # Accessors
26
+
27
+ # @return [String] the MMS ID, as a string
28
+ attr_reader :mms_id
29
+
30
+ # @return [String] the type prefix part of the MMS ID. Note that only bibliographic records
31
+ # (prefix `99`) are supported.
32
+ attr_reader :type_prefix
33
+
34
+ # @return [String] the unique part of the record number
35
+ attr_reader :unique_part
36
+
37
+ # @return [String] the four-digit institution code
38
+ attr_reader :institution
39
+
40
+ # ------------------------------------------------------------
41
+ # Initializer
42
+
43
+ # Initializes a new {MMSID} from a string.
44
+ #
45
+ # @param id [String] the ID string
46
+ # @raise [ArgumentError] if the specified string is not an Alma bibliographic MMS ID.
47
+ def initialize(id)
48
+ @mms_id, @type_prefix, @unique_part, @institution = parse_mms_id(id)
49
+ end
50
+
51
+ # ------------------------------------------------------------
52
+ # Instance methods
53
+
54
+ # Returns the MMS ID as a string.
55
+ #
56
+ # @return [String] the MMS ID
57
+ def to_s
58
+ mms_id
59
+ end
60
+
61
+ # Returns the permalink URI for this MMS ID.
62
+ # Requires {Config#alma_permalink_base_uri} to be set.
63
+ #
64
+ # @return [URI] the permalink URI.
65
+ def permalink_uri
66
+ URIs.append(permalink_base_uri, "alma#{mms_id}")
67
+ end
68
+
69
+ # Returns the SRU query value for this MMS ID.
70
+ #
71
+ # @return [String] the SRU query value
72
+ def sru_query_value
73
+ "alma.mms_id=#{mms_id}"
74
+ end
75
+
76
+ # Whether this ID appears to be for a Berkeley record, based on its institution code and on
77
+ # whether the unique part of the ID starts with the expected prefix for Berkeley.
78
+ #
79
+ # @return [TrueClass, FalseClass] true if this ID appears to be for a Berkeley record, false otherwise
80
+ def berkeley?
81
+ unique_part.start_with?(UNIQ_PREFIX_UCB) && institution == INST_CODE_UCB
82
+ end
83
+
84
+ # ------------------------------------------------------------
85
+ # Private methods
86
+
87
+ private
88
+
89
+ def permalink_base_uri
90
+ Config.alma_permalink_base_uri
91
+ end
92
+
93
+ def parse_mms_id(id)
94
+ raise ArgumentError, "Not an MMS ID: #{id.inspect}" unless (md = ALMA_RECORD_RE.match(id.to_s))
95
+
96
+ md.to_a
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,14 @@
1
+ module BerkeleyLibrary
2
+ module Alma
3
+ class ModuleInfo
4
+ NAME = 'berkeley_library-alma'.freeze
5
+ AUTHOR = 'David Moles'.freeze
6
+ AUTHOR_EMAIL = 'dmoles@berkeley.edu'.freeze
7
+ SUMMARY = 'Alma/Primo utilities for the UC Berkeley Library'.freeze
8
+ DESCRIPTION = 'A gem providing Alma/Primo-related utility code for the UC Berkeley Library'.freeze
9
+ LICENSE = 'MIT'.freeze
10
+ VERSION = '0.0.1'.freeze
11
+ HOMEPAGE = 'https://github.com/BerkeleyLibrary/alma'.freeze
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,113 @@
1
+ require 'berkeley_library/logging'
2
+ require 'berkeley_library/marc'
3
+ require 'berkeley_library/util/uris'
4
+ require 'berkeley_library/alma/constants'
5
+
6
+ module BerkeleyLibrary
7
+ module Alma
8
+ # Encapsulates an ID that can be used to look up records in Alma via SRU.
9
+ module RecordId
10
+ include BerkeleyLibrary::Logging
11
+ include BerkeleyLibrary::Util
12
+ include Comparable
13
+ include Constants
14
+
15
+ # ------------------------------------------------------------
16
+ # Class methods
17
+
18
+ class << self
19
+ include Constants
20
+
21
+ # Parses a string record ID and returns a {RecordId} object. For convenience,
22
+ # also accepts a {RecordId} and simply returns it, so it can be used in
23
+ # situations where it may not be clear whether the ID has already been parsed.
24
+ #
25
+ # @param id [String, RecordId] the ID to parse
26
+ # @return [RecordId, nil] an {MMSID} or {BibNumber}, depending on the type of ID,
27
+ # or `nil` if the specified `id` is neither an MMS ID nor a bib number
28
+ # @raise [ArgumentError] if the specified string is a correctly formatted Millennium
29
+ # bib number, but has an incorrect check digit
30
+ def parse(id)
31
+ # noinspection RubyMismatchedReturnType
32
+ return id if id.is_a?(RecordId)
33
+
34
+ return MMSID.new(id) if ALMA_RECORD_RE =~ id
35
+ return BibNumber.new(id) if MILLENNIUM_RECORD_RE =~ id
36
+ end
37
+ end
38
+
39
+ # ------------------------------------------------------------
40
+ # Instance methods
41
+
42
+ # Returns a URI for retrieving MARCXML from this record via SRU.
43
+ # Requires {Config#alma_sru_base_uri} to be set.
44
+ #
45
+ # @return [URI] the MARC URI
46
+ def marc_uri
47
+ query_string = URI.encode_www_form(
48
+ 'version' => '1.2',
49
+ 'operation' => 'searchRetrieve',
50
+ 'query' => sru_query_value
51
+ )
52
+
53
+ URIs.append(Config.alma_sru_base_uri, '?', query_string)
54
+ end
55
+
56
+ # Makes an SRU query for this record and returns a MARC record, or nil if the
57
+ # record is not found.
58
+ #
59
+ # Note that in the event the SRU query finds multiple records, only the first
60
+ # record is returned.
61
+ #
62
+ # @return [MARC::Record, nil] the MARC record
63
+ # rubocop:disable Naming/AccessorMethodName
64
+ def get_marc_record
65
+ marc_xml = get_marc_xml
66
+ logger.warn("GET #{marc_uri} did not return a MARC record") unless (marc_record = parse_marc_xml(marc_xml))
67
+ marc_record
68
+ end
69
+ # rubocop:enable Naming/AccessorMethodName
70
+
71
+ # Makes an SRU query for this record and returns the XML query response
72
+ # as a string.
73
+ #
74
+ # @return [String, nil] the SRU query response body, or nil in the event of an error.
75
+ # rubocop:disable Naming/AccessorMethodName
76
+ def get_marc_xml
77
+ URIs.get(marc_uri, headers: { user_agent: DEFAULT_USER_AGENT })
78
+ rescue RestClient::Exception => e
79
+ logger.warn("GET #{marc_uri} failed", e)
80
+ nil
81
+ end
82
+ # rubocop:enable Naming/AccessorMethodName
83
+
84
+ # ------------------------------------------------------------
85
+ # Comparable
86
+
87
+ # Compares this {RecordId} with another based on their string representations.
88
+ #
89
+ # @see Comparable#<=>
90
+ # @return [Integer, nil]
91
+ def <=>(other)
92
+ return 0 if equal?(other)
93
+ return unless other
94
+ return unless other.is_a?(RecordId)
95
+
96
+ to_s <=> other.to_s
97
+ end
98
+
99
+ # ------------------------------------------------------------
100
+ # Private methods
101
+
102
+ private
103
+
104
+ def parse_marc_xml(xml)
105
+ return unless xml
106
+
107
+ input = StringIO.new(xml.scrub)
108
+ reader = MARC::XMLReader.new(input)
109
+ reader.first
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1 @@
1
+ Dir.glob(File.expand_path('alma/*.rb', __dir__)).sort.each(&method(:require))
@@ -0,0 +1,8 @@
1
+ namespace :bundle do
2
+ desc 'Updates the ruby-advisory-db then runs bundle-audit'
3
+ task :audit do
4
+ require 'bundler/audit/cli'
5
+ Bundler::Audit::CLI.start ['update']
6
+ Bundler::Audit::CLI.start %w[check --ignore CVE-2015-9284]
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ require 'ci/reporter/rake/rspec'
2
+
3
+ # Configure CI::Reporter report generation
4
+ ENV['GENERATE_REPORTS'] ||= 'true'
5
+ ENV['CI_REPORTS'] = 'artifacts/rspec'
6
+
7
+ desc 'Run all specs in spec directory, with coverage'
8
+ task coverage: ['ci:setup:rspec'] do
9
+ ENV['COVERAGE'] ||= 'true'
10
+ Rake::Task[:spec].invoke
11
+ end