neu-mods 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/README.md +7 -0
- data/lib/neu/mods/projection.rb +99 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5416d755319f464e252ef4fa07d0372efb11e1379d76ad6155b78f42a61c221a
|
|
4
|
+
data.tar.gz: ddfcf9eb792155c216bf9710ef6cfcc379f914791a782da30577158bb02a1da7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d465b12175d8b15f7da7d7c355af0246294b3e7df0c276be775b6ccf41c14840f716fa7624ed86a54bff69da56625041cf7ca1f245e9ff6bb8f4035b44f70799
|
|
7
|
+
data.tar.gz: ce373c9bb16d1a7e6142a4e0556e669ddb07d0bbe77db172f7954d4d54eb298b4a688c5cf3a60bd5237611670bbfb162c67d7d9c1bd5377a6579d0c78b9a9030
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.9.0
|
data/README.md
CHANGED
|
@@ -50,6 +50,13 @@ doc.related_items # => [{ type: "otherFormat", title: "..." }, ...]
|
|
|
50
50
|
# every relatedItem that is not a series or a host
|
|
51
51
|
doc.location # => [{ physical_location:, shelf_location:, url: }, ...]
|
|
52
52
|
doc.map_data # => [{ scale:, projection:, coordinates: }, ...]
|
|
53
|
+
doc.title_subjects # => ["The Great Gatsby"] composed like the main title
|
|
54
|
+
doc.hierarchical_geographic_subjects
|
|
55
|
+
# => [{ country:, state:, city:, ... }, ...] eleven levels,
|
|
56
|
+
# structured for the reason map_data is
|
|
57
|
+
doc.record_info # => { content_source:, origin:, description_standard:,
|
|
58
|
+
# creation_date:, change_date:, language_of_cataloging: }
|
|
59
|
+
# describes the CATALOGUING, not the resource
|
|
53
60
|
doc.to_h # => full projection, keyed to Atlas's Metadata::MODS attributes
|
|
54
61
|
|
|
55
62
|
# The field registry -- the single declaration of what this gem projects.
|
data/lib/neu/mods/projection.rb
CHANGED
|
@@ -130,6 +130,37 @@ module NEU
|
|
|
130
130
|
def personal_name_subjects = name_subjects("personal")
|
|
131
131
|
def corporate_name_subjects = name_subjects("corporate")
|
|
132
132
|
|
|
133
|
+
def genre_subjects = texts_at("/mods:mods/mods:subject/mods:genre")
|
|
134
|
+
|
|
135
|
+
# A MARC GAC code. Projected as the record wrote it: turning it into a
|
|
136
|
+
# place name needs a lookup table, which is the same call the gem already
|
|
137
|
+
# made for MARC relators -- the label vocabulary belongs to the consumer.
|
|
138
|
+
def geographic_code_subjects = texts_at("/mods:mods/mods:subject/mods:geographicCode")
|
|
139
|
+
|
|
140
|
+
# A subject that is a work has a nonSort, a subTitle and part numbers like
|
|
141
|
+
# any other titleInfo, so it composes through the same port as the main
|
|
142
|
+
# title rather than taking titleInfo/title alone.
|
|
143
|
+
def title_subjects
|
|
144
|
+
doc.xpath("/mods:mods/mods:subject/mods:titleInfo", NAMESPACE).filter_map do |node|
|
|
145
|
+
parts = title_parts_of(node).transform_values { |value| NEU::MODS.normalize(value.to_s) }
|
|
146
|
+
clean(Projection.compose_title(parts))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Kept structured for the reason #map_data is. Flattening country / state
|
|
151
|
+
# / city into "United States -- New York (State) -- Parksville" would make
|
|
152
|
+
# a consumer that wants the city alone unpick a sentence.
|
|
153
|
+
#
|
|
154
|
+
# This is the axis bdr_43888.mods.xml uses INSTEAD of subject/geographic,
|
|
155
|
+
# so that record projected no place at all -- a live ingest path, not a
|
|
156
|
+
# hypothetical.
|
|
157
|
+
def hierarchical_geographic_subjects
|
|
158
|
+
doc.xpath("/mods:mods/mods:subject/mods:hierarchicalGeographic", NAMESPACE).filter_map do |node|
|
|
159
|
+
entry = HIERARCHICAL_GEOGRAPHIC_LEVELS.to_h { |level| [level, child_text(node, "mods:#{camelize(level)}")] }
|
|
160
|
+
entry if entry.values.any?
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
133
164
|
# --- Names ---------------------------------------------------------------
|
|
134
165
|
|
|
135
166
|
# One name as the access copy wants it. `affiliation` is how a reader
|
|
@@ -208,6 +239,23 @@ module NEU
|
|
|
208
239
|
# back by nothing.
|
|
209
240
|
def publication_information = texts_at("/mods:mods/mods:originInfo/mods:publisher")
|
|
210
241
|
def edition = texts_at("/mods:mods/mods:originInfo/mods:edition")
|
|
242
|
+
def place_of_publication = texts_at("/mods:mods/mods:originInfo/mods:place/mods:placeTerm")
|
|
243
|
+
def issuance = texts_at("/mods:mods/mods:originInfo/mods:issuance")
|
|
244
|
+
|
|
245
|
+
# Serials. The @authority a record puts on a frequency is not projected:
|
|
246
|
+
# authority handling is a question the gem defers everywhere else -- for
|
|
247
|
+
# genre, subject and name -- and answering it for one field would be
|
|
248
|
+
# inconsistent.
|
|
249
|
+
def frequency = texts_at("/mods:mods/mods:originInfo/mods:frequency")
|
|
250
|
+
|
|
251
|
+
def table_of_contents = texts_at("/mods:mods/mods:tableOfContents")
|
|
252
|
+
def reformatting_quality = texts_at("/mods:mods/mods:physicalDescription/mods:reformattingQuality")
|
|
253
|
+
|
|
254
|
+
# An LCC or DDC call number. Note this is NOT the same concept as Atlas's
|
|
255
|
+
# classification_ssim, which carries a FileSet content-type vocabulary --
|
|
256
|
+
# the name collision is accidental and the consumer has to pick a free
|
|
257
|
+
# Solr field.
|
|
258
|
+
def classification = texts_at("/mods:mods/mods:classification")
|
|
211
259
|
|
|
212
260
|
# Every top-level note, keeping its @type. The type carries meaning -- a
|
|
213
261
|
# "statement of responsibility" is not a "funding" note -- so flattening
|
|
@@ -297,6 +345,26 @@ module NEU
|
|
|
297
345
|
# or a full date. Matching the shape explicitly, rather than widening
|
|
298
346
|
# DateTime.parse, is what lets the declared precision fall out of the parse
|
|
299
347
|
# instead of being guessed after it.
|
|
348
|
+
# The eleven children the XSD allows under hierarchicalGeographic, in the
|
|
349
|
+
# order MODS lists them -- broadest first, which is also the order a
|
|
350
|
+
# consumer composing a place string wants to reverse.
|
|
351
|
+
HIERARCHICAL_GEOGRAPHIC_LEVELS = %i[
|
|
352
|
+
continent country province region state territory county city
|
|
353
|
+
city_section island area
|
|
354
|
+
].freeze
|
|
355
|
+
|
|
356
|
+
# recordInfo children. Read as a single value: the schema repeats the
|
|
357
|
+
# element, but a record with two cataloguing provenances is not a case
|
|
358
|
+
# anyone has, and an array here buys nothing.
|
|
359
|
+
RECORD_INFO_PARTS = {
|
|
360
|
+
content_source: "mods:recordContentSource",
|
|
361
|
+
origin: "mods:recordOrigin",
|
|
362
|
+
description_standard: "mods:descriptionStandard",
|
|
363
|
+
creation_date: "mods:recordCreationDate",
|
|
364
|
+
change_date: "mods:recordChangeDate",
|
|
365
|
+
language_of_cataloging: "mods:languageOfCataloging/mods:languageTerm"
|
|
366
|
+
}.freeze
|
|
367
|
+
|
|
300
368
|
W3CDTF_DATE = /\A(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?\z/
|
|
301
369
|
|
|
302
370
|
# What #date_parts returns when the element is absent entirely, so an
|
|
@@ -351,6 +419,19 @@ module NEU
|
|
|
351
419
|
def date_issued_with_precision = [date_issued, date_issued_precision]
|
|
352
420
|
def copyright_date_with_precision = [copyright_date, copyright_date_precision]
|
|
353
421
|
|
|
422
|
+
# Who catalogued this record, to what standard, and when. It describes the
|
|
423
|
+
# CATALOGUING rather than the resource, which is why it is one value and
|
|
424
|
+
# why a consumer is unlikely to want it beside Publisher -- but dropping a
|
|
425
|
+
# preservation repository's provenance statement on read is wrong on its
|
|
426
|
+
# face, so it is projected and the display question is the consumer's.
|
|
427
|
+
def record_info
|
|
428
|
+
node = doc.at_xpath("/mods:mods/mods:recordInfo", NAMESPACE)
|
|
429
|
+
return nil unless node
|
|
430
|
+
|
|
431
|
+
entry = RECORD_INFO_PARTS.transform_values { |xpath| child_text(node, xpath) }
|
|
432
|
+
entry if entry.values.any?
|
|
433
|
+
end
|
|
434
|
+
|
|
354
435
|
# --- Full projection -----------------------------------------------------
|
|
355
436
|
|
|
356
437
|
# The field registry: the single declaration of what this gem projects.
|
|
@@ -379,7 +460,10 @@ module NEU
|
|
|
379
460
|
|
|
380
461
|
# origin
|
|
381
462
|
publication_information: :many,
|
|
463
|
+
place_of_publication: :many,
|
|
382
464
|
edition: :many,
|
|
465
|
+
issuance: :many,
|
|
466
|
+
frequency: :many,
|
|
383
467
|
# Six rows per originInfo date. Flat rather than one nested value,
|
|
384
468
|
# because the value half has consumers that need a real date object.
|
|
385
469
|
date_created: :one,
|
|
@@ -407,7 +491,9 @@ module NEU
|
|
|
407
491
|
format: :many,
|
|
408
492
|
extent: :many,
|
|
409
493
|
digital_origin: :many,
|
|
494
|
+
reformatting_quality: :many,
|
|
410
495
|
notes: :many,
|
|
496
|
+
table_of_contents: :many,
|
|
411
497
|
|
|
412
498
|
# subjects
|
|
413
499
|
topical_subjects: :many,
|
|
@@ -415,6 +501,10 @@ module NEU
|
|
|
415
501
|
temporal_subjects: :many,
|
|
416
502
|
personal_name_subjects: :many,
|
|
417
503
|
corporate_name_subjects: :many,
|
|
504
|
+
genre_subjects: :many,
|
|
505
|
+
geographic_code_subjects: :many,
|
|
506
|
+
title_subjects: :many,
|
|
507
|
+
hierarchical_geographic_subjects: :many,
|
|
418
508
|
map_data: :many,
|
|
419
509
|
|
|
420
510
|
# related items
|
|
@@ -424,7 +514,9 @@ module NEU
|
|
|
424
514
|
|
|
425
515
|
# identifiers and location
|
|
426
516
|
identifiers: :many,
|
|
517
|
+
classification: :many,
|
|
427
518
|
permanent_url: :one,
|
|
519
|
+
record_info: :one,
|
|
428
520
|
location: :many,
|
|
429
521
|
|
|
430
522
|
# access
|
|
@@ -575,6 +667,13 @@ module NEU
|
|
|
575
667
|
# [nil], and every consumer of that array has to guard for it.
|
|
576
668
|
# #texts_at scoped to a node rather than the document, for a repeatable
|
|
577
669
|
# child of one element.
|
|
670
|
+
# :city_section -> "citySection". The level names are snake_case in the
|
|
671
|
+
# projection and camelCase in the schema.
|
|
672
|
+
def camelize(level)
|
|
673
|
+
head, *rest = level.to_s.split("_")
|
|
674
|
+
[head, *rest.map(&:capitalize)].join
|
|
675
|
+
end
|
|
676
|
+
|
|
578
677
|
def texts_under(node, xpath)
|
|
579
678
|
node.xpath(xpath, NAMESPACE).filter_map { |child| clean(child.text) }
|
|
580
679
|
end
|