Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract out description. #829

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ gem 'bootsnap', '>= 1.1.0', require: false # Reduces boot times through caching;
gem 'cancancan' # authorization
gem 'config' # simple rails environment specific config
gem "cssbundling-rails", "~> 1.1"
gem 'dry-struct' # immutable value objects
gem 'faraday' # HTTP client
gem "geo_coord", require: "geo/coord"
gem 'honeybadger' # exception reporting
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ GEM
dry-logic (>= 1.4, < 2)
dry-types (>= 1.7, < 2)
zeitwerk (~> 2.6)
dry-struct (1.6.0)
dry-core (~> 1.0, < 2)
dry-types (>= 1.7, < 2)
ice_nine (~> 0.11)
zeitwerk (~> 2.6)
dry-types (1.7.1)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
Expand Down Expand Up @@ -190,6 +195,7 @@ GEM
htmlentities (4.3.4)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
iiif-presentation (1.2.0)
activesupport (>= 3.2.18)
faraday (~> 2.7)
Expand Down Expand Up @@ -445,6 +451,7 @@ DEPENDENCIES
debug
dlss-capistrano
dor-rights-auth (~> 1.6)
dry-struct
faraday
geo_coord
honeybadger
Expand Down
26 changes: 14 additions & 12 deletions app/models/purl_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def schema_dot_org?
concerning :Metadata do
def title
if mods?
# This is from ModsDisplay::HTML
Array.wrap(mods.title).join(' -- ')
else
public_xml.title
Expand All @@ -161,6 +162,7 @@ def description
return unless mods?

@description ||= begin
# This is from ModsDisplay::HTML
abstract = mods.abstract.detect { |a| a.respond_to? :values }
if abstract
abstract.values.join.strip
Expand Down Expand Up @@ -212,6 +214,8 @@ def use_and_reproduction

delegate :released_to?, to: :public_xml

delegate :doi, :doi_id, to: :desc

def representative_thumbnail?
representative_thumbnail.present?
end
Expand All @@ -220,22 +224,12 @@ def representative_thumbnail
"#{iiif_manifest.thumbnail_base_uri}/full/!400,400/0/default.jpg" if iiif_manifest.thumbnail_base_uri.present?
end

# @return [String,nil] DOI (with https://doi.org/ prefix) if present
def doi
@doi ||= mods_ng_document.root&.at_xpath('mods:identifier[@type="doi"]', mods: MODS_NS)&.text
end

# @return [String,nil] DOI (without https://doi.org/ prefix) if present
def doi_id
doi&.delete_prefix('https://doi.org/')
end

def publication_date
@publication_date ||= ::Metadata::PublicationDate.call(mods_ng_document)
desc.publication_year
end

def authors
@authors ||= ::Metadata::Authors.call(mods_ng_document)
desc.formatted_contributors
end

def schema_dot_org
Expand Down Expand Up @@ -325,6 +319,14 @@ def mods_ng_document
@mods_ng_document ||= Nokogiri::XML(mods_body)
end

def cocina_json
@cocina_json ||= cocina_body.present? ? JSON.parse(cocina_body) : nil
end

def desc
@desc ||= Description.new(mods_ng: mods_ng_document, cocina_json:)
end

def logger
Rails.logger
end
Expand Down
88 changes: 88 additions & 0 deletions lib/description.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# require 'dry-struct'

class Description
MODS_NS = 'http://www.loc.gov/mods/v3'.freeze

# Temporarily accepting nils to avoid fixing all tests.
def initialize(mods_ng: nil, cocina_json: nil)
@mods_ng = mods_ng
@cocina_json = cocina_json
end

# Conventions:
# * Prefer cocina naming.
# * "formatted_" indicates that a complex structure has been reduced to a string.

delegate :doi, :doi_id, to: :mods_identifier
delegate :publication_year, to: :mods_origin_info
delegate :formatted_title, to: :cocina_title
delegate :descriptions, :formatted_description, to: :cocina_note
delegate :contributors, to: :cocina_contributor

def formatted_contributors
# Temporary workaround to avoid fixing all tests.
# Otherwise, would be: delegate :doi, to: :cocina_identifier
cocina_json.present? ? cocina_contributor.formatted_contributors : mods_formatted_name.formatted_names
end

def doi
# Temporary workaround to avoid fixing all tests.
# Otherwise, would be: delegate :doi, to: :cocina_identifier
cocina_json.present? ? cocina_identifier.doi : mods_identifier.doi
end

def doi_id
# Temporary workaround to avoid fixing all tests.
# Otherwise, would be: delegate :doi_id, to: :cocina_identifier
cocina_json.present? ? cocina_identifier.doi_id : mods_identifier.doi_id
end

private

attr_reader :mods_ng, :cocina_json

def cocina_identifier
@cocina_identifier ||= CocinaIdentifier.new(cocina_json:)
end

def mods_identifier
@mods_identifier ||= ModsIdentifier.new(mods_ng:)
end

def mods_origin_info
@mods_origin_info ||= ModsOriginInfo.new(mods_ng:)
end

def mods_formatted_name
@mods_formatted_name ||= ModsFormattedName.new(mods_ng:)
end

def cocina_title
@cocina_title ||= CocinaTitle.new(cocina_json:)
end

def cocina_note
@cocina_note ||= CocinaNote.new(cocina_json:)
end

def cocina_contributor
@cocina_contributor ||= CocinaContributor.new(cocina_json:)
end

module Types
include Dry.Types()
end

# Base class for Structs
class DescriptionStruct < Dry::Struct
transform_keys(&:to_sym)
schema schema.strict
end

class Contributor < DescriptionStruct
attribute :name, Types::String
attribute? :forename, Types::String
attribute? :surname, Types::String
attribute? :orcid, Types::String
end
end
81 changes: 81 additions & 0 deletions lib/description/cocina_contributor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
class Description
class CocinaContributor
def initialize(cocina_json:)
@cocina_json = cocina_json
end

# @return [Array<Description::Contributor>] contributors
def contributors
@contributors ||= cocina_contributors.map { |cocina_contributor| contributor(cocina_contributor) }
end

# @return [Array<String>] contributors
def formatted_contributors
@formatted_contributors ||= contributors.map(&:name)
end

private

attr_reader :cocina_json

def cocina_contributors
JsonPath.new('$.description.contributor[*]').on(@cocina_json)
end

def contributor(cocina_contributor)
Description::Contributor.new(
**ContributorBuilder.new(cocina_contributor:).build
)
end

class ContributorBuilder
def initialize(cocina_contributor:)
@cocina_contributor = cocina_contributor
end

def build
{ name:,
forename:,
surname:,
orcid: }.compact
end

private

attr_reader :cocina_contributor

def name
# contributor.name.value or concatenated contributor.name.structuredValue
JsonPath.new('$.name.value').first(cocina_contributor) || structured_name
end

def structured_name
# concatenated contributor.name.structuredValue
[forename, surname].join(' ')
end

def forename
# contributor.name.structuredValue.value with type "forename"
JsonPath.new("$.name[0].structuredValue[*].[?(@['type'] == 'forename')].value").first(cocina_contributor)
end

def surname
# contributor.name.structuredValue.value with type "surname"
JsonPath.new("$.name[0].structuredValue[*].[?(@['type'] == 'surname')].value").first(cocina_contributor)
end

def orcid
# contributor.identifier.uri or contributor.identifier.value with type "orcid" (case-insensitive), made into URI if identifier only
id_uri = JsonPath.new('$.identifier.uri').first(cocina_contributor)
return id_uri if id_uri.present?

orcid = JsonPath.new("$.identifier.[?(@['type'] == 'ORCID' || @['type'] == 'orcid')].value").first(cocina_contributor)
return if orcid.blank?

return orcid if orcid.start_with?('https://orcid.org')

URI.join('https://orcid.org/', orcid).to_s
end
end
end
end
31 changes: 31 additions & 0 deletions lib/description/cocina_identifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class Description
class CocinaIdentifier
def initialize(cocina_json:)
@cocina_json = cocina_json
end

# identification.doi or identifier.uri or identifier.value with type "doi" (case-insensitive), made into URI if identifier only
# @return [String,nil] DOI (with https://doi.org/ prefix) if present
def doi
@doi ||= begin
identifier = JsonPath.new('$.identification.doi').first(@cocina_json) ||
JsonPath.new('$.description.identifier..uri').first(@cocina_json) ||
JsonPath.new("$.description.identifier[?(@['type'] == 'doi')].value").first(@cocina_json)
if identifier&.start_with?('https://doi.org')
identifier
elsif identifier
URI.join('https://doi.org', identifier).to_s
end
end
end

# @return [String,nil] DOI (without https://doi.org/ prefix) if present
def doi_id
doi&.delete_prefix('https://doi.org/')
end

private

attr_reader :cocina_json
end
end
22 changes: 22 additions & 0 deletions lib/description/cocina_note.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Description
class CocinaNote
def initialize(cocina_json:)
@cocina_json = cocina_json
end

# value for description.note where type=summary or type=abstract
# @return [Array<String>] description notes
def descriptions
@descriptions ||= JsonPath.new("$.description.note[?(@['type'] == 'summary' || @['type'] == 'abstract')].value").on(cocina_json)
end

# @return [String, nil] formatted description
def formatted_description(delimiter: '\n')
@formatted_description ||= descriptions.join(delimiter) unless descriptions.empty?
end

private

attr_reader :cocina_json
end
end
25 changes: 25 additions & 0 deletions lib/description/cocina_title.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Description
class CocinaTitle
def initialize(cocina_json:)
@cocina_json = cocina_json
end

# Concatenated title.structuredValue for title with status "primary" if present
# Otherwise, title.value for first title
# @return [String, nil] formatted title
def formatted_title(delimiter: '\n')
@formatted_title ||= begin
titles = JsonPath.new("$.description.title[?(@['status' == 'primary'])].structuredValue[*].value").on(cocina_json)
if titles.present?
titles.join(delimiter)
else
JsonPath.new('$.description.title[0].value').first(cocina_json)
end
end
end

private

attr_reader :cocina_json
end
end
40 changes: 40 additions & 0 deletions lib/description/mods_formatted_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class Description
class ModsFormattedName
def initialize(mods_ng:)
@mods_ng = mods_ng
end

# Names with author roles.
# Otherwise, names without roles.
# Otherwise, first name with any role.
# Names are formatted as a string with ModsDisplay::NameFormatter.
# @return [Array<String>] formatted names
def formatted_names
@formatted_names ||= name_elements.map { |name_element| ModsDisplay::NameFormatter.format(name_element) }
end

private

attr_reader :mods_ng

def name_elements
names_with_author_roles.to_a.presence \
|| names_without_roles.to_a.presence \
|| [first_name_with_any_role].compact.presence \
|| []
end

def names_with_author_roles
mods_ng.root&.xpath('mods:name[mods:role/mods:roleTerm[contains(text(), "AUT") ' \
'or contains(text(), "aut") or contains(text(), "author") or contains(text(), "Author")]]', mods: MODS_NS)
end

def names_without_roles
mods_ng.root&.xpath('mods:name[count(mods:role) = 0]', mods: MODS_NS)
end

def first_name_with_any_role
mods_ng.root&.at_xpath('mods:name[mods:role]', mods: MODS_NS)
end
end
end
Loading