• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / pdc_describe / 1aaf6302-d8cf-4943-bb96-5e86951c32a3

pending completion
1aaf6302-d8cf-4943-bb96-5e86951c32a3

Pull #1079

circleci

Bess Sadler
Nil safe doi gsub
Pull Request #1079: Nil safe collection title

2 of 2 new or added lines in 1 file covered. (100.0%)

1777 of 2063 relevant lines covered (86.14%)

100.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.24
/app/models/pdc_serialization/datacite.rb
1
# frozen_string_literal: true
2
module PDCSerialization
1✔
3
  # The common use for this class is:
4
  #
5
  #   work = Work.find(123)
6
  #   datacite_xml = PDCSerialization::Datacite.new_from_work(work).to_xml
7
  #
8
  # For testing purposes you can also quickly get an XML serialization with the helper:
9
  #
10
  #   datacite_xml = PDCSerialization::Datacite.skeleton_datacite_xml(...)
11
  #
12
  # You can also pass a PDCMetadata::Resource which is useful to test with a more
13
  # complex dataset without saving the work to the database:
14
  #
15
  #   jsonb_hash = JSON.parse({...}.to_json)
16
  #   resource = PDCMetadata::Resource.new_from_jsonb(jsonb_hash)
17
  #   datacite_xml = PDCSerialization::Datacite.new_from_work_resource(resource).to_xml
18
  #
19
  # For information
20
  #   Datacite schema: https://support.datacite.org/docs/datacite-metadata-schema-v44-properties-overview
21
  #   Datacite mapping gem: https://github.com/CDLUC3/datacite-mapping
22
  #
23
  # rubocop:disable Metrics/ClassLength
24
  class Datacite
1✔
25
    attr_reader :mapping, :errors
1✔
26

27
    def initialize(mapping)
1✔
28
      @mapping = mapping
31✔
29
      @errors = []
31✔
30
    end
31

32
    # Returns the XML serialization for the Datacite record
33
    # Note that the actual XML serialization is done by the datacite-mapping gem.
34
    def to_xml
1✔
35
      @mapping.write_xml
43✔
36
    end
37

38
    # Validate this DataCite XML serialization against the official DataCite schema
39
    # By default we validate against DataCite 4.4. This will shift over time as new
40
    # versions of the datacite schema are released.
41
    # @return [Boolean]
42
    def valid?
1✔
43
      @errors = []
12✔
44
      datacite_xml = Nokogiri::XML(to_xml)
12✔
45
      schema_location = Rails.root.join("config", "schema")
12✔
46
      Dir.chdir(schema_location) do
12✔
47
        xsd = Nokogiri::XML::Schema(File.read("datacite_4_4.xsd"))
12✔
48
        xsd.validate(datacite_xml).each do |error|
12✔
49
          @errors << error
×
50
        end
51
      end
52
      return true if @errors.empty?
12✔
53
      false
×
54
    end
55

56
    ##
57
    # Returns the XML serialization for a valid Datacite skeleton record based on a few required values.
58
    # Useful for early in the workflow when we don't have much data yet and for testing.
59
    #
60
    # @param [String] identifier, e.g "10.1234/567"
61
    # @param [String] title
62
    # @param [String] creator
63
    # @param [String] publisher
64
    # @param [String] publication_year
65
    # @param [String] resource_type
66
    def self.skeleton_datacite_xml(identifier:, title:, creator:, publisher:, publication_year:, resource_type:)
1✔
67
      mapping = ::Datacite::Mapping::Resource.new(
×
68
        identifier: ::Datacite::Mapping::Identifier.new(value: identifier),
69
        creators: [] << ::Datacite::Mapping::Creator.new(name: creator),
70
        titles: [] << ::Datacite::Mapping::Title.new(value: title),
71
        publisher: ::Datacite::Mapping::Publisher.new(value: publisher),
72
        publication_year: publication_year,
73
        resource_type: datacite_resource_type(resource_type)
74
      )
75
      mapping.write_xml
×
76
    end
77

78
    ##
79
    # Creates a PDCSerialization::Datacite object from a Work
80
    def self.new_from_work(work)
1✔
81
      new_from_work_resource(work.resource)
12✔
82
    end
83

84
    ##
85
    # Creates a PDCSerialization::Datacite object from a PDCMetadata::Resource
86
    # rubocop:disable Metrics/MethodLength
87
    def self.new_from_work_resource(resource)
1✔
88
      mapping = ::Datacite::Mapping::Resource.new(
31✔
89
        identifier: ::Datacite::Mapping::Identifier.new(value: resource.doi),
90
        creators: creators_from_work_resource(resource.creators),
91
        contributors: individual_contributors_from_work_resource(resource.individual_contributors) +
92
          organizational_contributors_from_work_resource(resource.organizational_contributors),
93
        descriptions: descriptions_from_work_resource(resource.description),
94
        titles: titles_from_work_resource(resource.titles),
95
        publisher: ::Datacite::Mapping::Publisher.new(value: resource.publisher),
96
        publication_year: resource.publication_year,
97
        resource_type: datacite_resource_type(resource.resource_type),
98
        related_identifiers: related_identifiers_from_work_resource(resource),
99
        rights_list: rights_from_work_resource(resource),
100
        version: resource.version_number,
101
        funding_references: funding_references_from_work_resource(resource)
102
      )
103
      Datacite.new(mapping)
31✔
104
    end
105
    # rubocop:enable Metrics/MethodLength
106

107
    class << self
1✔
108
      def datacite_resource_type(value)
1✔
109
        resource_type = ::Datacite::Mapping::ResourceTypeGeneral.find_by_value(value)
31✔
110
        ::Datacite::Mapping::ResourceType.new(resource_type_general: resource_type)
31✔
111
      end
112

113
      def datacite_contributor_type(value)
1✔
114
        ::Datacite::Mapping::ContributorType.find_by_value(value)
×
115
      end
116

117
      private
1✔
118

119
        def creators_from_work_resource(creators)
1✔
120
          creators.sort_by(&:sequence).map do |creator|
31✔
121
            ::Datacite::Mapping::Creator.new(
107✔
122
              name: creator.value,
123
              given_name: creator.given_name,
124
              family_name: creator.family_name,
125
              identifier: name_identifier_from_identifier(creator.identifier),
126
              affiliations: nil
127
            )
128
          end
129
        end
130

131
        def individual_contributors_from_work_resource(contributors)
1✔
132
          contributors.sort_by(&:sequence).map do |contributor|
31✔
133
            ::Datacite::Mapping::Contributor.new(
×
134
              name: contributor.value,
135
              identifier: name_identifier_from_identifier(contributor.identifier),
136
              affiliations: nil,
137
              type: datacite_contributor_type(contributor.type)
138
            )
139
          end
140
        end
141

142
        def organizational_contributors_from_work_resource(contributors)
1✔
143
          contributors.map do |contributor|
31✔
144
            ::Datacite::Mapping::Contributor.new(
×
145
              name: contributor.value,
146
              identifier: name_identifier_from_identifier(contributor.identifier),
147
              affiliations: nil,
148
              type: datacite_contributor_type(contributor.type)
149
            )
150
          end
151
        end
152

153
        ##
154
        # We are deliberately not differentiating between "abstract", "methods" and other kinds of descriptions.
155
        # We are instead putting all description into the same field and classifying it as "OTHER".
156
        def descriptions_from_work_resource(description)
1✔
157
          return [] if description.blank?
31✔
158
          [] << ::Datacite::Mapping::Description.new(type: ::Datacite::Mapping::DescriptionType::OTHER, value: description)
31✔
159
        end
160

161
        def name_identifier_from_identifier(identifier)
1✔
162
          return nil if identifier.nil?
107✔
163
          ::Datacite::Mapping::NameIdentifier.new(
6✔
164
            scheme: identifier.scheme,
165
            scheme_uri: identifier.scheme_uri,
166
            value: identifier.value
167
          )
168
        end
169

170
        def titles_from_work_resource(titles)
1✔
171
          titles.map do |title|
31✔
172
            if title.main?
32✔
173
              ::Datacite::Mapping::Title.new(value: title.title)
31✔
174
            else
175
              title_type = ::Datacite::Mapping::TitleType.find_by_value(title.title_type)
1✔
176
              ::Datacite::Mapping::Title.new(value: title.title, type: title_type) if title_type
1✔
177
            end
178
          end.compact
179
        end
180

181
        ##
182
        # Add related identifiers from various locations in the metadata.
183
        # @param [PDCMetadata::Resource] resource
184
        # @return [<::Datacite::Mapping::RelatedIdentifier>]
185
        def related_identifiers_from_work_resource(resource)
1✔
186
          related_identifiers = []
31✔
187
          related_identifiers = related_identifiers.union(extract_ark_as_related_identfier(resource))
31✔
188
          related_identifiers = related_identifiers.union(extract_related_objects(resource))
31✔
189
          related_identifiers
31✔
190
        end
191

192
        def extract_ark_as_related_identfier(resource)
1✔
193
          related_ids = []
31✔
194
          if resource.ark.present?
31✔
195
            related_ids << ::Datacite::Mapping::RelatedIdentifier.new(
15✔
196
              relation_type: ::Datacite::Mapping::RelationType::IS_IDENTICAL_TO,
197
              value: resource.ark,
198
              identifier_type: ::Datacite::Mapping::RelatedIdentifierType::ARK
199
            )
200
          end
201
          related_ids
31✔
202
        end
203

204
        def extract_related_objects(resource)
1✔
205
          related_objects = []
31✔
206
          resource.related_objects.each do |ro|
31✔
207
            related_objects << ::Datacite::Mapping::RelatedIdentifier.new(
5✔
208
              relation_type: ::Datacite::Mapping::RelationType.find_by_value(ro.relation_type),
209
              value: ro.related_identifier,
210
              identifier_type: ::Datacite::Mapping::RelatedIdentifierType.find_by_value(ro.related_identifier_type)
211
            )
212
          end
213
          related_objects
31✔
214
        end
215

216
        def rights_from_work_resource(resource)
1✔
217
          rights = []
31✔
218
          if resource.rights.present?
31✔
219
            rights << ::Datacite::Mapping::Rights.new(
31✔
220
              value: resource.rights.name,
221
              uri: resource.rights.uri,
222
              identifier: resource.rights.identifier
223
            )
224
          end
225
          rights
31✔
226
        end
227

228
        def funding_references_from_work_resource(resource)
1✔
229
          resource.funders.map do |funder|
31✔
230
            award = ::Datacite::Mapping::AwardNumber.new(uri: funder.award_uri, value: funder.award_number)
21✔
231
            if funder.ror.present?
21✔
232
              type = ::Datacite::Mapping::FunderIdentifierType::ROR
21✔
233
              funder_identifier = ::Datacite::Mapping::FunderIdentifier.new(type: type, value: funder.ror)
21✔
234
              ::Datacite::Mapping::FundingReference.new(name: funder.funder_name, award_number: award, identifier: funder_identifier)
21✔
235
            else
236
              ::Datacite::Mapping::FundingReference.new(name: funder.funder_name, award_number: award)
×
237
            end
238
          end
239
        end
240
      end
241
  end
242
  # rubocop:enable Metrics/ClassLength
243
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc