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.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +18 -0
- data/.gitignore +388 -0
- data/.idea/.gitignore +8 -0
- data/.idea/alma.iml +55 -0
- data/.idea/codeStyles/codeStyleConfig.xml +5 -0
- data/.idea/inspectionProfiles/Project_Default.xml +26 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rubocop.yml +334 -0
- data/.ruby-version +1 -0
- data/.simplecov +8 -0
- data/.yardopts +2 -0
- data/CHANGES.md +3 -0
- data/Dockerfile +54 -0
- data/Gemfile +3 -0
- data/Jenkinsfile +18 -0
- data/LICENSE.md +21 -0
- data/README.md +210 -0
- data/Rakefile +20 -0
- data/berkeley_library-alma.gemspec +42 -0
- data/bin/alma-mms-lookup +64 -0
- data/docker-compose.yml +15 -0
- data/lib/berkeley_library/alma/bib_number.rb +90 -0
- data/lib/berkeley_library/alma/config.rb +178 -0
- data/lib/berkeley_library/alma/constants.rb +16 -0
- data/lib/berkeley_library/alma/mms_id.rb +100 -0
- data/lib/berkeley_library/alma/module_info.rb +14 -0
- data/lib/berkeley_library/alma/record_id.rb +113 -0
- data/lib/berkeley_library/alma.rb +1 -0
- data/rakelib/bundle.rake +8 -0
- data/rakelib/coverage.rake +11 -0
- data/rakelib/gem.rake +54 -0
- data/rakelib/rubocop.rake +18 -0
- data/rakelib/spec.rake +2 -0
- data/spec/.rubocop.yml +37 -0
- data/spec/data/991054360089706532-sru.xml +186 -0
- data/spec/data/b11082434-sru.xml +165 -0
- data/spec/data/bibs_with_check_digits.txt +151 -0
- data/spec/lib/berkeley_library/alma/bib_number_spec.rb +95 -0
- data/spec/lib/berkeley_library/alma/config_spec.rb +94 -0
- data/spec/lib/berkeley_library/alma/mms_id_spec.rb +111 -0
- data/spec/lib/berkeley_library/alma/record_id_spec.rb +41 -0
- data/spec/spec_helper.rb +56 -0
- 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
|
data/bin/alma-mms-lookup
ADDED
@@ -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
|
data/docker-compose.yml
ADDED
@@ -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))
|
data/rakelib/bundle.rake
ADDED
@@ -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
|