sul-dlss/orcid_client

View on GitHub
lib/sul_orcid_client/work_mapper.rb

Summary

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

class SulOrcidClient
  # Maps a Cocina Description to an Orcid Work.

  # Note that this mapping is currently based on a description generated
  # from an H2 work. However, it could be extended to more completely map descriptions.
  # rubocop:disable Metrics/ClassLength
  # rubocop:disable Metrics/MethodLength
  class WorkMapper
    # Error raised by WorkMapper
    class WorkMapperError < StandardError; end

    def self.map(description:, doi: nil)
      new(description:, doi:).map
    end

    # @param [Cocina::Models::Description] description to map
    # @param [String] doi from identification.doi
    def initialize(description:, doi: nil)
      @description = description
      @doi = doi
    end

    def map
      {
        title: map_title,
        'short-description': map_short_description,
        citation: map_citation,
        type: map_type,
        'publication-date': map_publication_date,
        'external-ids': map_external_ids,
        url: description.purl,
        contributors: map_contributors,
        'language-code': 'en',
        country: {
          value: 'US'
        }
      }.compact
    end

    private

    attr_reader :description, :doi

    def map_title
      title = description.title.first&.value
      raise WorkMapperError, 'Title not mapped' unless title

      {
        title: {
          value: title.truncate(500) # ORCID has a max length for this field
        }
      }
    end

    def map_short_description
      description.note.find { |note| note.type == 'abstract' }&.value
    end

    def map_citation
      citation = description.note.find { |note| note.type == 'preferred citation' }&.value
      return unless citation

      {
        'citation-type': 'formatted-unspecified',
        'citation-value': citation
      }
    end

    def map_external_ids
      { 'external-id':
        [
          map_external_id('uri', description.purl, description.purl)
        ].tap do |ids|
          ids << map_external_id('doi', doi, "https://doi.org/#{doi}") if doi
        end }
    end

    def map_external_id(type, value, url)
      {
        'external-id-type': type,
        'external-id-value': value,
        'external-id-url': {
          value: url
        },
        'external-id-relationship': 'self'
      }
    end

    def map_publication_date
      date = event_value('publication') || event_value('deposit')
      return unless date

      year, month, day = parse_date(date)
      return unless year

      {
        year: {
          value: year
        }
      }.tap do |publication_date|
        publication_date[:month] = { value: month } if month
        publication_date[:day] = { value: day } if month && day
      end
    end

    def parse_date(date)
      matcher = date.match(/(\d{4})-?(\d{2})?-?(\d{2})?/)
      return [nil, nil, nil] unless matcher

      matcher[1..3]
    end

    def event_value(type)
      description&.event&.find { |event| event.type == type }&.date&.first&.value
    end

    H2_TERM_MAP = {
      'Data' => 'data-set',
      'Software/Code' => 'software',
      'Article' => 'journal-article',
      'Book' => 'book',
      'Book chapter' => 'book-chapter',
      'Code' => 'software',
      'Conference session' => 'lecture-speech',
      'Course/instructional materials' => 'manual',
      'Database' => 'data-set',
      'Dramatic performance' => 'artistic-performance',
      'Geospatial data' => 'data-set',
      'Journal/periodical issue' => 'journal-issue',
      'Performance' => 'artistic-performance',
      'Poetry reading' => 'artistic-performance',
      'Poster' => 'conference-poster',
      'Preprint' => 'preprint',
      'Presentation recording' => 'lecture-speech',
      'Questionnaire' => 'research-technique',
      'Report' => 'report',
      'Software' => 'software',
      'Speech' => 'lecture-speech',
      'Statistical model' => 'research-technique',
      'Syllabus' => 'manual',
      'Tabular data' => 'data-set',
      'Technical report' => 'report',
      'Text corpus' => 'data-set',
      'Thesis' => 'dissertation-thesis',
      'Working paper' => 'working-paper'
    }.freeze

    def map_type
      # See https://info.orcid.org/ufaqs/what-work-types-does-orcid-support/
      # For now, only mapping H2 terms; if there is not an H2 term, using "other".

      h2_form = description.form.find { |form| form.source&.value == 'Stanford self-deposit resource types' }
      return 'other' unless h2_form

      map_h2_resource_types(h2_form)
    end

    def map_h2_resource_types(form)
      # Try to match subtypes first, then type
      subtype_terms = form.structuredValue.select { |term| term.type == 'subtype' }.map(&:value)
      type_term = form.structuredValue.find { |term| term.type == 'type' }&.value
      matching_term = (subtype_terms + [type_term]).find { |term| H2_TERM_MAP.key?(term) }
      return 'other' unless matching_term

      H2_TERM_MAP[matching_term]
    end

    def map_contributors
      contributors = description.contributor.filter_map do |contributor|
        ContributorMapper.map(contributor:)
      end.presence
      return unless contributors

      {
        contributor: contributors
      }
    end
  end
  # rubocop:enable Metrics/ClassLength
  # rubocop:enable Metrics/MethodLength
end