Skip to content

WIP towards Alma SRU model#404

Open
matt-bernhardt wants to merge 3 commits into
mainfrom
use-598
Open

WIP towards Alma SRU model#404
matt-bernhardt wants to merge 3 commits into
mainfrom
use-598

Conversation

@matt-bernhardt

@matt-bernhardt matt-bernhardt commented Jun 5, 2026

Copy link
Copy Markdown
Member

Why are these changes being introduced:

For performance reasons, we stripped out holdings information from our initial Primo API call. However, we do want to display these holdings to users where they are available - so we need to add them back as an async API lookup.

Relevant ticket:

https://mitlibraries.atlassian.net/browse/use-598

How does this address that need:

This adds an AlmaSru model that will handle these async lookups. This ticket calls for only the model, the rest of the integration will happen in subsequent tickets.

The model provides a lookup method which accepts an identifier argument. It validates that identifier, submits it to the Alma SRU endpoint, receives the result, and parses that response into a human-friendly format.

PLEASE NOTE: There is no visible impact to this PR in a browser. You will need to interact with AlmaSru in a console, either within Heroku in the PR build, or locally in your terminal.

Document any side effects to this change:

We consult a variety of external APIs in this application, and each of them follows slightly different patterns. Hopefully this does not exacerbate that range terribly.

Developer

Accessibility
  • ANDI or WAVE has been run in accordance to our guide.
  • This PR contains no changes to the view layer.
  • New issues flagged by ANDI or WAVE have been resolved.
  • New issues flagged by ANDI or WAVE have been ticketed (link in the Pull Request details above).
  • No new accessibility issues have been flagged.
New ENV
  • All new ENV is documented in README.
  • All new ENV has been added to Heroku Pipeline, Staging and Prod.
  • ENV has not changed.

Please note: - this implementation does newly rely on an already-defined ENV variable, so you'll see a change in .env.test. The value itself is already in this file in several places, so I don't believe it is a concern to add it in the way I'm doing here.

Approval beyond code review
  • UXWS/stakeholder approval has been confirmed.
  • UXWS/stakeholder review will be completed retroactively.
  • UXWS/stakeholder review is not needed.
Additional context needed to review

There are a few questions I'd like to ask be considered during code review:

  1. What behavior do we want if any of the relied-upon subfields is ever not present? The phrase from Primo that I'm aiming to replicate is, apparently, e q c (d) - and all the records I've inspected during testing have had these populated (along with a range of others that are hit-or-miss). For the moment, I've written a test making clear that each of these is treated as optional - which results in a rather non-sensical statement if all four are ever blank.

In an alternative case, I could define each of these four as required during processing, and return a placeholder statement when warranted (either because one value is missing, some percentage of values is missing, etc)

  1. Copilot is consistently flagging that validate_alma_id is too permissive, because it doesn't flag characters in the value beyond the "alma" prefix. I've written a test in the suite to confirm that characters like this don't cause a breakage, which I think is sufficient - but I'm open to a counterargument here that we should be enforcing an all-numeric format.

  2. I'm proposing a test pattern for certain events to get logged for local debugging, but this is starting to feel like it is more trouble than it is worth. I'd like to have some visibility into certain outcomes from this model, but I don't feel confident that any of the available options are what we need. Sentry is overkill, I don't want to pollute the log stream in production with error messages that aren't actually errors, and confirming that the debug messages are working as intended is proving challenging without causing other issues (see Copilot messages).

There are other Copilot and Qlty flags being thrown, but I'm not convinced any of them are worth considering at this point, and I'm wary of re-submitting another round of changes only to hear about new issues on unchanged lines of code.

Code Reviewer

Code
  • I have confirmed that the code works as intended.
  • Any CodeClimate issues have been fixed or confirmed as
    added technical debt.
Documentation
  • The commit message is clear and follows our guidelines
    (not just this pull request message).
  • The documentation has been updated or is unnecessary.
  • New dependencies are appropriate or there were no changes.
Testing
  • There are appropriate tests covering any new functionality.
  • No additional test coverage is required.

@coveralls

coveralls commented Jun 5, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 27219406924

Coverage increased (+0.07%) to 98.384%

Details

  • Coverage increased (+0.07%) from the base build.
  • Patch coverage: 57 of 57 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1485
Covered Lines: 1461
Line Coverage: 98.38%
Coverage Strength: 72.54 hits per line

💛 - Coveralls

@mitlib mitlib temporarily deployed to timdex-ui-pi-use-598-bk9oj2w2g June 5, 2026 19:33 Inactive
** Why are these changes being introduced:

For performance reasons, we stripped out holdings information from our
initial Primo API call. However, we do want to display these holdings to
users where they are available - so we need to add them back as an async
API lookup.

** Relevant ticket(s):

* https://mitlibraries.atlassian.net/browse/use-598

** How does this address that need:

This adds an AlmaSru model that will handle these async lookups. This
ticket calls for only the model, the rest of the integration will happen
in subsequent tickets.

The model provides a .lookup method which accepts an identifier argument.
It validates that identifier, submits it to the Alma SRU endpoint,
receives the result, and parses that response into a human-friendly
format.

** Document any side effects to this change:

We consult a variety of external APIs in this application, and each of
them follows slightly different patterns. Hopefully this does not
exacerbate that range terribly.
@mitlib mitlib temporarily deployed to timdex-ui-pi-use-598-bk9oj2w2g June 9, 2026 14:24 Inactive
@matt-bernhardt matt-bernhardt marked this pull request as ready for review June 9, 2026 14:25
@matt-bernhardt matt-bernhardt requested a review from Copilot June 9, 2026 14:26
@qltysh

qltysh Bot commented Jun 9, 2026

Copy link
Copy Markdown

All good ✅

Comment thread app/models/alma_sru.rb

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an AlmaSru model to support asynchronous holdings/availability lookups against Alma’s SRU endpoint, returning user-displayable availability strings. This fits alongside existing external-API models (LibKey/BrowZine/OpenAlex/etc.) and sets up the groundwork for later UI integration.

Changes:

  • Introduces AlmaSru.lookup with SRU request/response parsing and availability formatting.
  • Adds model test coverage using new XML fixtures and VCR cassettes for SRU scenarios (single, multiple, none, nonexistent).
  • Adds new test ENV values (MIT_ALMA_URL, EXL_INST_ID) and ignores .qlty artifacts.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
app/models/alma_sru.rb New Alma SRU client/model with ID validation, request building, XML parsing, and formatting.
test/models/alma_sru_test.rb Unit tests for Alma SRU lookup/validation/parsing + error handling.
test/vcr_cassettes/alma_sru_single_record.yml VCR cassette for successful SRU lookup with one availability.
test/vcr_cassettes/alma_sru_multiple_records.yml VCR cassette for SRU response containing multiple AVA entries.
test/vcr_cassettes/alma_sru_no_availability.yml VCR cassette for SRU response where availability tags don’t produce user-facing output.
test/vcr_cassettes/alma_sru_nonexistent_record.yml VCR cassette for SRU response with numberOfRecords=0.
test/fixtures/alma/sru_success.xml XML fixture used to test controlfield extraction.
test/fixtures/alma/sru_nocontrol.xml XML fixture for missing controlfield(001) behavior.
test/fixtures/alma/sru_wrong_order.xml XML fixture for verifying availability reordering logic.
.env.test Adds Alma SRU-related env vars for test runs (EXL_INST_ID, MIT_ALMA_URL).
.gitignore Ignores .qlty directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/models/alma_sru.rb
Comment on lines +74 to +90
# validate_alma_id ensures we are only submitting valid Alma IDs to the SRU endpoint.
#
# It needs to do two thigns:
# 1. Remove the "alma" prefix if one is present. Otherwise, no manipulation of the submitted value should occur.
# 2. Enforce the formatting requirements for a valid alma identifier (start with "99", and end with "6761").
def self.validate_alma_id(raw)
parsed = if raw.start_with?('alma')
raw.delete_prefix('alma')
else
raw
end

raise InvalidAlmaId unless parsed.to_s.start_with?('99')
raise InvalidAlmaId unless parsed.to_s.end_with?('6761')

parsed
end
Comment thread app/models/alma_sru.rb
Comment thread app/models/alma_sru.rb
Comment thread test/models/alma_sru_test.rb
Comment thread test/models/alma_sru_test.rb
Comment thread test/models/alma_sru_test.rb
Comment thread test/models/alma_sru_test.rb Outdated
Comment thread test/models/alma_sru_test.rb Outdated
Comment thread .env.test
Comment on lines +221 to +225
# Is this the behavior we want?
test 'format_availability does not error with missing fields' do
ava_hash = {
'b' => 'beta'
}
@mitlib mitlib temporarily deployed to timdex-ui-pi-use-598-bk9oj2w2g June 9, 2026 15:22 Inactive
Comment thread app/models/alma_sru.rb

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Comment thread app/models/alma_sru.rb
Comment thread app/models/alma_sru.rb Outdated
Comment thread test/vcr_cassettes/alma_sru_single_record.yml Outdated
Comment thread test/vcr_cassettes/alma_sru_multiple_records.yml Outdated
Comment thread test/vcr_cassettes/alma_sru_no_availability.yml Outdated
Comment thread test/vcr_cassettes/alma_sru_nonexistent_record.yml Outdated
- Add env vars to README
- Refactor guard clause for env into separate method, expand to include
  EXL_INST_ID
- Fix typo in method comment
- Restore logger to original state using begin/ensure blocks for those
  tests
- Adds tests for updated guard clause
- Update test names to reflect empty list return, not nil return
- More robust test for env vars includes a confirmation of initial success
- Adds test helper for scrubbing cassettes of cookie values
- Scrubs cassettes

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.

Comment thread app/models/alma_sru.rb
Comment on lines +79 to +90
def self.validate_alma_id(raw)
parsed = if raw.start_with?('alma')
raw.delete_prefix('alma')
else
raw
end

raise InvalidAlmaId unless parsed.to_s.start_with?('99')
raise InvalidAlmaId unless parsed.to_s.end_with?('6761')

parsed
end
Comment thread app/models/alma_sru.rb
Comment on lines +94 to +102
def self.ava_to_hash(node)
rebuilt = {}

node.children.each do |child|
rebuilt[child.attribute_nodes[0].value] = child.text if child.instance_of?(Nokogiri::XML::Element)
end

rebuilt
end
Comment thread app/models/alma_sru.rb
Comment on lines +124 to +126
def self.format_availability(availability)
"#{availability['e']&.humanize} at #{availability['q']} #{availability['c']} (#{availability['d']})"
end
Comment on lines +68 to +87
test 'lookup returns empty list for non-existent records' do
output = StringIO.new
original_logger = Rails.logger
Rails.logger = Logger.new(output)

begin
VCR.use_cassette('alma sru nonexistent record') do
needle = 'alma9900000000006761'

result = AlmaSru.lookup(needle)

assert_equal(result, [])

# This condition gets logged for local investigation due to control field mismatch
assert_match('Alma lookup failure: Control field mismatch', output.string)
end
ensure
Rails.logger = original_logger
end
end
Comment on lines +121 to +141
test 'lookup survives failing to connect to Alma SRU' do
alma_client = AlmaConnectionError.new
output = StringIO.new
original_logger = Rails.logger
Rails.logger = Logger.new(output)

needle = 'alma990014651640106761'

begin
assert_nothing_raised do
result = AlmaSru.lookup(needle, alma_client: alma_client)

assert_equal([], result)

# This condition gets logged for local investigation
assert_match('Alma SRU connection error', output.string)
end
ensure
Rails.logger = original_logger
end
end
Comment on lines +143 to +163
test 'lookup survives Alma SRU errors' do
alma_client = AlmaErrorResponse.new
output = StringIO.new
original_logger = Rails.logger
Rails.logger = Logger.new(output)

needle = 'alma990014651640106761'

begin
assert_nothing_raised do
result = AlmaSru.lookup(needle, alma_client: alma_client)

assert_equal(result, [])

# This condition gets logged for local investigation
assert_match('Alma lookup failure: 500', output.string)
end
ensure
Rails.logger = original_logger
end
end
Comment on lines +41 to +44
result = AlmaSru.lookup(needle)

assert_equal(result, ['Available at Rotch Library Stacks (NA680.C25 2007)'])
end
@JPrevost JPrevost self-assigned this Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants