cocina_display 0.1.0 → 0.3.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/README.md +14 -18
- data/lib/cocina_display/cocina_record.rb +43 -2
- data/lib/cocina_display/concerns/events.rb +137 -0
- data/lib/cocina_display/dates/date.rb +688 -0
- data/lib/cocina_display/dates/date_range.rb +122 -0
- data/lib/cocina_display/imprint.rb +139 -0
- data/lib/cocina_display/marc_country_codes.rb +394 -0
- data/lib/cocina_display/title_builder.rb +374 -0
- data/lib/cocina_display/version.rb +1 -1
- metadata +68 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ef43e2d573c99c10d6db87974ff32091ff7e7d312bf2e812ef1043f513f291c
|
4
|
+
data.tar.gz: f04b35117201aaebff5e8d041d2776525dcd18a46bc408baf9d8649376d90618
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c36dd129d129ce44c7c516b670295b59f68732d0270664dda63ce119fe37ec60e68c6fd615e37ada5ecef06777db7be30cd708d0f0c461c18c97085e98eb553f
|
7
|
+
data.tar.gz: cddd56237670d908a6be7951e24fe62a0fbd3076fd0eff96693c7debc03fa9ab8e01157a330923938143ec2067a4d97d4ead339f44fd2b00d0bcf99ea92b6af0
|
data/README.md
CHANGED
@@ -28,31 +28,26 @@ To start, you need some Cocina in JSON form.
|
|
28
28
|
|
29
29
|
You can download some directly from PURL by visiting an object's PURL URL and appending `.json` to the end, like `https://purl.stanford.edu/bb112zx3193.json`. Some examples are available in the `spec/fixtures` directory.
|
30
30
|
|
31
|
-
|
31
|
+
There is also a helper method to fetch the Cocina JSON for a given DRUID and immediately parse it into a `CocinaRecord` object:
|
32
32
|
|
33
33
|
```ruby
|
34
|
-
|
35
|
-
|
34
|
+
> record = CocinaDisplay::CocinaRecord.fetch('bb112zx3193')
|
35
|
+
=> #<CocinaDisplay::CocinaRecord:0x00007f8c8c0b5c80
|
36
36
|
```
|
37
37
|
|
38
38
|
### Working with objects
|
39
39
|
|
40
|
-
|
40
|
+
The `CocinaRecord` class provides some methods to access common fields, as well as an underlying hash representation parsed from the JSON.
|
41
41
|
|
42
42
|
```ruby
|
43
|
-
>
|
44
|
-
=>
|
45
|
-
> record = CocinaDisplay::CocinaRecord.new(cocina_json)
|
46
|
-
=>
|
47
|
-
#<CocinaDisplay::CocinaRecord:0x000000012d11b600
|
48
|
-
...
|
49
|
-
> record.titles
|
50
|
-
=> ["Bugatti Type 51A. Road & Track Salon January 1957"]
|
43
|
+
> record.title
|
44
|
+
=> "Bugatti Type 51A. Road & Track Salon January 1957"
|
51
45
|
> record.content_type
|
52
46
|
=> "image"
|
53
47
|
> record.iiif_manifest_url
|
54
48
|
=> "https://purl.stanford.edu/bb112zx3193/iiif3/manifest"
|
55
|
-
|
49
|
+
# access the hash representation
|
50
|
+
> record.cocina_doc.dig("description", "contributor", 0, "name", 0, "value")
|
56
51
|
=> "Hearst Magazines, Inc."
|
57
52
|
```
|
58
53
|
|
@@ -65,14 +60,15 @@ Fetching data deeply nested in the record, especially when you need to filter ba
|
|
65
60
|
The previous example used `Hash#dig` to access the first contributor's first name value. Using `#path`, you can query for _all_ contributor name values, or even filter to particular contributors:
|
66
61
|
|
67
62
|
```ruby
|
68
|
-
|
63
|
+
# name values for all contributors in description
|
64
|
+
> record.path('$.description.contributor[*].name[*].value').search
|
69
65
|
=> ["Hearst Magazines, Inc.", "Chesebrough, Jerry"]
|
70
|
-
|
66
|
+
# only contributors with a role with value "photographer"
|
67
|
+
> record.path("$.description.contributor[?@.role[?@.value == 'photographer']].name[*].value").search
|
71
68
|
=> ["Chesebrough, Jerry"]
|
72
|
-
>
|
73
69
|
```
|
74
70
|
|
75
|
-
The JsonPath implementation used is [
|
71
|
+
The JsonPath implementation used is [janeway](https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md), which supports the full syntax from the [finalized 2024 version of the specification](https://www.rfc-editor.org/rfc/rfc9535.html). Results returned from `#path` are Enumerators.
|
76
72
|
|
77
73
|
In the following example, we start an expression with `"$.."` to search for contributor nodes at _any_ level (e.g. `event.contributors`) and discover that there is a third contributor, but it has no `name` value. Using the `['code', 'value']` syntax, we can retrieve both `code` and `value` and show where they came from:
|
78
74
|
|
@@ -103,7 +99,7 @@ Documentation is generated using [yard](https://yardoc.org). You can generate it
|
|
103
99
|
|
104
100
|
## Background
|
105
101
|
|
106
|
-
Historically, applications at SUL used a combination of several gems to render objects represented by MODS XML. With the transition to the Cocina data model, infrastructure applications adopted the [
|
102
|
+
Historically, applications at SUL used a combination of several gems to render objects represented by MODS XML. With the transition to the Cocina data model, infrastructure applications adopted the [cocina-models gem](https://github.com/sul-dlss/cocina-models), which provides accessor objects and validators over Cocina JSON. Internal applications can fetch such objects over HTTP using [dor-services-client](https://github.com/sul-dlss/dor-services-client).
|
107
103
|
|
108
104
|
On the access side, Cocina JSON (the "public Cocina") is available statically via [PURL](https://purl.stanford.edu), but is only updated when an object is published ("shelved") from SDR. This frequently results in data that is technically invalid with respect to `cocina-models` but is still valid in the context of a patron-facing application.
|
109
105
|
|
@@ -1,11 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "cocina/models"
|
4
3
|
require "janeway"
|
4
|
+
require "json"
|
5
|
+
require "net/http"
|
6
|
+
require "active_support"
|
7
|
+
require "active_support/core_ext/object/blank"
|
8
|
+
require "active_support/core_ext/hash/conversions"
|
9
|
+
|
10
|
+
require_relative "title_builder"
|
11
|
+
require_relative "concerns/events"
|
5
12
|
|
6
13
|
module CocinaDisplay
|
7
|
-
# Public Cocina metadata for an SDR object
|
14
|
+
# Public Cocina metadata for an SDR object, as fetched from PURL.
|
8
15
|
class CocinaRecord
|
16
|
+
include CocinaDisplay::Concerns::Events
|
17
|
+
|
18
|
+
# Fetch a public Cocina document from PURL and create a CocinaRecord.
|
19
|
+
# @note This is intended to be used in development or testing only.
|
20
|
+
# @param druid [String] The bare DRUID of the object to fetch.
|
21
|
+
# @return [CocinaDisplay::CocinaRecord]
|
22
|
+
# :nocov:
|
23
|
+
def self.fetch(druid)
|
24
|
+
new(Net::HTTP.get(URI("https://purl.stanford.edu/#{druid}.json")))
|
25
|
+
end
|
26
|
+
# :nocov:
|
27
|
+
|
9
28
|
# The parsed Cocina document.
|
10
29
|
# @return [Hash]
|
11
30
|
attr_reader :cocina_doc
|
@@ -109,6 +128,28 @@ module CocinaDisplay
|
|
109
128
|
content_type == "collection"
|
110
129
|
end
|
111
130
|
|
131
|
+
# The main title for the object.
|
132
|
+
# @note If you need more formatting control, consider using {CocinaDisplay::TitleBuilder} directly.
|
133
|
+
# @return [String]
|
134
|
+
# @example
|
135
|
+
# record.title #=> "Bugatti Type 51A. Road & Track Salon January 1957"
|
136
|
+
def title
|
137
|
+
CocinaDisplay::TitleBuilder.build(
|
138
|
+
cocina_doc.dig("description", "title"),
|
139
|
+
catalog_links: cocina_doc.dig("identification", "catalogLinks")
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Alternative or translated titles for the object. Does not include the main title.
|
144
|
+
# @return [Array<String>]
|
145
|
+
# @example
|
146
|
+
# record.additional_titles #=> ["Alternate title 1", "Alternate title 2"]
|
147
|
+
def additional_titles
|
148
|
+
CocinaDisplay::TitleBuilder.additional_titles(
|
149
|
+
cocina_doc.dig("description", "title")
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
112
153
|
# Traverse nested FileSets and return an enumerator over their files.
|
113
154
|
# Each file is a +Hash+.
|
114
155
|
# @return [Enumerator] Enumerator over file hashes
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require_relative "../dates/date"
|
2
|
+
require_relative "../dates/date_range"
|
3
|
+
require_relative "../imprint"
|
4
|
+
|
5
|
+
module CocinaDisplay
|
6
|
+
module Concerns
|
7
|
+
module Events
|
8
|
+
# The earliest preferred publication date as a Date object.
|
9
|
+
# If the date was a range or interval, uses the start (or end if no start).
|
10
|
+
# Considers publication, creation, and capture dates in that order.
|
11
|
+
# Prefers dates marked as primary and those with a declared encoding.
|
12
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
13
|
+
# @return [Date, nil]
|
14
|
+
# @see https://github.com/inukshuk/edtf-ruby
|
15
|
+
def pub_date_edtf(ignore_qualified: false)
|
16
|
+
date = pub_date(ignore_qualified: ignore_qualified)
|
17
|
+
return unless date
|
18
|
+
|
19
|
+
if date.is_a? CocinaDisplay::Dates::DateRange
|
20
|
+
date = date.start || date.stop
|
21
|
+
end
|
22
|
+
|
23
|
+
edtf_date = date.date
|
24
|
+
return unless edtf_date
|
25
|
+
|
26
|
+
if edtf_date.is_a? EDTF::Interval
|
27
|
+
edtf_date.from
|
28
|
+
else
|
29
|
+
edtf_date
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The earliest preferred publication year as an integer.
|
34
|
+
# If the date was a range or interval, uses the start (or end if no start).
|
35
|
+
# Considers publication, creation, and capture dates in that order.
|
36
|
+
# Prefers dates marked as primary and those with a declared encoding.
|
37
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
38
|
+
# @return [Integer, nil]
|
39
|
+
# @note 6 BCE will return -5; 4 CE will return 4.
|
40
|
+
def pub_year_int(ignore_qualified: false)
|
41
|
+
pub_date_edtf(ignore_qualified: ignore_qualified)&.year
|
42
|
+
end
|
43
|
+
|
44
|
+
# String for displaying the earliest preferred publication year or range.
|
45
|
+
# Considers publication, creation, and capture dates in that order.
|
46
|
+
# Prefers dates marked as primary and those with a declared encoding.
|
47
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
48
|
+
# @return [String, nil]
|
49
|
+
# @example Year range
|
50
|
+
# CocinaRecord.fetch('bb099mt5053').pub_year_display_str #=> "1932 - 2012"
|
51
|
+
def pub_year_display_str(ignore_qualified: false)
|
52
|
+
date = pub_date(ignore_qualified: ignore_qualified)
|
53
|
+
return unless date
|
54
|
+
|
55
|
+
date.decoded_value(allowed_precisions: [:year, :decade, :century])
|
56
|
+
end
|
57
|
+
|
58
|
+
# String for displaying the imprint statement(s).
|
59
|
+
# @return [String, nil]
|
60
|
+
# @see CocinaDisplay::Imprint#display_str
|
61
|
+
# @example
|
62
|
+
# CocinaRecord.fetch('bt553vr2845').imprint_display_str #=> "New York : Meridian Book, 1993, c1967"
|
63
|
+
def imprint_display_str
|
64
|
+
imprints.map(&:display_str).compact_blank.join("; ")
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Event dates as an array of CocinaDisplay::Dates::Date objects.
|
70
|
+
# If type is provided, keep dates with a matching event type OR date type.
|
71
|
+
# @param type [Symbol, nil] Filter by event type (e.g. :publication).
|
72
|
+
# @return [Array<CocinaDisplay::Dates::Date>] The list of event dates
|
73
|
+
def event_dates(type: nil)
|
74
|
+
filter_expr = type.present? ? "?match(@.type, \"#{type}\")" : "*"
|
75
|
+
|
76
|
+
Enumerator::Chain.new(
|
77
|
+
path("$.description.event[*].date[#{filter_expr}]"),
|
78
|
+
path("$.description.event[#{filter_expr}].date[*]")
|
79
|
+
).uniq.map do |date|
|
80
|
+
CocinaDisplay::Dates::Date.from_cocina(date)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Array of CocinaDisplay::Imprint objects for all relevant Cocina events.
|
85
|
+
# Considers publication, creation, capture, and copyright events.
|
86
|
+
# Considers event types as well as date types if the event is untyped.
|
87
|
+
# Prefers events where the date was not encoded, if any.
|
88
|
+
# @return [Array<CocinaDisplay::Imprint>] The list of Imprint objects
|
89
|
+
def imprints
|
90
|
+
filter_expr = "\"(publication|creation|capture|copyright)\""
|
91
|
+
|
92
|
+
imprints = Enumerator::Chain.new(
|
93
|
+
path("$.description.event[?match(@.type, #{filter_expr})]"),
|
94
|
+
path("$.description.event[?@.date[?match(@.type, #{filter_expr})]]")
|
95
|
+
).uniq.map do |event|
|
96
|
+
CocinaDisplay::Imprint.new(event)
|
97
|
+
end
|
98
|
+
|
99
|
+
imprints.reject(&:date_encoding?).presence || imprints
|
100
|
+
end
|
101
|
+
|
102
|
+
# The earliest preferred publication date as a CocinaDisplay::Dates::Date object.
|
103
|
+
# Considers publication, creation, and capture dates in that order.
|
104
|
+
# Prefers dates marked as primary and those with a declared encoding.
|
105
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
106
|
+
# @return [CocinaDisplay::Dates::Date] The earliest preferred date
|
107
|
+
# @return [nil] if no dates are left after filtering
|
108
|
+
def pub_date(ignore_qualified: false)
|
109
|
+
[:publication, :creation, :capture].map do |type|
|
110
|
+
earliest_preferred_date(event_dates(type: type), ignore_qualified: ignore_qualified)
|
111
|
+
end.compact.first
|
112
|
+
end
|
113
|
+
|
114
|
+
# Choose the earliest, best date from a provided list of event dates.
|
115
|
+
# Rules to consider:
|
116
|
+
# 1. Reject any dates that were not parsed.
|
117
|
+
# 2. If `ignore_qualified` is true, reject any qualified dates.
|
118
|
+
# 3. If there are any primary dates, prefer those dates.
|
119
|
+
# 4. If there are any encoded dates, prefer those dates.
|
120
|
+
# 5. From whatever is left, choose the earliest date.
|
121
|
+
# @param dates [Array<CocinaDisplay::Dates::Date>] The list of dates
|
122
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
123
|
+
# @return [CocinaDisplay::Dates::Date] The earliest preferred date
|
124
|
+
# @return [nil] if no dates are left after filtering
|
125
|
+
def earliest_preferred_date(dates, ignore_qualified: false)
|
126
|
+
return nil if dates.empty?
|
127
|
+
|
128
|
+
dates.filter!(&:parsed_date?)
|
129
|
+
dates.reject!(&:approximate?) if ignore_qualified
|
130
|
+
dates = dates.filter(&:primary?).presence || dates
|
131
|
+
dates = dates.filter(&:encoding?).presence || dates
|
132
|
+
|
133
|
+
dates.min
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|