WIP towards Alma SRU model#404
Open
matt-bernhardt wants to merge 3 commits into
Open
Conversation
Coverage Report for CI Build 27219406924Coverage increased (+0.07%) to 98.384%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
** 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.
There was a problem hiding this comment.
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.lookupwith 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.qltyartifacts.
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 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 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' | ||
| } |
- 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
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 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 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
AlmaSruin 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
New ENV
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
Additional context needed to review
There are a few questions I'd like to ask be considered during code review:
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)
Copilot is consistently flagging that
validate_alma_idis 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.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
added technical debt.
Documentation
(not just this pull request message).
Testing