sul-dlss/cocina-models

View on GitHub
lib/cocina/models/mapping/to_mods/form.rb

Summary

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

module Cocina
  module Models
    module Mapping
      module ToMods
        # Maps forms from cocina to MODS XML
        class Form # rubocop:disable Metrics/ClassLength
          # NOTE: H2 is the first case of structured form values we're implementing
          H2_SOURCE_LABEL = 'Stanford self-deposit resource types'
          PHYSICAL_DESCRIPTION_TAG = {
            'carrier' => :form,
            'digital origin' => :digitalOrigin,
            'extent' => :extent,
            'form' => :form,
            'material' => :form,
            'media' => :form,
            'media type' => :internetMediaType,
            'reformatting quality' => :reformattingQuality,
            'technique' => :form
          }.freeze

          # @params [Nokogiri::XML::Builder] xml
          # @params [Array<Cocina::Models::DescriptiveValue>] forms
          # @params [IdGenerator] id_generator
          def self.write(xml:, forms:, id_generator:)
            new(xml: xml, forms: forms, id_generator: id_generator).write
          end

          def initialize(xml:, forms:, id_generator:)
            @xml = xml
            @forms = forms
            @id_generator = id_generator
          end

          def write
            other_forms = Array(forms).reject do |form|
              physical_description?(form) || manuscript?(form) || collection?(form)
            end
            is_manuscript = Array(forms).any? { |form| manuscript?(form) }
            is_collection = Array(forms).any? { |form| collection?(form) }

            if other_forms.present?
              write_other_forms(other_forms, is_manuscript, is_collection)
            else
              write_attributes_only(is_manuscript, is_collection)
            end

            write_physical_descriptions
          end

          private

          attr_reader :xml, :forms, :id_generator

          def physical_description?(form, type: nil)
            (form.note.present? && form.type != 'genre') ||
              PHYSICAL_DESCRIPTION_TAG.key?(type) ||
              PHYSICAL_DESCRIPTION_TAG.key?(form.type) ||
              PHYSICAL_DESCRIPTION_TAG.key?(form.groupedValue&.first&.type) ||
              (form.parallelValue.present? && physical_description?(form.parallelValue.first))
          end

          def manuscript?(form)
            form.value == 'manuscript' && form.source&.value == 'MODS resource types'
          end

          def collection?(form)
            form.value == 'collection' && form.source&.value == 'MODS resource types'
          end

          def write_other_forms(forms, is_manuscript, is_collection)
            forms.each do |form|
              if form.parallelValue.present?
                write_parallel_forms(form, is_manuscript, is_collection)
              else
                write_form(form, is_manuscript, is_collection)
              end
            end
          end

          def write_parallel_forms(form, is_manuscript, is_collection)
            alt_rep_group = id_generator.next_altrepgroup
            form.parallelValue.each do |form_value|
              write_form(form_value, is_manuscript, is_collection, alt_rep_group: alt_rep_group)
            end
          end

          def write_form(form, is_manuscript, is_collection, alt_rep_group: nil)
            if form.structuredValue.present?
              write_structured(form)
            elsif form.value
              write_basic(form, is_manuscript: is_manuscript, is_collection: is_collection,
                                alt_rep_group: alt_rep_group)
            end
          end

          def write_physical_descriptions
            parallel_physical_descr_forms, other_physical_descr_forms = Array(forms).select do |form|
                                                                          physical_description?(form)
                                                                        end.partition { |form| form.parallelValue.present? }
            write_physical_description(other_physical_descr_forms)

            parallel_physical_descr_forms.each do |parallel_physical_descr_form|
              alt_rep_group = id_generator.next_altrepgroup
              write_physical_description(parallel_physical_descr_form.parallelValue, alt_rep_group: alt_rep_group,
                                                                                     display_label: parallel_physical_descr_form.displayLabel,
                                                                                     form: parallel_physical_descr_form)
            end
          end

          # rubocop:disable Metrics/CyclomaticComplexity
          def write_physical_description(physical_descr_forms, alt_rep_group: nil, display_label: nil, form: nil)
            grouped_forms = []
            # Each of these are its own physicalDescription
            simple_forms = []
            # These all get grouped together to form a single physicalDescription.
            other_forms = []
            other_notes = []
            Array(physical_descr_forms).select do |physical_descr_form|
              physical_description?(physical_descr_form, type: form&.type)
            end.each do |physical_descr_form|
              if physical_descr_form.groupedValue.present?
                grouped_forms << physical_descr_form
              elsif merge_form?(physical_descr_form, display_label) || alt_rep_group
                simple_forms << physical_descr_form
              else
                other_notes << physical_descr_form if physical_descr_form.note.present?
                other_forms << physical_descr_form if physical_descr_form.value
              end
            end

            if other_forms.present?
              write_basic_physical_description(other_forms, other_notes, alt_rep_group: alt_rep_group, form: form)
            else
              other_notes.each do |other_note|
                write_basic_physical_description([], [other_note], alt_rep_group: alt_rep_group, form: form)
              end
            end
            simple_forms.each do |simple_form|
              write_basic_physical_description([simple_form], [simple_form], alt_rep_group: alt_rep_group,
                                                                             form: form)
            end
            grouped_forms.each do |grouped_form|
              write_grouped_physical_description(grouped_form, alt_rep_group: alt_rep_group, form: form)
            end
          end

          # rubocop:enable Metrics/CyclomaticComplexity
          def merge_form?(form, display_label)
            form.value && (Array(form.note).any? do |note|
                             note.type != 'unit'
                           end || form.displayLabel || display_label)
          end

          def write_basic_physical_description(forms, note_forms, alt_rep_group: nil, form: nil)
            physical_description_attrs = {
              displayLabel: forms.first&.displayLabel || form&.displayLabel,
              altRepGroup: alt_rep_group
            }.compact

            xml.physicalDescription physical_description_attrs do
              write_physical_description_form_values(forms, form: form)
              note_forms.each { |note_form| write_notes(note_form) if note_form.present? }
            end
          end

          def write_grouped_physical_description(grouped_form, alt_rep_group: nil, form: nil)
            physical_description_attrs = {
              displayLabel: grouped_form.displayLabel || form&.displayLabel,
              altRepGroup: alt_rep_group
            }.compact

            xml.physicalDescription physical_description_attrs do
              write_physical_description_form_values(grouped_form.groupedValue, form: form)
              write_notes(grouped_form)
            end
          end

          def write_physical_description_form_values(form_values, form: nil)
            form_values.each do |form_value|
              form_type = form_value.type || form&.type
              attributes = {
                unit: unit_for(form_value)
              }.tap do |attrs|
                attrs[:type] = form_type if PHYSICAL_DESCRIPTION_TAG.fetch(form_type) == :form && form_type != 'form'
              end.compact

              xml.public_send PHYSICAL_DESCRIPTION_TAG.fetch(form_type), form_value.value,
                              attributes.merge(uri_attrs(form_value)).merge(uri_attrs(form))
            end
          end

          def write_notes(form)
            Array(form.note).reject { |note| note.type == 'unit' }.each do |note|
              attributes = {
                displayLabel: note.displayLabel,
                type: note.type
              }.compact
              xml.note note.value, attributes
            end
          end

          def write_basic(form, is_manuscript: false, is_collection: false, alt_rep_group: nil)
            return write_datacite(form) if form.source&.value == 'DataCite resource types'

            attributes = form_attributes(form, alt_rep_group)

            case form.type
            when 'resource type'
              attributes[:manuscript] = 'yes' if is_manuscript
              attributes[:collection] = 'yes' if is_collection
              xml.typeOfResource form.value, attributes
            when 'map scale', 'map projection'
              # do nothing, these end up in subject/cartographics
            else # genre
              xml.genre form.value, attributes.merge({ type: genre_type_for(form) }.compact)
            end
          end

          def write_datacite(form)
            xml.extension displayLabel: 'datacite' do
              xml.resourceType(datacite_resource_type, resourceTypeGeneral: form.value)
            end
          end

          def datacite_resource_type
            self_deposit_types = forms.find do |candidate|
              candidate.source.value == 'Stanford self-deposit resource types'
            end
            return unless self_deposit_types

            parts = self_deposit_types.structuredValue.select do |val|
              val.type == 'subtype'
            end.presence || self_deposit_types.structuredValue
            parts.map(&:value).join('; ')
          end

          def unit_for(form)
            Array(form.note).find { |note| note.type == 'unit' }&.value
          end

          def genre_type_for(form)
            Array(form.note).find { |note| note.type == 'genre type' }&.value
          end

          def form_attributes(form, alt_rep_group)
            {
              altRepGroup: alt_rep_group,
              displayLabel: form.displayLabel,
              usage: form.status,
              lang: form.valueLanguage&.code,
              script: form.valueLanguage&.valueScript&.code
            }.merge(uri_attrs(form)).compact
          end

          def write_attributes_only(is_manuscript, is_collection)
            return unless is_manuscript || is_collection

            attributes = {}
            attributes[:manuscript] = 'yes' if is_manuscript
            attributes[:collection] = 'yes' if is_collection
            xml.typeOfResource(nil, attributes)
          end

          def write_structured(form)
            # The only use case we're supporting for structured forms at the
            # moment is for H2. Short-circuit if that's not what we get.
            return if form.source.value != H2_SOURCE_LABEL

            form.structuredValue.each do |genre|
              xml.genre genre.value, type: "H2 #{genre.type}"
            end
          end

          def uri_attrs(form)
            return {} if form.nil?

            {
              valueURI: form.uri,
              authorityURI: form.source&.uri,
              authority: form.source&.code
            }.compact
          end
        end
      end
    end
  end
end