diff --git a/app/services/bulkrax/factory_class_finder.rb b/app/services/bulkrax/factory_class_finder.rb index bd85f8cdb..ea3a31fd1 100644 --- a/app/services/bulkrax/factory_class_finder.rb +++ b/app/services/bulkrax/factory_class_finder.rb @@ -2,28 +2,62 @@ module Bulkrax class FactoryClassFinder + ## + # The v6.0.0 default coercer. Responsible for converting a factory class name to a constant. + module DefaultCoercer + ## + # @param name [String] + # @return [Class] when the name is a coercible constant. + # @raise [NameError] when the name is not coercible to a constant. + def self.call(name) + name.constantize + end + end + + ## + # A name coercer that favors classes that end with "Resource" but will attempt to fallback to + # those that don't. + module ValkyrieMigrationCoercer + SUFFIX = "Resource" + + ## + # @param name [String] + # @param suffix [String] the suffix we use for a naming convention. + # + # @return [Class] when the name is a coercible constant. + # @raise [NameError] when the name is not coercible to a constant. + def self.call(name, suffix: SUFFIX) + if name.end_with?(suffix) + name.constantize + else + begin + "#{name}#{suffix}".constantize + rescue NameError + name.constantize + end + end + end + end + ## # @param entry [Bulkrax::Entry] # @return [Class] - def self.find(entry:) - new(entry: entry).find + def self.find(entry:, coercer: Bulkrax.factory_class_name_coercer || DefaultCoercer) + new(entry: entry, coercer: coercer).find end - def initialize(entry:) + def initialize(entry:, coercer:) @entry = entry + @coercer = coercer end - attr_reader :entry + attr_reader :entry, :coercer ## # @return [Class] when we are able to derive the class based on the {#name}. # @return [Nil] when we encounter errors with constantizing the {#name}. # @see #name def find - # TODO: We have a string, now we want to consider how we coerce. Let's say we have Work and - # WorkResource in our upstream application. Work extends ActiveFedora::Base and is legacy. - # And WorkResource extends Valkyrie::Resource and is where we want to be moving. We may want - # to coerce the "Work" name into "WorkResource" - name.constantize + coercer.call(name) rescue NameError nil rescue @@ -41,13 +75,13 @@ def name # details. Array.wrap(entry.parsed_metadata['work_type']).first else - # The string might be frozen, so lets duplicate - entry.default_work_type.dup + entry.default_work_type end - # Let's coerce this into the right shape. - fc.tr!(' ', '_') - fc.downcase! if fc.match?(/[-_]/) + # Let's coerce this into the right shape; we're not mutating the string because it might well + # be frozen. + fc = fc.tr(' ', '_') + fc = fc.downcase if fc.match?(/[-_]/) fc.camelcase rescue entry.default_work_type diff --git a/lib/bulkrax.rb b/lib/bulkrax.rb index 3c4c08a0f..e88f608bf 100644 --- a/lib/bulkrax.rb +++ b/lib/bulkrax.rb @@ -43,6 +43,25 @@ class Configuration # @param adapter [Class] attr_writer :persistence_adapter + ## + # @param coercer [#call] + # @see Bulkrax::FactoryClassFinder + attr_writer :factory_class_name_coercer + + ## + # A function responsible for converting the name of a factory class to the corresponding + # constant. + # + # @return [#call, Bulkrax::FactoryClassFinder::DefaultCoercer] an object responding to call, + # with one positional parameter (e.g. arity == 1) + # + # @example + # Bulkrax.factory_class_name_coercer.call("Work") + # => Work + def factory_class_name_coercer + @factory_class_name_coercer || Bulkrax::FactoryClassFinder::DefaultCoercer + end + ## # Configure the persistence adapter used for persisting imported data. # @@ -99,6 +118,8 @@ def config :default_work_type=, :export_path, :export_path=, + :factory_class_name_coercer, + :factory_class_name_coercer=, :field_mappings, :field_mappings=, :file_model_class, diff --git a/spec/lib/bulkrax_spec.rb b/spec/lib/bulkrax_spec.rb index 4701b004a..e66e3fa7f 100644 --- a/spec/lib/bulkrax_spec.rb +++ b/spec/lib/bulkrax_spec.rb @@ -205,4 +205,14 @@ it { is_expected.to respond_to(:solr_name) } it { is_expected.to respond_to(:clean!) } end + + context '.factory_class_name_coercer' do + subject { described_class.factory_class_name_coercer } + + it { is_expected.to respond_to(:call) } + + it "has a method arity of 1" do + expect(subject.method(:call).arity).to eq 1 + end + end end diff --git a/spec/services/bulkrax/factory_class_finder_spec.rb b/spec/services/bulkrax/factory_class_finder_spec.rb new file mode 100644 index 000000000..1ebdbd4fb --- /dev/null +++ b/spec/services/bulkrax/factory_class_finder_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Bulkrax::FactoryClassFinder do + let(:legacy_work_class) { Class.new } + let(:legacy_work_resource_class) { Class.new } + let(:valkyrie_only_resource_class) { Class.new } + let(:active_fedora_only_class) { Class.new } + + before do + Object.const_set(:LegacyWork, legacy_work_class) + Object.const_set(:LegacyWorkResource, legacy_work_resource_class) + Object.const_set(:ValkyrieOnlyResource, valkyrie_only_resource_class) + Object.const_set(:ActiveFedoraOnly, active_fedora_only_class) + end + + after do + Object.send(:remove_const, :LegacyWork) + Object.send(:remove_const, :LegacyWorkResource) + Object.send(:remove_const, :ValkyrieOnlyResource) + Object.send(:remove_const, :ActiveFedoraOnly) + end + + describe "DefaultCoercer" do + it "simply constantizes (unsafely) the given string" do + factory_class_name = "Work" + expect(described_class::DefaultCoercer.call(factory_class_name)).to eq(Work) + end + end + + describe "ValkyrieMigrationCoercer" do + it 'favors mapping names to those ending in Resource' do + expect(described_class::ValkyrieMigrationCoercer.call("LegacyWork")).to eq(legacy_work_resource_class) + expect(described_class::ValkyrieMigrationCoercer.call("LegacyWorkResource")).to eq(legacy_work_resource_class) + expect(described_class::ValkyrieMigrationCoercer.call("ValkyrieOnlyResource")).to eq(valkyrie_only_resource_class) + expect(described_class::ValkyrieMigrationCoercer.call("ValkyrieOnly")).to eq(valkyrie_only_resource_class) + expect(described_class::ValkyrieMigrationCoercer.call("ActiveFedoraOnly")).to eq(active_fedora_only_class) + expect { described_class::ValkyrieMigrationCoercer.call("ActiveFedoraOnlyResource") }.to raise_error(NameError) + end + end + + describe '.find' do + let(:entry) { double(Bulkrax::Entry, parsed_metadata: { "model" => model_name }, default_work_type: "Work") } + subject(:finder) { described_class.find(entry: entry, coercer: coercer) } + + [ + [Bulkrax::FactoryClassFinder::DefaultCoercer, "Legacy Work", "LegacyWork"], + [Bulkrax::FactoryClassFinder::ValkyrieMigrationCoercer, "Legacy Work", "LegacyWorkResource"], + [Bulkrax::FactoryClassFinder::DefaultCoercer, "Legacy Work Resource", "LegacyWorkResource"] + ].each do |given_coercer, given_model_name, expected_class_name| + context "with an entry with model: #{given_model_name} and coercer: #{given_coercer}" do + let(:model_name) { given_model_name } + let(:coercer) { given_coercer } + + it { is_expected.to eq expected_class_name.constantize } + end + end + end +end