berkeley_library-alma 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|