Skip to content

Instantly share code, notes, and snippets.

@mbklein
Last active July 6, 2021 15:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbklein/462912e9ec12efefdf3c6d126804d47f to your computer and use it in GitHub Desktop.
Save mbklein/462912e9ec12efefdf3c6d126804d47f to your computer and use it in GitHub Desktop.
NU's IR DOI Minting Service
class DOI
class Error < StandardError
attr_reader :response
def initialize(response)
@response = response
end
def to_s
"#{response.status} #{response.reason_phrase}\n\t#{response.body}"
end
end
attr_reader :document
delegate :client, :server_reachable?, to: :class
delegate :attributes, :attributes=, to: :document
TOMBSTONE_URL = 'https://www.datacite.org/invalid.html'.freeze
PERMITTED_ATTRIBUTES = %w[
bibtex bodyhasPid citationDoi citeproc contentType contentUrl contributors creators
dates dcIdentifier descriptions doi downloadLatency error event firstPage formats
fundingReferences geoLocations hasSchemaOrg identifier identifierType identifiers isActive
issue landingPage language lastPage metadataVersion prefix publicationYear publisher
redirectCount redirectUrls relatedIdentifiers resourceType rightsList ris schemaOrg schemaOrgid
schemaVersion sizes source state subjects suffix title titles types url url version volume
].freeze
REJECTABLE_VALUES = [nil, '', 0, [], {}].freeze
class << self
def client
return nil unless configured?
if @client.nil?
uri_klass = (Settings.doi_credentials.use_ssl ? URI::HTTPS : URI::HTTP)
datacite_uri = uri_klass.build(host: Settings.doi_credentials.host, port: Settings.doi_credentials.port, path: '/')
@client = Faraday.new(url: datacite_uri).tap do |conn|
conn.basic_auth(Settings.doi_credentials.user, Settings.doi_credentials.password)
end
end
@client
end
def configured?
Settings.doi_credentials.host.present?
end
def server_reachable?
client.get('heartbeat').status == 200
rescue
false
end
end
def initialize(id = nil)
@document = Hashie::Mash.new(id: id.to_s.delete('doi:'), type: 'dois')
reload
end
def id
return nil if document.id.blank?
['doi', document.id].join(':')
end
def reload
return if id.nil?
load_from(client.get("dois/#{document.id}"))
end
def register
attributes.prefix ||= Settings.doi_credentials.default_shoulder
attributes.delete('event')
load_from(client.post('dois', to_json, content_type: 'application/vnd.api+json'))
end
alias register! register
def save
load_from(client.put("dois/#{document.id}", to_json, content_type: 'application/vnd.api+json'))
end
def hide!
document.id.blank? ? register : event('hide')
end
def publish!
register if document.id.blank?
event('publish')
end
def tombstone!
attributes.url = TOMBSTONE_URL
save
end
def to_json
{ 'data' => document }.to_json
end
private
def event(state)
attributes.event = state
save
end
def load_from(response)
raise Error, response unless response.success?
@document = Hashie::Mash.new(JSON.parse(response.body)).data.tap do |result|
result.delete('relationships')
result.attributes.select! { |k, v| PERMITTED_ATTRIBUTES.include?(k) && !REJECTABLE_VALUES.include?(v) }
end
self
end
end
class DoiMintingService
include Rails.application.routes.url_helpers
include ActionDispatch::Routing::PolymorphicRoutes
attr_reader :work, :doi
RESOURCE_TYPE_MAP = {
'Audio' => 'Sound',
'Book' => 'Text',
'Dataset' => 'Dataset',
'Dissertation' => 'Text',
'Image' => 'Image',
'Article' => 'Text',
'Masters Thesis' => 'Text',
'Part of Book' => 'Text',
'Poster' => 'Text',
'Report' => 'Text',
'Research Paper' => 'Text',
'Video' => 'Audiovisual',
'Software or Program Code' => 'Software'
}.freeze
def self.mint_identifier_for(work)
DoiMintingService.new(work).run
rescue => err
Rails.logger.warn("DOI not assigned for work #{work.id}: #{err.message}")
Honeybadger.notify(err)
end
def self.tombstone_identifier_for(work)
DoiMintingService.new(work).tombstone
rescue => err
Rails.logger.warn("DOI #{work.doi} for work #{work.id} not tombstoned: #{err.message}")
Honeybadger.notify(err)
end
def initialize(obj)
@work = obj
@doi = DOI.new(obj.doi)
end
def run
return unless DOI.configured? && DOI.server_reachable?
doi.attributes = metadata
doi.publish!.tap do |result|
if work.doi.nil?
work.doi = result.id
work.save
end
end
end
def tombstone
return unless DOI.configured? && DOI.server_reachable?
doi.tombstone!
end
private
def metadata
Hashie::Mash.new(
doi: work.doi,
creators: creators,
titles: titles,
publisher: publishers,
dates: [dates_created].flatten,
publicationYear: work.create_date.year,
types: resource_type,
url: url
)
end
def list_or_unknown(value)
value.empty? ? ['Unknown'] : value
end
def creators
list_or_unknown(work.creator).collect do |v|
{ name: v, nameIdentifiers: [{}] }
end
end
def titles
list_or_unknown(work.title).collect do |v|
{ title: v, lang: 'und' }
end
end
def publishers
list_or_unknown(work.publisher).join('; ')
end
def dates_created
list_or_unknown(work.date_created).collect do |v|
{ date: v, dateType: 'Created' }
end
end
def resource_type
{
resourceTypeGeneral: RESOURCE_TYPE_MAP.fetch(work.resource_type.first, 'Other'),
resourceType: work.resource_type.first
}
end
def url
polymorphic_url(work, host: Settings.arch.host)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment