berkeley_library-alma 0.0.1

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