sul-dlss/argo

View on GitHub
app/services/item_change_set_persister.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
95%
# frozen_string_literal: true

# Writes updates Cocina Models
class ItemChangeSetPersister
  # @param [Cocina::Models::DRO] model the orignal state of the model
  # @param [ItemChangeSet] change_set the values to update.
  # @return [Cocina::Models::DRO] the model with updates applied
  def self.update(model, change_set)
    new(model, change_set).update
  end

  def initialize(model, change_set)
    @model = model
    @change_set = change_set
  end

  # @raises [Dor::Services::Client::UnexpectedResponse] when an error occurs updating the object
  def update
    updated_model = update_identification(model)
                    .then { |updated| updated_administrative(updated) }
                    .then { |updated| updated_structural_collections(updated) }
                    .then { |updated| updated_access(updated) }

    Repository.store(updated_model)
  end

  private

  # The map between the change set fields and the Cocina field names
  ACCESS_FIELDS = {
    copyright: :copyright,
    license: :license,
    use_statement: :useAndReproductionStatement,
    view_access: :view,
    download_access: :download,
    access_location: :location,
    controlled_digital_lending: :controlledDigitalLending
  }.freeze

  attr_reader :model, :change_set

  delegate :admin_policy_id, :barcode, :catalog_record_ids, :refresh, :source_id, :collection_ids,
           *ACCESS_FIELDS.keys, :rights_changed?,
           :changed?, to: :change_set

  def object_client
    Dor::Services::Client.object(model.externalIdentifier)
  end

  def updated_administrative(updated)
    return updated unless changed?(:admin_policy_id)

    updated_administrative = updated.administrative.new(hasAdminPolicy: admin_policy_id)
    updated.new(administrative: updated_administrative)
  end

  def updated_structural_collections(updated)
    return updated unless changed?(:collection_ids)

    updated_structural = if collection_ids
                           updated.structural.new(isMemberOf: collection_ids)
                         else
                           updated.structural.to_h.without(:isMemberOf) # clear collection membership
                         end
    updated.new(structural: updated_structural)
  end

  ### Access and structural have to be updated simultaneously or it may trigger
  ### the validation about the file being published when access is dark.
  # @param [Cocina::Models::Dro] updated the DRO metadata to modify
  def updated_access(updated)
    updated.new(
      structural: structural_with_updated_file_access(updated),
      access: updated_object_access(updated)
    )
  end

  # If the rights on the object were changed, then we copy the new rights to the file level
  # @param [Cocina::Models::Dro] updated the DRO metadata to modify
  # @return [Cocina::Models::DROStructural] the new structural modified to have appropriate file access.
  def structural_with_updated_file_access(updated)
    return updated.structural unless rights_changed?

    # Convert to hash so we can mutate it
    structure_hash = updated.structural.to_h
    Array(structure_hash[:contains]).each do |fileset|
      fileset[:structural][:contains].each do |file|
        case view_access
        when 'dark'
          # Ensure files attached to dark objects are neither published nor shelved
          file[:access].merge!(view: 'dark', download: 'none', controlledDigitalLending: false, location: nil)
          file[:administrative][:publish] = false
          file[:administrative].merge!(shelve: false)
        when 'citation-only'
          file[:access].merge!(view: 'dark', download: 'none', controlledDigitalLending: false, location: nil)
        else
          file[:access].merge!(view: view_access,
                               download: download_access,
                               controlledDigitalLending: controlled_digital_lending,
                               location: access_location)
        end
      end
    end
    Cocina::Models::DROStructural.new(structure_hash)
  end

  def access_changed?
    ACCESS_FIELDS.keys.any? { |field| changed?(field) }
  end

  def identification_changed?
    changed?(:source_id) || changed?(:catalog_record_ids) || changed?(:barcode) || changed?(:refresh)
  end

  def updated_object_access(updated)
    access_changed? ? updated.access.new(updated_access_properties) : updated.access
  end

  def updated_access_properties
    {}.tap do |access_properties|
      ACCESS_FIELDS.filter { |field, _cocina_field| changed?(field) }.each do |field, cocina_field|
        val = public_send(field)
        access_properties[cocina_field] = val.is_a?(String) ? val.presence : val # allow boolean false
      end
    end
  end

  def update_identification(updated)
    return updated unless identification_changed?

    identification_props = updated.identification.to_h
    identification_props[:sourceId] = source_id if changed?(:source_id)
    if changed?(:catalog_record_ids) || changed?(:refresh)
      identification_props[:catalogLinks] =
        CatalogRecordId.serialize(model, catalog_record_ids,
                                  refresh:)
    end
    identification_props[:barcode] = barcode.presence if changed?(:barcode)

    updated.new(identification: identification_props.presence)
  end
end