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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c0467914a611aa9d46065edcd2c0a18cd0decd642e769b70ecfaecc18e7ef1e
4
- data.tar.gz: f97ba11b67874fefcb98f9de2af47eda370bb61f679e9b10994cf465b31876f2
3
+ metadata.gz: 5416d755319f464e252ef4fa07d0372efb11e1379d76ad6155b78f42a61c221a
4
+ data.tar.gz: ddfcf9eb792155c216bf9710ef6cfcc379f914791a782da30577158bb02a1da7
5
5
  SHA512:
6
- metadata.gz: 94029ec3b5d70d835677c4ad49cfc02bf7a501c782a91adb38c3a5cf6310ee727ffcef7736975526b699c796ff574ab3be5645ecaa7d576494b9b28c88c67ac1
7
- data.tar.gz: 3b5c3b2f36af51e6af7424ff5ecdbf365a6c56358bf3aec3cff370b2473ad334cfaed9c20eae4aa2025ab583eb9d0ec9d7439d94fb7d8687e80dd301a64c7c38
6
+ metadata.gz: d465b12175d8b15f7da7d7c355af0246294b3e7df0c276be775b6ccf41c14840f716fa7624ed86a54bff69da56625041cf7ca1f245e9ff6bb8f4035b44f70799
7
+ data.tar.gz: ce373c9bb16d1a7e6142a4e0556e669ddb07d0bbe77db172f7954d4d54eb298b4a688c5cf3a60bd5237611670bbfb162c67d7d9c1bd5377a6579d0c78b9a9030
data/.version CHANGED
@@ -1 +1 @@
1
- 0.8.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.
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neu-mods
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff