From 9394f8afc4035b40d63f6373bba6f5d6d8b80ee6 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Thu, 4 Dec 2025 14:49:45 -0500 Subject: [PATCH 1/8] FOLIOSYNC-12 create HyacinthSynchronizer class to handle downloading and syncing logic --- .../hyacinth_synchronizer.rb | 110 ++++++++++++++++++ lib/tasks/hyacinth_sync.rake | 58 +++++---- 2 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb diff --git a/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb b/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb new file mode 100644 index 0000000..f6ebae6 --- /dev/null +++ b/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module FolioSync + module FolioToHyacinth + class HyacinthSynchronizer + attr_reader :downloading_errors, :syncing_errors + + def initialize + # @logger = Logger.new($stdout) + end + + # Performs MARC downloads and syncs resources to Hyacinth + # @param [Integer] last_x_hours Records newer than this are synced. + def download_and_sync_folio_to_hyacinth_records(last_x_hours) + download_marc_from_folio(last_x_hours) + prepare_hyacinth_records + end + + def download_marc_from_folio(last_x_hours) + downloader = FolioSync::FolioToHyacinth::MarcDownloader.new + downloader.download_965hyacinth_marc_records(last_x_hours) + + return if downloader.downloading_errors.blank? + + puts "Error downloading MARC records from FOLIO: #{downloader.downloading_errors}" + @downloading_errors = downloader.downloading_errors + end + + def prepare_hyacinth_records + marc_files = Dir.glob(downloaded_marc_files_path) + puts "Processing #{marc_files.count} MARC files" + + marc_files.each do |marc_file_path| + process_marc_file(marc_file_path) + end + end + + private + + def downloaded_marc_files_path + "#{Rails.configuration.folio_to_hyacinth[:download_directory]}/*.mrc" + end + + def process_marc_file(marc_file_path) + folio_hrid = extract_hrid_from_filename(marc_file_path) + hyacinth_results = fetch_hyacinth_results(marc_file_path) + puts "Found #{hyacinth_results.length} Hyacinth records for FOLIO HRID #{folio_hrid}" + + case hyacinth_results.length + when 0 + create_new_hyacinth_record(marc_file_path, folio_hrid) + when 1 + update_existing_hyacinth_record(marc_file_path, hyacinth_results.first, folio_hrid) + else + handle_multiple_records_error(folio_hrid) + end + rescue StandardError => e + puts "Failed to process #{folio_hrid}: #{e.message}" + @syncing_errors << "Error processing #{folio_hrid}: #{e.message}" + end + + def extract_hrid_from_filename(marc_file_path) + File.basename(marc_file_path, '.mrc') + end + + def create_new_hyacinth_record(marc_file_path, folio_hrid) + puts "Creating new Hyacinth record for #{folio_hrid}" + + new_record = FolioToHyacinthRecord.new(marc_file_path) + response = FolioSync::Hyacinth::Client.instance.create_new_record( + new_record.digital_object_data, + publish: true + ) + + puts "Created record for #{folio_hrid}: #{response.inspect}" + response + end + + def update_existing_hyacinth_record(marc_file_path, existing_record, folio_hrid) + puts "Updating existing Hyacinth record for #{folio_hrid}" + + preserved_data = { 'identifiers' => existing_record['identifiers'] } + updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) + + response = FolioSync::Hyacinth::Client.instance.update_existing_record( + existing_record['pid'], + updated_record.digital_object_data, + publish: true + ) + + puts "Updated record #{existing_record['pid']}: #{response.inspect}" + response + end + + def handle_multiple_records_error(folio_hrid) + error_message = "Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}" + puts error_message + @syncing_errors << error_message + end + + def fetch_hyacinth_results(marc_file_path) + folio_hrid = File.basename(marc_file_path, '.mrc') + potential_clio_identifier = "clio#{folio_hrid}" + client = FolioSync::Hyacinth::Client.instance + client.find_by_identifier(potential_clio_identifier, + { f: { digital_object_type_display_label_sim: ['Item'] } }) + end + end + end +end diff --git a/lib/tasks/hyacinth_sync.rake b/lib/tasks/hyacinth_sync.rake index 712d168..7d91196 100644 --- a/lib/tasks/hyacinth_sync.rake +++ b/lib/tasks/hyacinth_sync.rake @@ -3,26 +3,32 @@ namespace :folio_sync do namespace :folio_to_hyacinth do task run: :environment do - modified_since = ENV['modified_since'] - modified_since_num = - if modified_since && !modified_since.strip.empty? - begin - Integer(modified_since) - rescue ArgumentError - puts 'Error: modified_since must be an integer (number of hours).' - exit 1 - end - end - - downloader = FolioSync::FolioToHyacinth::MarcDownloader.new - downloader.download_965hyacinth_marc_records(modified_since_num) - - if downloader.downloading_errors.present? - puts "Errors encountered during MARC download: #{downloader.downloading_errors}" - exit 1 - end + puts 'Starting Folio to Hyacinth sync task...' + synchronizer = FolioSync::FolioToHyacinth::HyacinthSynchronizer.new + synchronizer.download_and_sync_folio_to_hyacinth_records(24) end + # task run: :environment do + # modified_since = ENV['modified_since'] + # modified_since_num = + # if modified_since && !modified_since.strip.empty? + # begin + # Integer(modified_since) + # rescue ArgumentError + # puts 'Error: modified_since must be an integer (number of hours).' + # exit 1 + # end + # end + + # downloader = FolioSync::FolioToHyacinth::MarcDownloader.new + # downloader.download_965hyacinth_marc_records(modified_since_num) + + # if downloader.downloading_errors.present? + # puts "Errors encountered during MARC download: #{downloader.downloading_errors}" + # exit 1 + # end + # end + task download_single_file: :environment do FolioSync::Rake::EnvValidator.validate!( ['hrid'], @@ -62,10 +68,14 @@ namespace :folio_sync do pid = results.first['pid'] puts "Found 1 record with pid: #{pid}." + puts results.first.inspect + # Get only the data needed for update preserved_data = { 'identifiers' => results.first['identifiers'] } updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) puts "Updated record digital object data: #{updated_record.digital_object_data}" + + # return response = client.update_existing_record(pid, updated_record.digital_object_data, publish: true) puts "Response from Hyacinth when updating record #{pid}: #{response.inspect}" else @@ -91,7 +101,7 @@ namespace :folio_sync do end # Add 965p field with value academic_commons, ensure 965$a is set to 965hyacinth - marc_record.append(MARC::DataField.new('965', ' ', ' ', ['a', '965hyacinth'], ['p', 'academic_commons'], ['p', 'test'])) + marc_record.append(MARC::DataField.new('965', ' ', ' ', ['a', '965hyacinth'], ['p', 'Test'])) puts "Modified MARC record with new 965 field: #{marc_record.inspect}" new_filepath = Rails.root.join(Rails.configuration.folio_to_hyacinth[:download_directory], 'modified_marc.mrc') @@ -100,7 +110,15 @@ namespace :folio_sync do writer.write(marc_record) writer.close end - puts "Final MARC record: #{marc_record.inspect}" + # puts "Final MARC record: #{marc_record}" + + reader = MARC::Reader.new(new_filepath.to_s) + reader.each do |record| + # Get author fields by supplying a list of tags + record.fields.each_by_tag(['965']) do |field| + puts field + end + end end task create_new_hyacinth_record: :environment do From ee65b7bfc830fcd67d7d900ce175c30c82ec1432 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Fri, 5 Dec 2025 16:28:05 -0500 Subject: [PATCH 2/8] FOLIOSYNC-12 extract out processing logic to a new MarcProcessor class --- .../hyacinth_synchronizer.rb | 75 +++--------------- .../folio_to_hyacinth/marc_processor.rb | 76 +++++++++++++++++++ lib/tasks/hyacinth_sync.rake | 61 ++++++++++----- 3 files changed, 126 insertions(+), 86 deletions(-) create mode 100644 lib/folio_sync/folio_to_hyacinth/marc_processor.rb diff --git a/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb b/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb index f6ebae6..49d560d 100644 --- a/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb +++ b/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb @@ -6,23 +6,28 @@ class HyacinthSynchronizer attr_reader :downloading_errors, :syncing_errors def initialize - # @logger = Logger.new($stdout) + @logger = Logger.new($stdout) end # Performs MARC downloads and syncs resources to Hyacinth # @param [Integer] last_x_hours Records newer than this are synced. def download_and_sync_folio_to_hyacinth_records(last_x_hours) - download_marc_from_folio(last_x_hours) + # download_marc_from_folio(last_x_hours) prepare_hyacinth_records end + def clear_downloads! + @logger.info('Clearing downloaded MARC files...') + FileUtils.rm_rf(downloaded_marc_files_path) + end + def download_marc_from_folio(last_x_hours) downloader = FolioSync::FolioToHyacinth::MarcDownloader.new downloader.download_965hyacinth_marc_records(last_x_hours) return if downloader.downloading_errors.blank? - puts "Error downloading MARC records from FOLIO: #{downloader.downloading_errors}" + @logger.error("Error downloading MARC records from FOLIO: #{downloader.downloading_errors}") @downloading_errors = downloader.downloading_errors end @@ -42,68 +47,8 @@ def downloaded_marc_files_path end def process_marc_file(marc_file_path) - folio_hrid = extract_hrid_from_filename(marc_file_path) - hyacinth_results = fetch_hyacinth_results(marc_file_path) - puts "Found #{hyacinth_results.length} Hyacinth records for FOLIO HRID #{folio_hrid}" - - case hyacinth_results.length - when 0 - create_new_hyacinth_record(marc_file_path, folio_hrid) - when 1 - update_existing_hyacinth_record(marc_file_path, hyacinth_results.first, folio_hrid) - else - handle_multiple_records_error(folio_hrid) - end - rescue StandardError => e - puts "Failed to process #{folio_hrid}: #{e.message}" - @syncing_errors << "Error processing #{folio_hrid}: #{e.message}" - end - - def extract_hrid_from_filename(marc_file_path) - File.basename(marc_file_path, '.mrc') - end - - def create_new_hyacinth_record(marc_file_path, folio_hrid) - puts "Creating new Hyacinth record for #{folio_hrid}" - - new_record = FolioToHyacinthRecord.new(marc_file_path) - response = FolioSync::Hyacinth::Client.instance.create_new_record( - new_record.digital_object_data, - publish: true - ) - - puts "Created record for #{folio_hrid}: #{response.inspect}" - response - end - - def update_existing_hyacinth_record(marc_file_path, existing_record, folio_hrid) - puts "Updating existing Hyacinth record for #{folio_hrid}" - - preserved_data = { 'identifiers' => existing_record['identifiers'] } - updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) - - response = FolioSync::Hyacinth::Client.instance.update_existing_record( - existing_record['pid'], - updated_record.digital_object_data, - publish: true - ) - - puts "Updated record #{existing_record['pid']}: #{response.inspect}" - response - end - - def handle_multiple_records_error(folio_hrid) - error_message = "Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}" - puts error_message - @syncing_errors << error_message - end - - def fetch_hyacinth_results(marc_file_path) - folio_hrid = File.basename(marc_file_path, '.mrc') - potential_clio_identifier = "clio#{folio_hrid}" - client = FolioSync::Hyacinth::Client.instance - client.find_by_identifier(potential_clio_identifier, - { f: { digital_object_type_display_label_sim: ['Item'] } }) + processor = FolioSync::FolioToHyacinth::MarcProcessor.new(marc_file_path) + processor.create_and_sync_hyacinth_record! end end end diff --git a/lib/folio_sync/folio_to_hyacinth/marc_processor.rb b/lib/folio_sync/folio_to_hyacinth/marc_processor.rb new file mode 100644 index 0000000..3c4f3db --- /dev/null +++ b/lib/folio_sync/folio_to_hyacinth/marc_processor.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class FolioSync::FolioToHyacinth::MarcProcessor + def initialize(marc_file_path) + @marc_file_path = marc_file_path + @logger = Logger.new($stdout) + @syncing_errors = [] + end + + def create_and_sync_hyacinth_record! + folio_hrid = extract_hrid_from_filename(@marc_file_path) + hyacinth_results = fetch_hyacinth_results(@marc_file_path) + @logger.info("Found #{hyacinth_results.length} Hyacinth records for FOLIO HRID #{folio_hrid}") + + case hyacinth_results.length + when 0 + create_new_hyacinth_record(@marc_file_path, folio_hrid) + when 1 + update_existing_hyacinth_record(@marc_file_path, hyacinth_results.first, folio_hrid) + else + handle_multiple_records_error(folio_hrid) + end + rescue StandardError => e + @logger.error("Failed to process #{folio_hrid}: #{e.message}") + @syncing_errors << "Error processing #{folio_hrid}: #{e.message}" + end + + def update_existing_hyacinth_record(marc_file_path, existing_record, folio_hrid) + @logger.info("Updating existing Hyacinth record for #{folio_hrid}") + + preserved_data = { 'identifiers' => existing_record['identifiers'] } + updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) + + response = FolioSync::Hyacinth::Client.instance.update_existing_record( + existing_record['pid'], + updated_record.digital_object_data, + publish: true + ) + + @logger.info("Updated record #{existing_record['pid']}: #{response.inspect}") + response + end + + def handle_multiple_records_error(folio_hrid) + error_message = "Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}" + @logger.error(error_message) + @syncing_errors << error_message + end + + def fetch_hyacinth_results(marc_file_path) + folio_hrid = File.basename(marc_file_path, '.mrc') + potential_clio_identifier = "clio#{folio_hrid}" + client = FolioSync::Hyacinth::Client.instance + client.find_by_identifier(potential_clio_identifier, + { f: { digital_object_type_display_label_sim: ['Item'] } }) + end + + def extract_hrid_from_filename(marc_file_path) + File.basename(marc_file_path, '.mrc') + end + + def create_new_hyacinth_record(marc_file_path, folio_hrid) + @logger.info("Creating new Hyacinth record for #{folio_hrid}") + + new_record = FolioToHyacinthRecord.new(marc_file_path) + puts "Digital object data: #{new_record.digital_object_data}" + + response = FolioSync::Hyacinth::Client.instance.create_new_record( + new_record.digital_object_data, + publish: true + ) + + @logger.info("Created record for #{folio_hrid}: #{response.inspect}") + response + end +end diff --git a/lib/tasks/hyacinth_sync.rake b/lib/tasks/hyacinth_sync.rake index 7d91196..66b5574 100644 --- a/lib/tasks/hyacinth_sync.rake +++ b/lib/tasks/hyacinth_sync.rake @@ -4,30 +4,49 @@ namespace :folio_sync do namespace :folio_to_hyacinth do task run: :environment do puts 'Starting Folio to Hyacinth sync task...' + + modified_since = ENV['modified_since'] + + modified_since_sanitized = + if modified_since && !modified_since.strip.empty? + begin + Integer(modified_since) + rescue ArgumentError + puts 'Error: modified_since must be an integer (number of hours).' + exit 1 + end + end + + clear_downloads = ENV['clear_downloads'].nil? || ENV['clear_downloads'] == 'true' + synchronizer = FolioSync::FolioToHyacinth::HyacinthSynchronizer.new - synchronizer.download_and_sync_folio_to_hyacinth_records(24) + synchronizer.clear_downloads! if clear_downloads + synchronizer.download_and_sync_folio_to_hyacinth_records(modified_since_sanitized) + + # Handle errors end - # task run: :environment do - # modified_since = ENV['modified_since'] - # modified_since_num = - # if modified_since && !modified_since.strip.empty? - # begin - # Integer(modified_since) - # rescue ArgumentError - # puts 'Error: modified_since must be an integer (number of hours).' - # exit 1 - # end - # end - - # downloader = FolioSync::FolioToHyacinth::MarcDownloader.new - # downloader.download_965hyacinth_marc_records(modified_since_num) - - # if downloader.downloading_errors.present? - # puts "Errors encountered during MARC download: #{downloader.downloading_errors}" - # exit 1 - # end - # end + # Download part only + task download_folio_marc_files: :environment do + modified_since = ENV['modified_since'] + modified_since_num = + if modified_since && !modified_since.strip.empty? + begin + Integer(modified_since) + rescue ArgumentError + puts 'Error: modified_since must be an integer (number of hours).' + exit 1 + end + end + + downloader = FolioSync::FolioToHyacinth::MarcDownloader.new + downloader.download_965hyacinth_marc_records(modified_since_num) + + if downloader.downloading_errors.present? + puts "Errors encountered during MARC download: #{downloader.downloading_errors}" + exit 1 + end + end task download_single_file: :environment do FolioSync::Rake::EnvValidator.validate!( From b845338220981dae8fe83844c404655efabecc48 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Mon, 8 Dec 2025 16:58:39 -0500 Subject: [PATCH 3/8] FOLIOSYNC-12 create RecordSyncer class to handle creating/updating Hyacinth records; add tests for HyacinthSynchronizer class --- .../hyacinth_synchronizer.rb | 14 +- .../folio_to_hyacinth/marc_processor.rb | 68 ++---- .../folio_to_hyacinth/record_syncer.rb | 68 ++++++ lib/tasks/hyacinth_sync.rake | 67 ++---- .../hyacinth_synchronizer_spec.rb | 219 ++++++++++++++++++ 5 files changed, 336 insertions(+), 100 deletions(-) create mode 100644 lib/folio_sync/folio_to_hyacinth/record_syncer.rb create mode 100644 spec/folio_sync/folio_to_hyacinth/hyacinth_synchronizer_spec.rb diff --git a/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb b/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb index 49d560d..02da020 100644 --- a/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb +++ b/lib/folio_sync/folio_to_hyacinth/hyacinth_synchronizer.rb @@ -7,13 +7,15 @@ class HyacinthSynchronizer def initialize @logger = Logger.new($stdout) + @downloading_errors = [] + @syncing_errors = [] end # Performs MARC downloads and syncs resources to Hyacinth # @param [Integer] last_x_hours Records newer than this are synced. def download_and_sync_folio_to_hyacinth_records(last_x_hours) - # download_marc_from_folio(last_x_hours) - prepare_hyacinth_records + download_marc_from_folio(last_x_hours) + prepare_and_sync_folio_to_hyacinth_records end def clear_downloads! @@ -31,9 +33,9 @@ def download_marc_from_folio(last_x_hours) @downloading_errors = downloader.downloading_errors end - def prepare_hyacinth_records + def prepare_and_sync_folio_to_hyacinth_records marc_files = Dir.glob(downloaded_marc_files_path) - puts "Processing #{marc_files.count} MARC files" + @logger.info("Processing #{marc_files.count} MARC files") marc_files.each do |marc_file_path| process_marc_file(marc_file_path) @@ -48,7 +50,9 @@ def downloaded_marc_files_path def process_marc_file(marc_file_path) processor = FolioSync::FolioToHyacinth::MarcProcessor.new(marc_file_path) - processor.create_and_sync_hyacinth_record! + processor.prepare_and_sync_folio_to_hyacinth_record! + + @syncing_errors.concat(processor.syncing_errors) if processor.syncing_errors.any? end end end diff --git a/lib/folio_sync/folio_to_hyacinth/marc_processor.rb b/lib/folio_sync/folio_to_hyacinth/marc_processor.rb index 3c4f3db..b9611ca 100644 --- a/lib/folio_sync/folio_to_hyacinth/marc_processor.rb +++ b/lib/folio_sync/folio_to_hyacinth/marc_processor.rb @@ -1,76 +1,40 @@ # frozen_string_literal: true class FolioSync::FolioToHyacinth::MarcProcessor + attr_reader :syncing_errors + def initialize(marc_file_path) @marc_file_path = marc_file_path @logger = Logger.new($stdout) + @record_syncer = FolioSync::FolioToHyacinth::RecordSyncer.new @syncing_errors = [] end - def create_and_sync_hyacinth_record! + def prepare_and_sync_folio_to_hyacinth_record! folio_hrid = extract_hrid_from_filename(@marc_file_path) - hyacinth_results = fetch_hyacinth_results(@marc_file_path) - @logger.info("Found #{hyacinth_results.length} Hyacinth records for FOLIO HRID #{folio_hrid}") + existing_records = fetch_existing_hyacinth_records(folio_hrid) + + @logger.info("Found #{existing_records.length} Hyacinth records for FOLIO HRID: #{folio_hrid}") - case hyacinth_results.length - when 0 - create_new_hyacinth_record(@marc_file_path, folio_hrid) - when 1 - update_existing_hyacinth_record(@marc_file_path, hyacinth_results.first, folio_hrid) - else - handle_multiple_records_error(folio_hrid) - end + @record_syncer.sync(@marc_file_path, folio_hrid, existing_records) + @syncing_errors.concat(@record_syncer.syncing_errors) if @record_syncer.syncing_errors.any? rescue StandardError => e @logger.error("Failed to process #{folio_hrid}: #{e.message}") @syncing_errors << "Error processing #{folio_hrid}: #{e.message}" end - def update_existing_hyacinth_record(marc_file_path, existing_record, folio_hrid) - @logger.info("Updating existing Hyacinth record for #{folio_hrid}") - - preserved_data = { 'identifiers' => existing_record['identifiers'] } - updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) - - response = FolioSync::Hyacinth::Client.instance.update_existing_record( - existing_record['pid'], - updated_record.digital_object_data, - publish: true - ) - - @logger.info("Updated record #{existing_record['pid']}: #{response.inspect}") - response - end - - def handle_multiple_records_error(folio_hrid) - error_message = "Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}" - @logger.error(error_message) - @syncing_errors << error_message - end - - def fetch_hyacinth_results(marc_file_path) - folio_hrid = File.basename(marc_file_path, '.mrc') - potential_clio_identifier = "clio#{folio_hrid}" - client = FolioSync::Hyacinth::Client.instance - client.find_by_identifier(potential_clio_identifier, - { f: { digital_object_type_display_label_sim: ['Item'] } }) - end + private def extract_hrid_from_filename(marc_file_path) File.basename(marc_file_path, '.mrc') end - def create_new_hyacinth_record(marc_file_path, folio_hrid) - @logger.info("Creating new Hyacinth record for #{folio_hrid}") - - new_record = FolioToHyacinthRecord.new(marc_file_path) - puts "Digital object data: #{new_record.digital_object_data}" - - response = FolioSync::Hyacinth::Client.instance.create_new_record( - new_record.digital_object_data, - publish: true + def fetch_existing_hyacinth_records(folio_hrid) + potential_clio_identifier = "clio#{folio_hrid}" + client = FolioSync::Hyacinth::Client.instance + client.find_by_identifier( + potential_clio_identifier, + { f: { digital_object_type_display_label_sim: ['Item'] } } ) - - @logger.info("Created record for #{folio_hrid}: #{response.inspect}") - response end end diff --git a/lib/folio_sync/folio_to_hyacinth/record_syncer.rb b/lib/folio_sync/folio_to_hyacinth/record_syncer.rb new file mode 100644 index 0000000..c40be4c --- /dev/null +++ b/lib/folio_sync/folio_to_hyacinth/record_syncer.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module FolioSync + module FolioToHyacinth + class RecordSyncer + attr_reader :syncing_errors + + def initialize + @logger = Logger.new($stdout) + @client = FolioSync::Hyacinth::Client.instance + @syncing_errors = [] + end + + # @param [String] marc_file_path + # @param [String] folio_hrid + # @param [Array] existing_records + def sync(marc_file_path, folio_hrid, existing_records) + case existing_records.length + when 0 + create_new_record(marc_file_path, folio_hrid) + when 1 + update_existing_record(marc_file_path, folio_hrid, existing_records.first) + else + handle_multiple_records(folio_hrid) + end + end + + private + + def create_new_record(marc_file_path, folio_hrid) + @logger.info("Creating new Hyacinth record for #{folio_hrid}") + + new_record = FolioToHyacinthRecord.new(marc_file_path) + response = @client.create_new_record(new_record.digital_object_data, publish: true) + + @logger.info("Created record for #{folio_hrid}: #{response.inspect}") + rescue StandardError => e + error_message = "Failed to create record for #{folio_hrid}: #{e.message}" + @logger.error(error_message) + @syncing_errors << error_message + end + + def update_existing_record(marc_file_path, folio_hrid, existing_record) + @logger.info("Updating existing Hyacinth record for #{folio_hrid}") + preserved_data = { 'identifiers' => existing_record['identifiers'] } + updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) + + response = @client.update_existing_record( + existing_record['pid'], + updated_record.digital_object_data, + publish: true + ) + + @logger.info("Updated record #{existing_record['pid']}: #{response.inspect}") + rescue StandardError => e + error_message = "Failed to update record #{existing_record['pid']} for #{folio_hrid}: #{e.message}" + @logger.error(error_message) + @syncing_errors << error_message + end + + def handle_multiple_records(folio_hrid) + error_message = "Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}" + @logger.error(error_message) + @syncing_errors << error_message + end + end + end +end diff --git a/lib/tasks/hyacinth_sync.rake b/lib/tasks/hyacinth_sync.rake index 66b5574..91d0347 100644 --- a/lib/tasks/hyacinth_sync.rake +++ b/lib/tasks/hyacinth_sync.rake @@ -18,18 +18,27 @@ namespace :folio_sync do end clear_downloads = ENV['clear_downloads'].nil? || ENV['clear_downloads'] == 'true' + puts "Will downloads be cleared? #{clear_downloads}" synchronizer = FolioSync::FolioToHyacinth::HyacinthSynchronizer.new synchronizer.clear_downloads! if clear_downloads synchronizer.download_and_sync_folio_to_hyacinth_records(modified_since_sanitized) - # Handle errors + if synchronizer.downloading_errors.any? || synchronizer.syncing_errors.any? + puts 'Errors encountered during Folio to Hyacinth sync:' + puts "Downloading Errors: #{synchronizer.downloading_errors}" if synchronizer.downloading_errors.any? + puts "Syncing Errors: #{synchronizer.syncing_errors}" if synchronizer.syncing_errors.any? + + exit 1 + else + puts 'Folio to Hyacinth sync completed successfully.' + end end - # Download part only + # Downloads FOLIO MARC records, skipping the syncing step task download_folio_marc_files: :environment do modified_since = ENV['modified_since'] - modified_since_num = + modified_since_sanitized = if modified_since && !modified_since.strip.empty? begin Integer(modified_since) @@ -40,7 +49,7 @@ namespace :folio_sync do end downloader = FolioSync::FolioToHyacinth::MarcDownloader.new - downloader.download_965hyacinth_marc_records(modified_since_num) + downloader.download_965hyacinth_marc_records(modified_since_sanitized) if downloader.downloading_errors.present? puts "Errors encountered during MARC download: #{downloader.downloading_errors}" @@ -59,47 +68,19 @@ namespace :folio_sync do downloader.download_single_965hyacinth_marc_record(folio_hrid) end - # WIP: This task syncs all downloaded FOLIO MARC records to Hyacinth + # Syncs all previously downloaded FOLIO MARC records to Hyacinth task sync_to_hyacinth: :environment do puts 'Starting Folio to Hyacinth sync task...' - file_dir = Rails.root.join('tmp/working_data/development/folio_to_hyacinth/downloaded_files') - - # For each MARC file in the download directory, create or update the corresponding Hyacinth record - Dir.glob(File.join(file_dir, '*.mrc')).each do |marc_file_path| - puts "Processing MARC file: #{marc_file_path}" - - # Check if the record already exists in Hyacinth - folio_hrid = File.basename(marc_file_path, '.mrc') - potential_clio_identifier = "clio#{folio_hrid}" - client = FolioSync::Hyacinth::Client.instance - results = client.find_by_identifier(potential_clio_identifier, - { f: { digital_object_type_display_label_sim: ['Item'] } }) - puts "Found #{results.length} records with identifier #{potential_clio_identifier}." - - # TODO: Eventually this logic will be placed under FolioToHyacinth namespace - if results.empty? - puts 'No records found. Creating a new record in Hyacinth.' - new_record = FolioToHyacinthRecord.new(marc_file_path) - puts "New record digital object data: #{new_record.digital_object_data}" - response = client.create_new_record(new_record.digital_object_data, publish: true) - puts "Response from Hyacinth when creating record with hrid #{folio_hrid}: #{response.inspect}" - elsif results.length == 1 - pid = results.first['pid'] - puts "Found 1 record with pid: #{pid}." - - puts results.first.inspect - - # Get only the data needed for update - preserved_data = { 'identifiers' => results.first['identifiers'] } - updated_record = FolioToHyacinthRecord.new(marc_file_path, preserved_data) - puts "Updated record digital object data: #{updated_record.digital_object_data}" - - # return - response = client.update_existing_record(pid, updated_record.digital_object_data, publish: true) - puts "Response from Hyacinth when updating record #{pid}: #{response.inspect}" - else - puts "Error: Found multiple records with identifier #{potential_clio_identifier}." - end + + synchronizer = FolioSync::FolioToHyacinth::HyacinthSynchronizer.new + synchronizer.prepare_and_sync_folio_to_hyacinth_records + + if synchronizer.syncing_errors.any? + puts 'Errors encountered during Folio to Hyacinth sync:' + puts "Syncing Errors: #{synchronizer.syncing_errors}" if synchronizer.syncing_errors.any? + exit 1 + else + puts 'Folio to Hyacinth sync completed successfully.' end end diff --git a/spec/folio_sync/folio_to_hyacinth/hyacinth_synchronizer_spec.rb b/spec/folio_sync/folio_to_hyacinth/hyacinth_synchronizer_spec.rb new file mode 100644 index 0000000..3f0a5a3 --- /dev/null +++ b/spec/folio_sync/folio_to_hyacinth/hyacinth_synchronizer_spec.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FolioSync::FolioToHyacinth::HyacinthSynchronizer do + let(:instance) { described_class.new } + let(:logger) { instance_double(Logger, info: nil, error: nil, debug: nil) } + let(:last_x_hours) { 24 } + let(:folio_to_hyacinth_config) do + { + download_directory: '/tmp/folio_to_hyacinth/downloaded_files' + } + end + + before do + allow(Logger).to receive(:new).and_return(logger) + allow(Rails).to receive_message_chain(:configuration, :folio_to_hyacinth).and_return(folio_to_hyacinth_config) + end + + describe '#initialize' do + it 'can be instantiated' do + expect(instance).to be_a(described_class) + end + + it 'initializes with a logger' do + expect(instance.instance_variable_get(:@logger)).to eq(logger) + end + + it 'initializes error arrays as empty' do + expect(instance.downloading_errors).to eq([]) + expect(instance.syncing_errors).to eq([]) + end + end + + describe '#download_and_sync_folio_to_hyacinth_records' do + before do + allow(instance).to receive(:download_marc_from_folio) + allow(instance).to receive(:prepare_and_sync_folio_to_hyacinth_records) + end + + it 'downloads MARC from FOLIO' do + instance.download_and_sync_folio_to_hyacinth_records(last_x_hours) + expect(instance).to have_received(:download_marc_from_folio).with(last_x_hours) + end + + it 'prepares and syncs records to Hyacinth' do + instance.download_and_sync_folio_to_hyacinth_records(last_x_hours) + expect(instance).to have_received(:prepare_and_sync_folio_to_hyacinth_records) + end + + it 'handles nil last_x_hours to download all records' do + instance.download_and_sync_folio_to_hyacinth_records(nil) + expect(instance).to have_received(:download_marc_from_folio).with(nil) + end + end + + describe '#download_marc_from_folio' do + let(:downloader) { instance_double(FolioSync::FolioToHyacinth::MarcDownloader, downloading_errors: []) } + + before do + allow(FolioSync::FolioToHyacinth::MarcDownloader).to receive(:new).and_return(downloader) + allow(downloader).to receive(:download_965hyacinth_marc_records) + end + + it 'creates a new MarcDownloader instance' do + expect(FolioSync::FolioToHyacinth::MarcDownloader).to receive(:new) + instance.download_marc_from_folio(last_x_hours) + end + + it 'calls download_965hyacinth_marc_records with correct parameter' do + expect(downloader).to receive(:download_965hyacinth_marc_records).with(last_x_hours) + instance.download_marc_from_folio(last_x_hours) + end + + context 'when there are no downloading errors' do + it 'does not set downloading_errors' do + instance.download_marc_from_folio(last_x_hours) + expect(instance.downloading_errors).to eq([]) + end + + it 'does not log errors' do + instance.download_marc_from_folio(last_x_hours) + expect(logger).not_to have_received(:error) + end + end + + context 'when there are downloading errors' do + let(:errors) { ['Error downloading record 1', 'Error downloading record 2'] } + + before do + allow(downloader).to receive(:downloading_errors).and_return(errors) + end + + it 'sets downloading_errors' do + instance.download_marc_from_folio(last_x_hours) + expect(instance.downloading_errors).to eq(errors) + end + + it 'logs the errors' do + expect(logger).to receive(:error).with(/Error downloading MARC records from FOLIO/) + instance.download_marc_from_folio(last_x_hours) + end + end + end + + describe '#prepare_and_sync_folio_to_hyacinth_records' do + let(:marc_files) { ['/tmp/downloads/record1.mrc', '/tmp/downloads/record2.mrc'] } + + before do + allow(Dir).to receive(:glob).and_return(marc_files) + allow(instance).to receive(:process_marc_file) + end + + it 'finds MARC files in the download directory' do + expect(Dir).to receive(:glob).with("#{folio_to_hyacinth_config[:download_directory]}/*.mrc") + instance.prepare_and_sync_folio_to_hyacinth_records + end + + it 'logs the number of files being processed' do + expect(logger).to receive(:info).with("Processing #{marc_files.count} MARC files") + instance.prepare_and_sync_folio_to_hyacinth_records + end + + it 'processes each MARC file' do + instance.prepare_and_sync_folio_to_hyacinth_records + marc_files.each do |marc_file| + expect(instance).to have_received(:process_marc_file).with(marc_file) + end + end + + context 'when there are no MARC files' do + let(:marc_files) { [] } + + it 'does not process any files' do + instance.prepare_and_sync_folio_to_hyacinth_records + expect(instance).not_to have_received(:process_marc_file) + end + end + end + + describe '#process_marc_file' do + let(:marc_file_path) { '/tmp/downloads/record1.mrc' } + let(:processor) { instance_double(FolioSync::FolioToHyacinth::MarcProcessor, syncing_errors: []) } + + before do + allow(FolioSync::FolioToHyacinth::MarcProcessor).to receive(:new).and_return(processor) + allow(processor).to receive(:prepare_and_sync_folio_to_hyacinth_record!) + end + + it 'creates a new MarcProcessor with the file path' do + expect(FolioSync::FolioToHyacinth::MarcProcessor).to receive(:new).with(marc_file_path) + instance.send(:process_marc_file, marc_file_path) + end + + it 'calls prepare_and_sync_folio_to_hyacinth_record!' do + expect(processor).to receive(:prepare_and_sync_folio_to_hyacinth_record!) + instance.send(:process_marc_file, marc_file_path) + end + + context 'when there are no syncing errors' do + it 'does not add to syncing_errors' do + instance.send(:process_marc_file, marc_file_path) + expect(instance.syncing_errors).to eq([]) + end + end + + context 'when there are syncing errors' do + let(:errors) { ['Error syncing record', 'Another error'] } + + before do + allow(processor).to receive(:syncing_errors).and_return(errors) + end + + it 'concatenates errors to syncing_errors' do + instance.send(:process_marc_file, marc_file_path) + expect(instance.syncing_errors).to eq(errors) + end + end + + context 'when processing multiple files with errors' do + let(:processor2) { instance_double(FolioSync::FolioToHyacinth::MarcProcessor, syncing_errors: ['Error from file 2']) } + let(:errors1) { ['Error from file 1'] } + + before do + allow(processor).to receive(:syncing_errors).and_return(errors1) + allow(FolioSync::FolioToHyacinth::MarcProcessor).to receive(:new) + .with('/tmp/downloads/record2.mrc') + .and_return(processor2) + allow(processor2).to receive(:prepare_and_sync_folio_to_hyacinth_record!) + end + + it 'accumulates errors from multiple processors' do + instance.send(:process_marc_file, marc_file_path) + instance.send(:process_marc_file, '/tmp/downloads/record2.mrc') + expect(instance.syncing_errors).to eq(['Error from file 1', 'Error from file 2']) + end + end + end + + describe '#clear_downloads!' do + let(:download_path) { "#{folio_to_hyacinth_config[:download_directory]}/*.mrc" } + + before do + allow(FileUtils).to receive(:rm_rf) + end + + it 'removes files matching the download path pattern' do + expect(FileUtils).to receive(:rm_rf).with(download_path) + instance.clear_downloads! + end + end + + describe '#downloaded_marc_files_path' do + it 'returns the correct glob pattern for MARC files' do + expected_path = "#{folio_to_hyacinth_config[:download_directory]}/*.mrc" + expect(instance.send(:downloaded_marc_files_path)).to eq(expected_path) + end + end +end \ No newline at end of file From 472ada4b573804d17205fabe6e5d5cc5b332d865 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Mon, 8 Dec 2025 17:01:30 -0500 Subject: [PATCH 4/8] FOLIOSYNC-12 update robocop todo --- .rubocop_todo.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 68b26d7..e482512 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-10-30 19:13:32 UTC using RuboCop version 1.78.0. +# on 2025-12-08 22:00:37 UTC using RuboCop version 1.78.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 20 +# Offense count: 19 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 34 @@ -21,7 +21,7 @@ Metrics/ClassLength: Metrics/CyclomaticComplexity: Max: 8 -# Offense count: 22 +# Offense count: 23 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: @@ -37,6 +37,7 @@ Metrics/MethodLength: - 'lib/folio_sync/archives_space_to_folio/record_processor.rb' - 'lib/folio_sync/folio_to_hyacinth/marc_downloader.rb' - 'lib/folio_sync/folio_to_hyacinth/marc_parsing_methods/title.rb' + - 'lib/folio_sync/folio_to_hyacinth/record_syncer.rb' - 'lib/folio_sync/rake/error_logger.rb' - 'lib/hyacinth_api/digital_objects.rb' @@ -66,14 +67,13 @@ Rails/FindEach: Exclude: - 'lib/tasks/test_create_or_update.rake' -# Offense count: 5 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Include. # Include: **/app/**/*.rb, **/config/**/*.rb, db/**/*.rb, **/lib/**/*.rb Rails/Output: Exclude: - 'app/models/folio_to_hyacinth_record.rb' - - 'lib/folio_sync/folio_to_hyacinth/marc_parsing_methods/project.rb' - 'lib/folio_sync/rake/env_validator.rb' # Offense count: 1 From 2acda88c8ec9bb6dd87e0c478b0021b85fc349dc Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Tue, 9 Dec 2025 10:35:46 -0500 Subject: [PATCH 5/8] FOLIOSYNC-12 add tests for the MarcDownloader class (part of Hyacinth sync) --- .../folio_to_hyacinth/marc_downloader_spec.rb | 234 +++++++++++++----- 1 file changed, 169 insertions(+), 65 deletions(-) diff --git a/spec/folio_sync/folio_to_hyacinth/marc_downloader_spec.rb b/spec/folio_sync/folio_to_hyacinth/marc_downloader_spec.rb index 96d5af0..35aaa6d 100644 --- a/spec/folio_sync/folio_to_hyacinth/marc_downloader_spec.rb +++ b/spec/folio_sync/folio_to_hyacinth/marc_downloader_spec.rb @@ -6,18 +6,17 @@ let(:instance) { described_class.new } let(:folio_client) { instance_double(FolioSync::Folio::Client) } let(:folio_reader) { instance_double(FolioSync::Folio::Reader) } - let(:config) { { download_directory: '/tmp/downloads' } } - let(:value_965) { '965hyacinth' } - + let(:value_965_hyacinth) { '965hyacinth' } + let(:folio_to_hyacinth_config) { { download_directory: '/tmp/folio_to_hyacinth/downloads' } } + let(:marc_record_with_965hyacinth) do { 'fields' => [ { '001' => '123456' }, - { '965' => { 'subfields' => [{ 'a' => '965hyacinth' }] } } + { '965' => { 'subfields' => [{ 'a' => value_965_hyacinth }] } } ] } end - let(:marc_record_without_965hyacinth) do { 'fields' => [ @@ -38,10 +37,15 @@ before do allow(FolioSync::Folio::Client).to receive(:instance).and_return(folio_client) allow(FolioSync::Folio::Reader).to receive(:new).and_return(folio_reader) - allow(Rails.configuration).to receive(:folio_to_hyacinth).and_return(config) + allow(Rails.configuration).to receive(:folio_to_hyacinth).and_return(folio_to_hyacinth_config) + allow(Rails).to receive(:logger).and_return(Logger.new(nil)) end describe '#initialize' do + it 'can be instantiated' do + expect(instance).to be_a(described_class) + end + it 'initializes with the correct dependencies' do expect(instance.instance_variable_get(:@folio_client)).to eq(folio_client) expect(instance.instance_variable_get(:@folio_reader)).to eq(folio_reader) @@ -50,155 +54,255 @@ end describe '#download_965hyacinth_marc_records' do - let(:parsed_records) { [marc_record_with_965hyacinth, marc_record_without_965hyacinth] } + let(:last_x_hours) { 24 } + let(:modified_since) { Time.now.utc - (3600 * last_x_hours) } before do - allow(folio_client).to receive(:find_source_marc_records) do |_modified_since, _options, &block| - parsed_records.each { |record| block.call(record) } - end + allow(Time).to receive(:now).and_return(Time.parse('2025-01-01 12:00:00 UTC')) + allow(folio_client).to receive(:find_source_marc_records).and_yield(marc_record_with_965hyacinth) allow(instance).to receive(:save_marc_record_to_file) - allow(Rails.logger).to receive(:info) end + it 'calculates modified_since correctly' do + expect(folio_client).to receive(:find_source_marc_records).with( + modified_since: '2024-12-31T12:00:00Z', + with_965_value: value_965_hyacinth + ) + instance.download_965hyacinth_marc_records(last_x_hours) + end + + context 'when last_x_hours is nil' do - it 'downloads all records without time filter' do + it 'downloads all records without modified_since filter' do + expect(folio_client).to receive(:find_source_marc_records).with( + modified_since: nil, + with_965_value: value_965_hyacinth + ) instance.download_965hyacinth_marc_records(nil) + end - expect(Rails.logger).to have_received(:info).with( - 'Downloading MARC with 965hyacinth (all records)' - ) - expect(folio_client).to have_received(:find_source_marc_records).with(modified_since: nil, with_965_value: value_965) + it 'logs that all records are being downloaded' do + expect(Rails.logger).to receive(:info).with(/Downloading MARC with 965hyacinth \(all records\)/) + instance.download_965hyacinth_marc_records(nil) + expect(folio_client).to have_received(:find_source_marc_records).with(modified_since: nil, with_965_value: value_965_hyacinth) expect(instance).to have_received(:save_marc_record_to_file).with(marc_record_with_965hyacinth).once expect(instance).not_to have_received(:save_marc_record_to_file).with(marc_record_without_965hyacinth) end end - context 'when last_x_hours is specified' do - let(:last_x_hours) { 24 } - let(:expected_time) { Time.parse('2024-06-25T10:00:00Z') } + context 'when record has 965hyacinth field' do + it 'saves the MARC record to file' do + expect(instance).to receive(:save_marc_record_to_file).with(marc_record_with_965hyacinth) + instance.download_965hyacinth_marc_records(last_x_hours) + end + end + + context 'when record does not have 965hyacinth field' do + before do + allow(folio_client).to receive(:find_source_marc_records).and_yield(marc_record_without_965hyacinth) + end + + it 'does not save the record' do + expect(instance).not_to receive(:save_marc_record_to_file) + instance.download_965hyacinth_marc_records(last_x_hours) + end + end + context 'when saving fails' do before do - allow(Time).to receive(:now).and_return(Time.parse('2024-06-26T10:00:00Z')) + allow(instance).to receive(:save_marc_record_to_file).and_raise(StandardError.new('File write error')) end - it 'downloads records modified since the specified time' do + it 'captures the error' do instance.download_965hyacinth_marc_records(last_x_hours) + expect(instance.downloading_errors).to include(/Failed to save MARC record 123456: File write error/) + end - expect(Rails.logger).to have_received(:info).with( - "Downloading MARC with 965hyacinth modified since: #{expected_time.utc.iso8601}" - ) - expect(folio_client).to have_received(:find_source_marc_records).with(modified_since: expected_time.utc.iso8601, with_965_value: value_965) - expect(instance).to have_received(:save_marc_record_to_file).with(marc_record_with_965hyacinth).once + it 'continues processing other records' do + allow(folio_client).to receive(:find_source_marc_records).and_yield(marc_record_with_965hyacinth).and_yield(marc_record_with_965hyacinth) + instance.download_965hyacinth_marc_records(last_x_hours) + expect(instance.downloading_errors.length).to eq(2) end end end describe '#has_965hyacinth_field?' do - context 'when record has 965 field with 965hyacinth value' do + context 'when record has 965$a with value 965hyacinth' do it 'returns true' do expect(instance.has_965hyacinth_field?(marc_record_with_965hyacinth)).to be true end end - context 'when record has 965 field but not with 965hyacinth value' do + context 'when record has no 965 field' do it 'returns false' do expect(instance.has_965hyacinth_field?(marc_record_without_965hyacinth)).to be false end end - context 'when record has no 965 field' do - let(:marc_record_no_965) do + context 'when record has 965 field but not in $a subfield' do + let(:marc_record_wrong_subfield) do { 'fields' => [ - { '001' => '345678' }, + { + '965' => { + 'subfields' => [ + { 'b' => value_965_hyacinth } + ] + } + } ] } end it 'returns false' do - expect(instance.has_965hyacinth_field?(marc_record_no_965)).to be false + expect(instance.has_965hyacinth_field?(marc_record_wrong_subfield)).to be false end end - context 'when 965 field has no subfields' do - let(:marc_record_965_no_subfields) do + context 'when record has 965$a with different value' do + let(:marc_record_wrong_value) do { 'fields' => [ - { '001' => '456789' }, - { '965' => {} } + { + '965' => { + 'subfields' => [ + { 'a' => 'different_value' } + ] + } + } ] } end it 'returns false' do - expect(instance.has_965hyacinth_field?(marc_record_965_no_subfields)).to be false + expect(instance.has_965hyacinth_field?(marc_record_wrong_value)).to be false + end + end + + context 'when record has multiple 965 fields' do + let(:marc_record_multiple_965) do + { + 'fields' => [ + { + '965' => { + 'subfields' => [ + { 'a' => 'other_value' } + ] + } + }, + { + '965' => { + 'subfields' => [ + { 'a' => value_965_hyacinth } + ] + } + } + ] + } + end + + it 'returns true if any 965$a has the correct value' do + expect(instance.has_965hyacinth_field?(marc_record_multiple_965)).to be true end end end describe '#save_marc_record_to_file' do - let(:formatted_marc) { double('MARC::Record') } - let(:expected_file_path) { '/tmp/downloads/123456.mrc' } + let(:marc_record) { instance_double(MARC::Record) } + let(:filename) { '123456' } + let(:marc_binary) { 'binary_marc_data' } before do - allow(MARC::Record).to receive(:new_from_hash).with(marc_record_with_965hyacinth).and_return(formatted_marc) - allow(formatted_marc).to receive(:to_marc).and_return('binary_marc_data') + allow(MARC::Record).to receive(:new_from_hash).with(marc_record_with_965hyacinth).and_return(marc_record) + allow(marc_record).to receive(:to_marc).and_return(marc_binary) allow(File).to receive(:binwrite) - allow(Rails.logger).to receive(:info) + allow(File).to receive(:join).and_return(folio_to_hyacinth_config[:download_directory]) end - it 'saves the MARC record to the correct file path' do + it 'extracts the filename from 001 field' do + expect(instance).to receive(:extract_id).with(marc_record_with_965hyacinth).and_return(filename) instance.save_marc_record_to_file(marc_record_with_965hyacinth) + end - expect(Rails.logger).to have_received(:info).with( - 'Saving MARC record with 001=123456 to /tmp/downloads/123456.mrc' - ) - expect(MARC::Record).to have_received(:new_from_hash).with(marc_record_with_965hyacinth) - expect(File).to have_received(:binwrite).with(expected_file_path, 'binary_marc_data') + it 'creates MARC record from hash' do + expect(MARC::Record).to receive(:new_from_hash).with(marc_record_with_965hyacinth) + instance.save_marc_record_to_file(marc_record_with_965hyacinth) + end + + it 'writes the MARC binary to file' do + expect(File).to receive(:binwrite).with(folio_to_hyacinth_config[:download_directory], marc_binary) + instance.save_marc_record_to_file(marc_record_with_965hyacinth) + end + + context 'when 001 field is missing' do + let(:marc_record_no_001) do + { + 'fields' => [ + { + '965' => { + 'subfields' => [ + { 'a' => value_965_hyacinth } + ] + } + } + ] + } + end + + it 'raises Missing001Field exception' do + expect { + instance.save_marc_record_to_file(marc_record_no_001) + }.to raise_error(FolioSync::Exceptions::Missing001Field, 'MARC record is missing required 001 field') + end end end describe '#download_single_965hyacinth_marc_record' do let(:folio_hrid) { 'test_hrid' } + before do + allow(folio_client).to receive(:find_source_record) + .with(instance_record_hrid: folio_hrid) + .and_return(source_record_with_965hyacinth) + allow(instance).to receive(:save_marc_record_to_file) + end - context 'when record exists and has 965hyacinth field' do - before do - allow(folio_client).to receive(:find_source_record) - .with(instance_record_hrid: folio_hrid) - .and_return(source_record_with_965hyacinth) - allow(instance).to receive(:save_marc_record_to_file) - end + it 'fetches the source record by HRID' do + expect(folio_client).to receive(:find_source_record).with(instance_record_hrid: folio_hrid) + instance.download_single_965hyacinth_marc_record(folio_hrid) + end - it 'downloads and saves the record' do - instance.download_single_965hyacinth_marc_record(folio_hrid) + it 'checks for 965hyacinth field' do + expect(instance).to receive(:has_965hyacinth_field?).with(marc_record_with_965hyacinth).and_call_original + instance.download_single_965hyacinth_marc_record(folio_hrid) + end - expect(folio_client).to have_received(:find_source_record).with(instance_record_hrid: folio_hrid) - expect(instance).to have_received(:save_marc_record_to_file).with(marc_record_with_965hyacinth) - end + it 'saves the MARC record to file' do + expect(instance).to receive(:save_marc_record_to_file).with(marc_record_with_965hyacinth) + instance.download_single_965hyacinth_marc_record(folio_hrid) end - context 'when record exists but does not have 965hyacinth field' do + context 'when record does not have 965hyacinth field' do before do allow(folio_client).to receive(:find_source_record) .with(instance_record_hrid: folio_hrid) .and_return(source_record_without_965hyacinth) end - it 'raises an exception' do + it 'raises an error' do expect { instance.download_single_965hyacinth_marc_record(folio_hrid) - }.to raise_error("Source record with HRID #{folio_hrid} doesn't have a 965 field with subfield $a value of '965hyacinth'.") + }.to raise_error(/doesn't have a 965 field with subfield \$a value of '965hyacinth'/) end end - context 'when record does not exist' do + context 'when source record does not exist' do before do allow(folio_client).to receive(:find_source_record) .with(instance_record_hrid: folio_hrid) .and_return(nil) end - it 'raises an exception' do + it 'raises an error' do expect { instance.download_single_965hyacinth_marc_record(folio_hrid) }.to raise_error(NoMethodError) From 3eea21cdea35682a6805a90a8b94e87c6309defb Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Tue, 9 Dec 2025 16:03:17 -0500 Subject: [PATCH 6/8] FOLIOSYNC-12 add MarcProcessor tests --- .../folio_to_hyacinth/marc_processor_spec.rb | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb diff --git a/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb b/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb new file mode 100644 index 0000000..16082da --- /dev/null +++ b/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FolioSync::FolioToHyacinth::MarcProcessor do + let(:marc_file_path) { '/tmp/folio_to_hyacinth/downloaded_files/45678.mrc' } + let(:instance) { described_class.new(marc_file_path) } + let(:logger) { instance_double(Logger, info: nil, error: nil, debug: nil) } + let(:record_syncer) { instance_double(FolioSync::FolioToHyacinth::RecordSyncer, syncing_errors: []) } + let(:hyacinth_client) { instance_double(FolioSync::Hyacinth::Client) } + let(:folio_hrid) { '45678' } + + before do + allow(Logger).to receive(:new).and_return(logger) + allow(FolioSync::FolioToHyacinth::RecordSyncer).to receive(:new).and_return(record_syncer) + allow(FolioSync::Hyacinth::Client).to receive(:instance).and_return(hyacinth_client) + end + + describe '#initialize' do + it 'sets the marc_file_path' do + expect(instance.instance_variable_get(:@marc_file_path)).to eq(marc_file_path) + end + + it 'initializes a logger' do + expect(instance.instance_variable_get(:@logger)).to eq(logger) + end + + it 'initializes a record syncer' do + expect(instance.instance_variable_get(:@record_syncer)).to eq(record_syncer) + end + + it 'initializes syncing_errors as empty array' do + expect(instance.syncing_errors).to eq([]) + end + end + + describe '#prepare_and_sync_folio_to_hyacinth_record!' do + let(:existing_records) { [] } + + before do + allow(hyacinth_client).to receive(:find_by_identifier).and_return(existing_records) + allow(record_syncer).to receive(:sync) + end + + it 'extracts the HRID from the filename' do + instance.prepare_and_sync_folio_to_hyacinth_record! + expect(hyacinth_client).to have_received(:find_by_identifier).with( + "clio#{folio_hrid}", + { f: { digital_object_type_display_label_sim: ['Item'] } } + ) + end + + it 'fetches existing Hyacinth records' do + expect(hyacinth_client).to receive(:find_by_identifier).with( + "clio#{folio_hrid}", + { f: { digital_object_type_display_label_sim: ['Item'] } } + ) + instance.prepare_and_sync_folio_to_hyacinth_record! + end + + it 'logs the number of existing records found' do + expect(logger).to receive(:info).with("Found 0 Hyacinth records for FOLIO HRID: #{folio_hrid}") + instance.prepare_and_sync_folio_to_hyacinth_record! + end + + it 'calls sync on the record syncer' do + expect(record_syncer).to receive(:sync).with(marc_file_path, folio_hrid, existing_records) + instance.prepare_and_sync_folio_to_hyacinth_record! + end + + context 'when one existing record is found' do + let(:existing_records) do + [ + { + 'pid' => 'abc123', + 'identifiers' => [{ 'value' => 'clio45678' }] + } + ] + end + + it 'logs the correct count' do + expect(logger).to receive(:info).with("Found 1 Hyacinth records for FOLIO HRID: #{folio_hrid}") + instance.prepare_and_sync_folio_to_hyacinth_record! + end + + it 'passes existing records to syncer' do + expect(record_syncer).to receive(:sync).with(marc_file_path, folio_hrid, existing_records) + instance.prepare_and_sync_folio_to_hyacinth_record! + end + end + + context 'when multiple existing records are found' do + let(:existing_records) do + [ + { 'pid' => 'abc123' }, + { 'pid' => 'def456' } + ] + end + + it 'logs the correct count' do + expect(logger).to receive(:info).with("Found 2 Hyacinth records for FOLIO HRID: #{folio_hrid}") + instance.prepare_and_sync_folio_to_hyacinth_record! + end + end + + context 'when record syncer has no errors' do + it 'does not add to syncing_errors' do + instance.prepare_and_sync_folio_to_hyacinth_record! + expect(instance.syncing_errors).to eq([]) + end + end + + context 'when record syncer has errors' do + let(:syncer_errors) { ['Failed to create record', 'Network timeout'] } + + before do + allow(record_syncer).to receive(:syncing_errors).and_return(syncer_errors) + end + + it 'concatenates sync errors to syncing_errors' do + instance.prepare_and_sync_folio_to_hyacinth_record! + expect(instance.syncing_errors).to eq(syncer_errors) + end + end + + context 'when an exception is raised' do + let(:error_message) { 'Connection refused' } + + before do + allow(hyacinth_client).to receive(:find_by_identifier).and_raise(StandardError.new(error_message)) + end + + it 'logs the error' do + expect(logger).to receive(:error).with("Failed to process #{folio_hrid}: #{error_message}") + instance.prepare_and_sync_folio_to_hyacinth_record! + end + + it 'adds error to syncing_errors' do + instance.prepare_and_sync_folio_to_hyacinth_record! + expect(instance.syncing_errors).to include("Error processing #{folio_hrid}: #{error_message}") + end + end + + context 'when fetching existing records fails' do + before do + allow(hyacinth_client).to receive(:find_by_identifier).and_raise(StandardError.new('API error')) + end + + it 'captures the error without calling sync' do + instance.prepare_and_sync_folio_to_hyacinth_record! + expect(record_syncer).not_to have_received(:sync) + expect(instance.syncing_errors).to include(/Error processing #{folio_hrid}/) + end + end + end + + describe '#extract_hrid_from_filename' do + it 'extracts HRID from .mrc file' do + result = instance.send(:extract_hrid_from_filename, '/tmp/downloads/45678.mrc') + expect(result).to eq('45678') + end + end + + describe '#fetch_existing_hyacinth_records' do + let(:clio_identifier) { "clio#{folio_hrid}" } + let(:search_params) { { f: { digital_object_type_display_label_sim: ['Item'] } } } + + before do + allow(hyacinth_client).to receive(:find_by_identifier).and_return([]) + end + + it 'constructs the correct clio identifier' do + expect(hyacinth_client).to receive(:find_by_identifier).with(clio_identifier, search_params) + instance.send(:fetch_existing_hyacinth_records, folio_hrid) + end + + it 'searches for Item type records' do + expect(hyacinth_client).to receive(:find_by_identifier).with( + anything, + { f: { digital_object_type_display_label_sim: ['Item'] } } + ) + instance.send(:fetch_existing_hyacinth_records, folio_hrid) + end + + it 'returns the search results' do + expected_results = [{ 'pid' => 'test123' }] + allow(hyacinth_client).to receive(:find_by_identifier).and_return(expected_results) + + result = instance.send(:fetch_existing_hyacinth_records, folio_hrid) + expect(result).to eq(expected_results) + end + end +end \ No newline at end of file From 00dabc617bd303dcd476e4a15c23372720252a69 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Wed, 18 Feb 2026 12:48:38 -0500 Subject: [PATCH 7/8] FOLIOSYNC-12 add tests for HyacinthRecordWriter (renamed from RecordSyncer) and Title parsing method --- .rubocop_todo.yml | 6 +- ...rd_syncer.rb => hyacinth_record_writer.rb} | 2 +- .../folio_to_hyacinth/marc_processor.rb | 2 +- .../hyacinth_record_writer_spec.rb | 186 ++++++++++++++++++ .../marc_parsing_methods/title_spec.rb | 67 +++++++ .../folio_to_hyacinth/marc_processor_spec.rb | 4 +- 6 files changed, 260 insertions(+), 7 deletions(-) rename lib/folio_sync/folio_to_hyacinth/{record_syncer.rb => hyacinth_record_writer.rb} (98%) create mode 100644 spec/folio_sync/folio_to_hyacinth/hyacinth_record_writer_spec.rb create mode 100644 spec/folio_sync/folio_to_hyacinth/marc_parsing_methods/title_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e482512..83047a1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-12-08 22:00:37 UTC using RuboCop version 1.78.0. +# on 2026-02-18 17:18:19 UTC using RuboCop version 1.78.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -14,7 +14,7 @@ Metrics/AbcSize: # Offense count: 3 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 144 + Max: 143 # Offense count: 3 # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -35,9 +35,9 @@ Metrics/MethodLength: - 'lib/folio_sync/archives_space_to_folio/job_result_processor.rb' - 'lib/folio_sync/archives_space_to_folio/marc_record_enhancer.rb' - 'lib/folio_sync/archives_space_to_folio/record_processor.rb' + - 'lib/folio_sync/folio_to_hyacinth/hyacinth_record_writer.rb' - 'lib/folio_sync/folio_to_hyacinth/marc_downloader.rb' - 'lib/folio_sync/folio_to_hyacinth/marc_parsing_methods/title.rb' - - 'lib/folio_sync/folio_to_hyacinth/record_syncer.rb' - 'lib/folio_sync/rake/error_logger.rb' - 'lib/hyacinth_api/digital_objects.rb' diff --git a/lib/folio_sync/folio_to_hyacinth/record_syncer.rb b/lib/folio_sync/folio_to_hyacinth/hyacinth_record_writer.rb similarity index 98% rename from lib/folio_sync/folio_to_hyacinth/record_syncer.rb rename to lib/folio_sync/folio_to_hyacinth/hyacinth_record_writer.rb index c40be4c..05695cc 100644 --- a/lib/folio_sync/folio_to_hyacinth/record_syncer.rb +++ b/lib/folio_sync/folio_to_hyacinth/hyacinth_record_writer.rb @@ -2,7 +2,7 @@ module FolioSync module FolioToHyacinth - class RecordSyncer + class HyacinthRecordWriter attr_reader :syncing_errors def initialize diff --git a/lib/folio_sync/folio_to_hyacinth/marc_processor.rb b/lib/folio_sync/folio_to_hyacinth/marc_processor.rb index b9611ca..21904f7 100644 --- a/lib/folio_sync/folio_to_hyacinth/marc_processor.rb +++ b/lib/folio_sync/folio_to_hyacinth/marc_processor.rb @@ -6,7 +6,7 @@ class FolioSync::FolioToHyacinth::MarcProcessor def initialize(marc_file_path) @marc_file_path = marc_file_path @logger = Logger.new($stdout) - @record_syncer = FolioSync::FolioToHyacinth::RecordSyncer.new + @record_syncer = FolioSync::FolioToHyacinth::HyacinthRecordWriter.new @syncing_errors = [] end diff --git a/spec/folio_sync/folio_to_hyacinth/hyacinth_record_writer_spec.rb b/spec/folio_sync/folio_to_hyacinth/hyacinth_record_writer_spec.rb new file mode 100644 index 0000000..e99e002 --- /dev/null +++ b/spec/folio_sync/folio_to_hyacinth/hyacinth_record_writer_spec.rb @@ -0,0 +1,186 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FolioSync::FolioToHyacinth::HyacinthRecordWriter do + let(:instance) { described_class.new } + let(:logger) { instance_double(Logger, info: nil, error: nil, debug: nil) } + let(:hyacinth_client) { instance_double(FolioSync::Hyacinth::Client) } + let(:marc_file_path) { '/tmp/folio_to_hyacinth/downloaded_files/12345678.mrc' } + let(:folio_hrid) { '12345678' } + + before do + allow(Logger).to receive(:new).and_return(logger) + allow(FolioSync::Hyacinth::Client).to receive(:instance).and_return(hyacinth_client) + end + + describe '#initialize' do + it 'sets up a logger' do + expect(instance.instance_variable_get(:@logger)).to eq(logger) + end + + it 'sets up the Hyacinth client' do + expect(instance.instance_variable_get(:@client)).to eq(hyacinth_client) + end + + it 'initializes syncing_errors as empty array' do + expect(instance.syncing_errors).to eq([]) + end + end + + describe '#sync' do + let(:folio_to_hyacinth_record) { instance_double(FolioToHyacinthRecord, digital_object_data: { 'title' => 'Test' }) } + + before do + allow(FolioToHyacinthRecord).to receive(:new).and_return(folio_to_hyacinth_record) + end + + context 'when no existing records are found' do + let(:existing_records) { [] } + + before do + allow(hyacinth_client).to receive(:create_new_record).and_return({ 'success' => true }) + end + + it 'creates a new record' do + expect(hyacinth_client).to receive(:create_new_record).with( + folio_to_hyacinth_record.digital_object_data, + publish: true + ) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'creates a FolioToHyacinthRecord with the marc file path' do + expect(FolioToHyacinthRecord).to receive(:new).with(marc_file_path) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'logs the creation' do + expect(logger).to receive(:info).with(/Creating new Hyacinth record for #{folio_hrid}/) + expect(logger).to receive(:info).with(/Created record for #{folio_hrid}/) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'does not add errors to syncing_errors' do + instance.sync(marc_file_path, folio_hrid, existing_records) + expect(instance.syncing_errors).to eq([]) + end + end + + context 'when exactly one existing record is found' do + let(:existing_pid) { 'abc123' } + let(:existing_identifiers) { ['clio12345678', 'doi:10.1234/test'] } + let(:existing_records) do + [ + { + 'pid' => existing_pid, + 'identifiers' => existing_identifiers + } + ] + end + + before do + allow(hyacinth_client).to receive(:update_existing_record).and_return({ 'success' => true }) + end + + it 'updates the existing record' do + expect(hyacinth_client).to receive(:update_existing_record).with( + existing_pid, + folio_to_hyacinth_record.digital_object_data, + publish: true + ) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'creates a FolioToHyacinthRecord with preserved identifiers' do + expect(FolioToHyacinthRecord).to receive(:new).with( + marc_file_path, + { 'identifiers' => existing_identifiers } + ) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'logs the update' do + expect(logger).to receive(:info).with("Updating existing Hyacinth record for #{folio_hrid}") + expect(logger).to receive(:info).with(/Updated record #{existing_pid}/) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'does not add errors to syncing_errors' do + instance.sync(marc_file_path, folio_hrid, existing_records) + expect(instance.syncing_errors).to eq([]) + end + end + + context 'when multiple existing records are found' do + let(:existing_records) do + [ + { 'pid' => 'abc123' }, + { 'pid' => 'def456' } + ] + end + + it 'does not attempt to create or update records' do + expect(hyacinth_client).not_to receive(:create_new_record) + expect(hyacinth_client).not_to receive(:update_existing_record) + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'logs an error' do + expect(logger).to receive(:error).with("Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}") + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'adds error to syncing_errors' do + instance.sync(marc_file_path, folio_hrid, existing_records) + expect(instance.syncing_errors).to include("Multiple Hyacinth records found for FOLIO HRID #{folio_hrid}") + end + end + + context 'when creating a new record fails' do + let(:existing_records) { [] } + let(:error_message) { 'API connection refused' } + + before do + allow(hyacinth_client).to receive(:create_new_record).and_raise(StandardError.new(error_message)) + end + + it 'logs the error' do + expect(logger).to receive(:error).with("Failed to create record for #{folio_hrid}: #{error_message}") + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'adds error to syncing_errors' do + instance.sync(marc_file_path, folio_hrid, existing_records) + expect(instance.syncing_errors).to include("Failed to create record for #{folio_hrid}: #{error_message}") + end + end + + context 'when updating an existing record fails' do + let(:existing_pid) { 'abc123' } + let(:existing_records) do + [ + { + 'pid' => existing_pid, + 'identifiers' => ['clio12345678'] + } + ] + end + let(:error_message) { 'Couldn\'t update record due to API error' } + + before do + allow(hyacinth_client).to receive(:update_existing_record).and_raise(StandardError.new(error_message)) + end + + it 'logs the error' do + expect(logger).to receive(:error).with("Failed to update record #{existing_pid} for #{folio_hrid}: #{error_message}") + instance.sync(marc_file_path, folio_hrid, existing_records) + end + + it 'adds error to syncing_errors' do + instance.sync(marc_file_path, folio_hrid, existing_records) + expect(instance.syncing_errors).to include("Failed to update record #{existing_pid} for #{folio_hrid}: #{error_message}") + end + end + end +end diff --git a/spec/folio_sync/folio_to_hyacinth/marc_parsing_methods/title_spec.rb b/spec/folio_sync/folio_to_hyacinth/marc_parsing_methods/title_spec.rb new file mode 100644 index 0000000..89bdcaf --- /dev/null +++ b/spec/folio_sync/folio_to_hyacinth/marc_parsing_methods/title_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FolioSync::FolioToHyacinth::MarcParsingMethods::Title do + let(:test_class) do + Class.new do + include FolioSync::FolioToHyacinth::MarcParsingMethods + include FolioSync::FolioToHyacinth::MarcParsingMethods::Title + + def initialize + @digital_object_data = { 'dynamic_field_data' => {} } + end + + def dynamic_field_data + @digital_object_data['dynamic_field_data'] + end + end + end + + let(:instance) { test_class.new } + + it 'registers add_title as a parsing method' do + expect(test_class.registered_parsing_methods).to include(:add_title) + end + + it 'does nothing when 245 field is missing' do + marc_record = MARC::Record.new + instance.add_title(marc_record, nil) + expect(instance.dynamic_field_data['title']).to be_nil + end + + it 'extracts title and splits it into non-sort and sort portions' do + marc_record = MARC::Record.new + marc_record.append(MARC::DataField.new('245', '0', '4', ['a', 'The Great Book'])) + + instance.add_title(marc_record, nil) + + expect(instance.dynamic_field_data['title'].first).to eq( + 'title_non_sort_portion' => 'The ', + 'title_sort_portion' => 'Great Book' + ) + end + + it 'combines $a and $b subfields' do + marc_record = MARC::Record.new + marc_record.append(MARC::DataField.new('245', '0', '0', ['a', 'Main Title'], ['b', 'subtitle'])) + + instance.add_title(marc_record, nil) + + expect(instance.dynamic_field_data['title'].first['title_sort_portion']).to eq('Main Title subtitle') + end + + it 'uses $f instead of $b for oral_history ruleset' do + marc_record = MARC::Record.new + marc_record.append(MARC::DataField.new('245', '0', '0', + ['a', 'Interview'], + ['b', 'ignored'], + ['f', '1965'] + )) + + instance.add_title(marc_record, 'oral_history') + + title = instance.dynamic_field_data['title'].first['title_sort_portion'] + expect(title).to eq('Interview 1965') + end +end diff --git a/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb b/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb index 16082da..35f1659 100644 --- a/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb +++ b/spec/folio_sync/folio_to_hyacinth/marc_processor_spec.rb @@ -6,13 +6,13 @@ let(:marc_file_path) { '/tmp/folio_to_hyacinth/downloaded_files/45678.mrc' } let(:instance) { described_class.new(marc_file_path) } let(:logger) { instance_double(Logger, info: nil, error: nil, debug: nil) } - let(:record_syncer) { instance_double(FolioSync::FolioToHyacinth::RecordSyncer, syncing_errors: []) } + let(:record_syncer) { instance_double(FolioSync::FolioToHyacinth::HyacinthRecordWriter, syncing_errors: []) } let(:hyacinth_client) { instance_double(FolioSync::Hyacinth::Client) } let(:folio_hrid) { '45678' } before do allow(Logger).to receive(:new).and_return(logger) - allow(FolioSync::FolioToHyacinth::RecordSyncer).to receive(:new).and_return(record_syncer) + allow(FolioSync::FolioToHyacinth::HyacinthRecordWriter).to receive(:new).and_return(record_syncer) allow(FolioSync::Hyacinth::Client).to receive(:instance).and_return(hyacinth_client) end From 60657646b5a23b04c183c902488333c6643cab18 Mon Sep 17 00:00:00 2001 From: Weronika Tomaszewska Date: Wed, 18 Feb 2026 12:54:55 -0500 Subject: [PATCH 8/8] FOLIOSYNC-12 update rubocop todo --- .rubocop_todo.yml | 6 +++--- lib/tasks/hyacinth_sync.rake | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 83047a1..69d0e88 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-02-18 17:18:19 UTC using RuboCop version 1.78.0. +# on 2026-02-18 17:54:03 UTC using RuboCop version 1.78.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -14,14 +14,14 @@ Metrics/AbcSize: # Offense count: 3 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 143 + Max: 144 # Offense count: 3 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 8 -# Offense count: 23 +# Offense count: 24 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: diff --git a/lib/tasks/hyacinth_sync.rake b/lib/tasks/hyacinth_sync.rake index 91d0347..f8ee7c6 100644 --- a/lib/tasks/hyacinth_sync.rake +++ b/lib/tasks/hyacinth_sync.rake @@ -110,11 +110,8 @@ namespace :folio_sync do writer.write(marc_record) writer.close end - # puts "Final MARC record: #{marc_record}" - reader = MARC::Reader.new(new_filepath.to_s) reader.each do |record| - # Get author fields by supplying a list of tags record.fields.each_by_tag(['965']) do |field| puts field end