From 440ebd29a163095fbb293627e88ff6daf9cf4daa Mon Sep 17 00:00:00 2001 From: Shana Moore Date: Wed, 16 Sep 2026 15:21:15 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20URI=20values=20in?= =?UTF-8?q?=20Bulkrax=20subject=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bulkrax's ApplicationMatcher#parse_subject lowercases the entire value then capitalizes the first character. This turns "http://id.loc.gov/..." into "Http://id.loc.gov/...", breaking every downstream check that pattern-matches on the URI scheme, including UtkUriLabelIndexing. Decorate parse_subject to return URIs verbatim while still applying sentence-casing to plain text subjects. The regex guards on http(s):// which covers all URI schemes in UTK's controlled vocabulary fields. Ref: - #73 Assisted by: Claude Opus 4.6 --- .../bulkrax/application_matcher_decorator.rb | 21 +++++++++ .../application_matcher_decorator_spec.rb | 43 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 app/matchers/bulkrax/application_matcher_decorator.rb create mode 100644 spec/matchers/bulkrax/application_matcher_decorator_spec.rb diff --git a/app/matchers/bulkrax/application_matcher_decorator.rb b/app/matchers/bulkrax/application_matcher_decorator.rb new file mode 100644 index 0000000..ee728c0 --- /dev/null +++ b/app/matchers/bulkrax/application_matcher_decorator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Bulkrax + # OVERRIDE Bulkrax 9.5.1: parse_subject lowercases the entire value then + # capitalises the first character ("Sentence case"). That mangles URIs: + # "http://id.loc.gov/…" becomes "Http://id.loc.gov/…", breaking every + # downstream check that pattern-matches on the scheme. + # + # Return URIs verbatim; apply the original sentence-casing only to plain + # text subjects. + module ApplicationMatcherDecorator + def parse_subject(src) + return if src.blank? + return src.strip if src.strip.match?(%r{\Ahttps?://}i) + + super + end + end +end + +Bulkrax::ApplicationMatcher.prepend(Bulkrax::ApplicationMatcherDecorator) diff --git a/spec/matchers/bulkrax/application_matcher_decorator_spec.rb b/spec/matchers/bulkrax/application_matcher_decorator_spec.rb new file mode 100644 index 0000000..d601b0c --- /dev/null +++ b/spec/matchers/bulkrax/application_matcher_decorator_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Bulkrax::ApplicationMatcherDecorator do + subject(:matcher) { Bulkrax::ApplicationMatcher.new(to: 'subject', parsed: true, split: nil, if: nil, excluded: false, nested_type: nil) } + + describe '#parse_subject' do + it 'preserves http URIs verbatim' do + uri = 'http://id.loc.gov/authorities/subjects/sh85101348' + expect(matcher.parse_subject(uri)).to eq(uri) + end + + it 'preserves https URIs verbatim' do + uri = 'https://id.loc.gov/authorities/subjects/sh85101348' + expect(matcher.parse_subject(uri)).to eq(uri) + end + + it 'strips whitespace from URIs' do + expect(matcher.parse_subject(' http://id.loc.gov/authorities/subjects/sh85101348 ')) + .to eq('http://id.loc.gov/authorities/subjects/sh85101348') + end + + it 'still sentence-cases plain text subjects' do + expect(matcher.parse_subject('photography')).to eq('Photography') + end + + it 'still sentence-cases multi-word plain text' do + expect(matcher.parse_subject('CIVIL WAR')).to eq('Civil war') + end + + it 'returns nil for blank values' do + expect(matcher.parse_subject('')).to be_nil + end + end + + describe 'integration with #result' do + it 'does not capitalize URIs when processing through the full matcher pipeline' do + result = matcher.result(nil, 'http://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to eq('http://id.loc.gov/authorities/subjects/sh85101348') + end + end +end From 7bb2b37d3955eec70618402131ed4e3b7ad629c1 Mon Sep 17 00:00:00 2001 From: Shana Moore Date: Thu, 17 Sep 2026 11:52:40 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20Downcase=20URI=20schemes=20a?= =?UTF-8?q?nd=20normalize=20capitalized=20URIs=20in=20parse=5Fsubject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downcase the entire URI instead of returning it verbatim so that capitalized schemes from either Bulkrax's sentence-casing or from the CSV itself (Http://, HTTP://) are normalized to lowercase, which UriLabelResolver requires to resolve labels. Assisted by: Claude Opus 4.6 --- .../bulkrax/application_matcher_decorator.rb | 8 +++-- .../application_matcher_decorator_spec.rb | 35 +++++++++++++++++-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/matchers/bulkrax/application_matcher_decorator.rb b/app/matchers/bulkrax/application_matcher_decorator.rb index ee728c0..ca97eda 100644 --- a/app/matchers/bulkrax/application_matcher_decorator.rb +++ b/app/matchers/bulkrax/application_matcher_decorator.rb @@ -6,12 +6,14 @@ module Bulkrax # "http://id.loc.gov/…" becomes "Http://id.loc.gov/…", breaking every # downstream check that pattern-matches on the scheme. # - # Return URIs verbatim; apply the original sentence-casing only to plain - # text subjects. + # Normalize the URI scheme to lowercase, preserving the rest of the URI; + # apply the original sentence-casing only to plain text subjects. module ApplicationMatcherDecorator def parse_subject(src) return if src.blank? - return src.strip if src.strip.match?(%r{\Ahttps?://}i) + + stripped = src.strip + return stripped.sub(%r{\Ahttps?}i, &:downcase) if stripped.match?(%r{\Ahttps?://}i) super end diff --git a/spec/matchers/bulkrax/application_matcher_decorator_spec.rb b/spec/matchers/bulkrax/application_matcher_decorator_spec.rb index d601b0c..9b3760b 100644 --- a/spec/matchers/bulkrax/application_matcher_decorator_spec.rb +++ b/spec/matchers/bulkrax/application_matcher_decorator_spec.rb @@ -16,6 +16,32 @@ expect(matcher.parse_subject(uri)).to eq(uri) end + it 'keeps the scheme lowercase so UriLabelResolver can resolve it' do + result = matcher.parse_subject('http://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to start_with('http://') + end + + it 'keeps the https scheme lowercase so UriLabelResolver can resolve it' do + result = matcher.parse_subject('https://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to start_with('https://') + end + + it 'normalizes a capitalized Http scheme from the CSV' do + result = matcher.parse_subject('Http://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to start_with('http://') + end + + it 'normalizes a capitalized Https scheme from the CSV' do + result = matcher.parse_subject('Https://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to start_with('https://') + end + + it 'downcases only the scheme of an all-caps URI, preserving the path' do + result = matcher.parse_subject('HTTP://ID.LOC.GOV/AUTHORITIES/SUBJECTS/SH85101348') + expect(result).to start_with('http://') + expect(result).to eq('http://ID.LOC.GOV/AUTHORITIES/SUBJECTS/SH85101348') + end + it 'strips whitespace from URIs' do expect(matcher.parse_subject(' http://id.loc.gov/authorities/subjects/sh85101348 ')) .to eq('http://id.loc.gov/authorities/subjects/sh85101348') @@ -35,9 +61,14 @@ end describe 'integration with #result' do - it 'does not capitalize URIs when processing through the full matcher pipeline' do + it 'preserves URI scheme through the full matcher pipeline so UriLabelResolver can resolve it' do result = matcher.result(nil, 'http://id.loc.gov/authorities/subjects/sh85101348') - expect(result).to eq('http://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to start_with('http://') + end + + it 'normalizes a capitalized URI scheme through the full matcher pipeline' do + result = matcher.result(nil, 'Http://id.loc.gov/authorities/subjects/sh85101348') + expect(result).to start_with('http://') end end end