cocina_display 0.5.0 → 0.7.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 +21 -16
- data/lib/cocina_display/cocina_record.rb +24 -7
- data/lib/cocina_display/concerns/contributors.rb +64 -41
- data/lib/cocina_display/concerns/events.rb +37 -25
- data/lib/cocina_display/concerns/forms.rb +134 -0
- data/lib/cocina_display/concerns/languages.rb +20 -0
- data/lib/cocina_display/concerns/subjects.rb +63 -16
- data/lib/cocina_display/contributor.rb +57 -8
- data/lib/cocina_display/dates/date.rb +9 -8
- data/lib/cocina_display/dates/date_range.rb +29 -9
- data/lib/cocina_display/events/event.rb +78 -0
- data/lib/cocina_display/events/imprint.rb +100 -0
- data/lib/cocina_display/events/location.rb +56 -0
- data/lib/cocina_display/language.rb +47 -0
- data/lib/cocina_display/subjects/subject.rb +63 -0
- data/lib/cocina_display/subjects/subject_value.rb +104 -0
- data/lib/cocina_display/title_builder.rb +2 -1
- data/lib/cocina_display/utils.rb +30 -5
- data/lib/cocina_display/version.rb +1 -1
- data/lib/cocina_display/vocabularies/marc_country_codes.rb +393 -0
- data/lib/cocina_display/vocabularies/marc_relator_codes.rb +318 -0
- data/lib/cocina_display/vocabularies/searchworks_languages.rb +526 -0
- data/script/find_records.rb +85 -0
- metadata +42 -5
- data/lib/cocina_display/imprint.rb +0 -123
- data/lib/cocina_display/marc_country_codes.rb +0 -394
- data/lib/cocina_display/subject.rb +0 -127
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 575cbfb8b11f3cf71950bfb5234ada4a916a5e55d21116422de84361854f0713
|
4
|
+
data.tar.gz: 4e06ee33d664822bdb6a3057ad8c40539f1f8363350f4648a463c7be0cb533ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76ecb30d8a60bbb42bab880b0c3c5288d8daa7504cc044fb06c157e1b5e2fe21fd3e56668c3479d291f1e683eab4d719c11b2ac5d185b6e98a062e917f5ea74a
|
7
|
+
data.tar.gz: ce4c61a927f9bc034aeebcd9cfc0cb56c8cd1b3c4ee13f7d8d0fcd0cc83290e8d2d1fe31ead879bdb4b1d0090ee60164158ba24d9c8eeb4a7499dfd315b9cf07
|
data/README.md
CHANGED
@@ -24,11 +24,11 @@ gem install cocina_display
|
|
24
24
|
|
25
25
|
### Obtaining Cocina
|
26
26
|
|
27
|
-
To start, you need some Cocina in JSON form.
|
27
|
+
To start, you need some Cocina in JSON form. Consumers of this gem are likely to be applications that are already harvesting this data (for indexing) or have it stored in an index or database (for display). In testing, you may have neither of these.
|
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
|
-
There is also a helper method to fetch the Cocina JSON for a given DRUID and immediately parse it into a `CocinaRecord` object:
|
31
|
+
There is also a helper method to fetch the Cocina JSON for a given DRUID over HTTP and immediately parse it into a `CocinaRecord` object:
|
32
32
|
|
33
33
|
```ruby
|
34
34
|
> record = CocinaDisplay::CocinaRecord.fetch('bb112zx3193')
|
@@ -51,7 +51,7 @@ The `CocinaRecord` class provides some methods to access common fields, as well
|
|
51
51
|
=> "Hearst Magazines, Inc."
|
52
52
|
```
|
53
53
|
|
54
|
-
See the [API Documentation](https://sul-dlss.github.io/cocina_display/CocinaDisplay/CocinaRecord.html) for more details on the methods available in the `CocinaRecord` class.
|
54
|
+
See the [API Documentation](https://sul-dlss.github.io/cocina_display/CocinaDisplay/CocinaRecord.html) for more details on the methods available in the `CocinaRecord` class. The gem provides a large number of methods, organized into concerns, to access different parts of the data.
|
55
55
|
|
56
56
|
### Fetching nested data
|
57
57
|
|
@@ -61,47 +61,52 @@ The previous example used `Hash#dig` to access the first contributor's first nam
|
|
61
61
|
|
62
62
|
```ruby
|
63
63
|
# name values for all contributors in description
|
64
|
-
> record.path('$.description.contributor
|
64
|
+
> record.path('$.description.contributor.*.name.*.value').search
|
65
65
|
=> ["Hearst Magazines, Inc.", "Chesebrough, Jerry"]
|
66
66
|
# only contributors with a role with value "photographer"
|
67
|
-
> record.path("$.description.contributor[?@.role[?@.value == 'photographer']].name
|
67
|
+
> record.path("$.description.contributor[?@.role[?@.value == 'photographer']].name.*.value").search
|
68
68
|
=> ["Chesebrough, Jerry"]
|
69
69
|
```
|
70
70
|
|
71
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.
|
72
72
|
|
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
|
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 the path they came from:
|
74
74
|
|
75
75
|
```ruby
|
76
|
-
> record.path("$..contributor
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
=> ["Hearst Magazines, Inc.", "Chesebrough, Jerry", "CSt"]
|
76
|
+
> record.path("$..contributor.*.name[*]['code', 'value']").map { |value, _node, key, path| [key, value, path] }
|
77
|
+
[["value", "Hearst Magazines, Inc.", "$['description']['contributor'][0]['name'][0]['value']"],
|
78
|
+
["value", "Chesebrough, Jerry", "$['description']['contributor'][1]['name'][0]['value']"],
|
79
|
+
["code", "CSt", "$['description']['adminMetadata']['contributor'][0]['name'][0]['code']"]]
|
81
80
|
```
|
82
81
|
|
83
82
|
There is also a command line utility for quickly querying a JSON file using JsonPath. Online syntax checkers may give different results, so it helps to test locally. You can run it with:
|
84
83
|
|
85
84
|
```bash
|
86
|
-
cat spec/fixtures/bb112zx3193.json | janeway "$.description.contributor[?@.role[?@.value == 'photographer']].name
|
85
|
+
cat spec/fixtures/bb112zx3193.json | janeway "$.description.contributor[?@.role[?@.value == 'photographer']].name.*.value"
|
87
86
|
[
|
88
87
|
"Chesebrough, Jerry"
|
89
88
|
]
|
90
89
|
```
|
91
90
|
|
91
|
+
### Searching for records
|
92
|
+
|
93
|
+
Sometimes you need to determine if records exist "in the wild" that exhibit particular characteristics in the Cocina metadata, like the presence or absence of a field, or a specific value in a field. There is a template script in the `scripts/` directory that can be used to crawl all DRUIDs released to a particular target, like Searchworks, and examine each record.
|
94
|
+
|
92
95
|
## Development
|
93
96
|
|
94
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
97
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. This is a useful place to try out JSONPath expressions with `CocinaRecord#path`.
|
98
|
+
|
99
|
+
Tests are written using [rspec](https://rspec.info), with coverage automatically measured via [simplecov](https://github.com/simplecov-ruby/simplecov). CI will fail if coverage drops below 100%. For convenience, if you invoke a single spec file locally, coverage will not be reported.
|
95
100
|
|
96
|
-
|
101
|
+
Documentation is generated using [yard](https://yardoc.org). You can generate it locally by running `yardoc`, or `yard server --reload` to start a local server and watch for changes as you edit. There is a GitHub action that automatically generates and publishes the documentation to GitHub Pages on every push/merge to `main`.
|
97
102
|
|
98
|
-
|
103
|
+
To release a new version, update the version number in `version.rb`, run `bundle` to update `Gemfile.lock`, commit your changes, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
99
104
|
|
100
105
|
## Background
|
101
106
|
|
102
107
|
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).
|
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
|
+
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` (i.e. it does not match the latest spec) but is still valid in the context of a patron-facing application (because it can still be rendered into useful information).
|
105
110
|
|
106
111
|
Cocina data can also be complex, representing the same underlying information in different ways. A "complete" implementation can involve checking multiple deeply nested paths to ensure no information is missed. Rather than tightly coupling access applications to `cocina-models`, this gem provides a set of helpers designed to safely parse Cocina JSON and render it in a consistent way across applications.
|
107
112
|
|
@@ -13,6 +13,9 @@ require_relative "concerns/identifiers"
|
|
13
13
|
require_relative "concerns/titles"
|
14
14
|
require_relative "concerns/access"
|
15
15
|
require_relative "concerns/subjects"
|
16
|
+
require_relative "concerns/forms"
|
17
|
+
require_relative "concerns/languages"
|
18
|
+
require_relative "utils"
|
16
19
|
|
17
20
|
module CocinaDisplay
|
18
21
|
# Public Cocina metadata for an SDR object, as fetched from PURL.
|
@@ -23,23 +26,37 @@ module CocinaDisplay
|
|
23
26
|
include CocinaDisplay::Concerns::Titles
|
24
27
|
include CocinaDisplay::Concerns::Access
|
25
28
|
include CocinaDisplay::Concerns::Subjects
|
29
|
+
include CocinaDisplay::Concerns::Forms
|
30
|
+
include CocinaDisplay::Concerns::Languages
|
26
31
|
|
27
32
|
# Fetch a public Cocina document from PURL and create a CocinaRecord.
|
28
33
|
# @note This is intended to be used in development or testing only.
|
29
34
|
# @param druid [String] The bare DRUID of the object to fetch.
|
35
|
+
# @param deep_compact [Boolean] If true, compact the JSON to remove blank values.
|
30
36
|
# @return [CocinaDisplay::CocinaRecord]
|
31
37
|
# :nocov:
|
32
|
-
def self.fetch(druid)
|
33
|
-
|
38
|
+
def self.fetch(druid, deep_compact: false)
|
39
|
+
from_json(Net::HTTP.get(URI("https://purl.stanford.edu/#{druid}.json")), deep_compact: deep_compact)
|
34
40
|
end
|
35
41
|
# :nocov:
|
36
42
|
|
43
|
+
# Create a CocinaRecord from a JSON string.
|
44
|
+
# @param cocina_json [String]
|
45
|
+
# @param deep_compact [Boolean] If true, compact the JSON to remove blank values.
|
46
|
+
# @return [CocinaDisplay::CocinaRecord]
|
47
|
+
def self.from_json(cocina_json, deep_compact: false)
|
48
|
+
cocina_doc = JSON.parse(cocina_json)
|
49
|
+
deep_compact ? new(Utils.deep_compact_blank(cocina_doc)) : new(cocina_doc)
|
50
|
+
end
|
51
|
+
|
37
52
|
# The parsed Cocina document.
|
38
53
|
# @return [Hash]
|
39
54
|
attr_reader :cocina_doc
|
40
55
|
|
41
|
-
|
42
|
-
|
56
|
+
# Initialize a CocinaRecord with a Cocina document hash.
|
57
|
+
# @param cocina_doc [Hash]
|
58
|
+
def initialize(cocina_doc)
|
59
|
+
@cocina_doc = cocina_doc
|
43
60
|
end
|
44
61
|
|
45
62
|
# Evaluate a JSONPath expression against the Cocina document.
|
@@ -47,9 +64,9 @@ module CocinaDisplay
|
|
47
64
|
# @param path_expression [String] The JSONPath expression to evaluate.
|
48
65
|
# @see https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md
|
49
66
|
# @example Name values for contributors
|
50
|
-
# record.path("$.description.contributor
|
67
|
+
# record.path("$.description.contributor.*.name.*.value").search #=> ["Smith, John", "ACME Corp."]
|
51
68
|
# @example Filtering nodes using a condition
|
52
|
-
# record.path("$.description.contributor[?(@.type == 'person')].name
|
69
|
+
# record.path("$.description.contributor[?(@.type == 'person')].name.*.value").search #=> ["Smith, John"]
|
53
70
|
def path(path_expression)
|
54
71
|
Janeway.enum_for(path_expression, cocina_doc)
|
55
72
|
end
|
@@ -99,7 +116,7 @@ module CocinaDisplay
|
|
99
116
|
# puts file["size"] #=> 123456
|
100
117
|
# end
|
101
118
|
def files
|
102
|
-
path("$.structural.contains
|
119
|
+
path("$.structural.contains.*.structural.contains[*]")
|
103
120
|
end
|
104
121
|
end
|
105
122
|
end
|
@@ -4,90 +4,113 @@ module CocinaDisplay
|
|
4
4
|
module Concerns
|
5
5
|
# Methods for finding and formatting names for contributors
|
6
6
|
module Contributors
|
7
|
-
# The main
|
7
|
+
# The main contributor's name, formatted for display.
|
8
8
|
# @param with_date [Boolean] Include life dates, if present
|
9
9
|
# @return [String]
|
10
|
-
# @return [nil] if no main
|
10
|
+
# @return [nil] if no main contributor is found
|
11
11
|
# @example
|
12
|
-
# record.
|
12
|
+
# record.main_contributor_name #=> "Smith, John"
|
13
13
|
# @example with date
|
14
|
-
# record.
|
15
|
-
def
|
16
|
-
|
14
|
+
# record.main_contributor_name(with_date: true) #=> "Smith, John, 1970-2020"
|
15
|
+
def main_contributor_name(with_date: false)
|
16
|
+
main_contributor&.display_name(with_date: with_date)
|
17
17
|
end
|
18
18
|
|
19
|
-
# All
|
19
|
+
# All contributor names except the main one, formatted for display.
|
20
20
|
# @param with_date [Boolean] Include life dates, if present
|
21
21
|
# @return [Array<String>]
|
22
|
-
def
|
23
|
-
|
22
|
+
def additional_contributor_names(with_date: false)
|
23
|
+
additional_contributors.map { |c| c.display_name(with_date: with_date) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# All names of publishers, formatted for display.
|
27
|
+
# @return [Array<String>]
|
28
|
+
def publisher_names
|
29
|
+
publisher_contributors.map(&:display_name)
|
24
30
|
end
|
25
31
|
|
26
32
|
# All names of authors who are people, formatted for display.
|
27
33
|
# @param with_date [Boolean] Include life dates, if present
|
28
34
|
# @return [Array<String>]
|
29
|
-
def
|
30
|
-
|
35
|
+
def person_contributor_names(with_date: false)
|
36
|
+
contributors.filter(&:person?).map { |c| c.display_name(with_date: with_date) }
|
31
37
|
end
|
32
38
|
|
33
|
-
# All names of non-person
|
39
|
+
# All names of non-person contributors, formatted for display.
|
34
40
|
# This includes organizations, conferences, families, etc.
|
35
41
|
# @return [Array<String>]
|
36
42
|
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-types
|
37
|
-
def
|
38
|
-
|
43
|
+
def impersonal_contributor_names
|
44
|
+
contributors.reject(&:person?).map(&:display_name)
|
39
45
|
end
|
40
46
|
|
41
|
-
# All names of
|
47
|
+
# All names of contributors that are organizations, formatted for display.
|
42
48
|
# @return [Array<String>]
|
43
|
-
def
|
44
|
-
|
49
|
+
def organization_contributor_names
|
50
|
+
contributors.filter(&:organization?).map(&:display_name)
|
45
51
|
end
|
46
52
|
|
47
|
-
# All names of
|
53
|
+
# All names of contributors that are conferences, formatted for display.
|
48
54
|
# @return [Array<String>]
|
49
|
-
def
|
50
|
-
|
55
|
+
def conference_contributor_names
|
56
|
+
contributors.filter(&:conference?).map(&:display_name)
|
51
57
|
end
|
52
58
|
|
53
|
-
# A
|
59
|
+
# A hash mapping role names to the names of contributors with that role.
|
60
|
+
# @param with_date [Boolean] Include life dates, if present
|
61
|
+
# @return [Hash<String, Array<String>>]
|
62
|
+
def contributor_names_by_role(with_date: false)
|
63
|
+
contributors.each_with_object({}) do |contributor, hash|
|
64
|
+
contributor.roles.each do |role|
|
65
|
+
hash[role.display_str] ||= []
|
66
|
+
hash[role.display_str] << contributor.display_name(with_date: with_date)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# A string value for sorting by contributor that sorts missing values last.
|
72
|
+
# Appends the sort title to break ties between contributor names.
|
54
73
|
# Ignores punctuation and leading/trailing spaces.
|
55
74
|
# @return [String]
|
56
|
-
def
|
57
|
-
|
75
|
+
def sort_contributor_name
|
76
|
+
sort_name = main_contributor&.display_name || "\u{10FFFF}"
|
77
|
+
sort_name_title = [sort_name, sort_title].join(" ")
|
78
|
+
sort_name_title.gsub(/[[:punct:]]*/, "").strip
|
58
79
|
end
|
59
80
|
|
60
|
-
private
|
61
|
-
|
62
81
|
# All contributors for the object, including authors, editors, etc.
|
82
|
+
# Checks both description.contributor and description.event.contributor.
|
63
83
|
# @return [Array<Contributor>]
|
64
84
|
def contributors
|
65
|
-
@contributors ||=
|
85
|
+
@contributors ||= Enumerator::Chain.new(
|
86
|
+
path("$.description.contributor.*"),
|
87
|
+
path("$.description.event.*.contributor.*")
|
88
|
+
).map { |c| Contributor.new(c) }
|
66
89
|
end
|
67
90
|
|
68
|
-
# All contributors with a "
|
91
|
+
# All contributors with a "publisher" role.
|
69
92
|
# @return [Array<Contributor>]
|
70
|
-
# @see Contributor#
|
71
|
-
def
|
72
|
-
contributors.filter(&:
|
93
|
+
# @see Contributor#publisher?
|
94
|
+
def publisher_contributors
|
95
|
+
contributors.filter(&:publisher?)
|
73
96
|
end
|
74
97
|
|
75
|
-
#
|
98
|
+
# Object representing the main contributor.
|
76
99
|
# Selected according to the following rules:
|
77
|
-
# 1. If there
|
78
|
-
# 2. If there are no primary
|
79
|
-
# 3. If there are
|
100
|
+
# 1. If there are contributors marked as primary, use the first one.
|
101
|
+
# 2. If there are no primary contributors, use the first contributor with no role.
|
102
|
+
# 3. If there are no contributors without a role, use the first contributor.
|
80
103
|
# @return [Contributor]
|
81
|
-
# @return [nil] if no
|
82
|
-
def
|
83
|
-
|
104
|
+
# @return [nil] if there are no contributors at all
|
105
|
+
def main_contributor
|
106
|
+
contributors.find(&:primary?).presence || contributors.find { |c| !c.role? }.presence || contributors.first
|
84
107
|
end
|
85
108
|
|
86
|
-
# All
|
109
|
+
# All contributors except the main one.
|
87
110
|
# @return [Array<Contributor>]
|
88
|
-
def
|
89
|
-
return [] if
|
90
|
-
|
111
|
+
def additional_contributors
|
112
|
+
return [] if contributors.empty? || contributors.one?
|
113
|
+
contributors - [main_contributor]
|
91
114
|
end
|
92
115
|
end
|
93
116
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative "../dates/date"
|
2
2
|
require_relative "../dates/date_range"
|
3
|
-
require_relative "../
|
3
|
+
require_relative "../events/event"
|
4
|
+
require_relative "../events/imprint"
|
4
5
|
|
5
6
|
module CocinaDisplay
|
6
7
|
module Concerns
|
@@ -75,24 +76,28 @@ module CocinaDisplay
|
|
75
76
|
# @example
|
76
77
|
# CocinaRecord.fetch('bt553vr2845').imprint_display_str #=> "New York : Meridian Book, 1993, c1967"
|
77
78
|
def imprint_display_str
|
78
|
-
|
79
|
+
imprint_events.map(&:display_str).compact_blank.join("; ")
|
79
80
|
end
|
80
81
|
|
81
|
-
|
82
|
+
# List of places of publication as strings.
|
83
|
+
# Considers locations for all publication, creation, and capture events.
|
84
|
+
# @return [Array<String>]
|
85
|
+
def publication_places
|
86
|
+
publication_events.flat_map { |event| event.locations.map(&:display_str) }
|
87
|
+
end
|
82
88
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
filter_expr = type.present? ? "?match(@.type, \"#{type}\")" : "*"
|
89
|
+
# All events associated with the object.
|
90
|
+
# @return [Array<CocinaDisplay::Events::Event>]
|
91
|
+
def events
|
92
|
+
@events ||= path("$.description.event.*").map { |event| CocinaDisplay::Events::Event.new(event) }
|
93
|
+
end
|
89
94
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
95
|
+
# All events that could be used to select a publication date.
|
96
|
+
# Includes publication, creation, and capture events.
|
97
|
+
# Considers event types as well as date types if the event is untyped.
|
98
|
+
# @return [Array<CocinaDisplay::Events::Event>]
|
99
|
+
def publication_events
|
100
|
+
events.filter { |event| event.has_any_type?("publication", "creation", "capture") }
|
96
101
|
end
|
97
102
|
|
98
103
|
# Array of CocinaDisplay::Imprint objects for all relevant Cocina events.
|
@@ -100,19 +105,22 @@ module CocinaDisplay
|
|
100
105
|
# Considers event types as well as date types if the event is untyped.
|
101
106
|
# Prefers events where the date was not encoded, if any.
|
102
107
|
# @return [Array<CocinaDisplay::Imprint>] The list of Imprint objects
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
path("$.description.event[?@.date[?match(@.type, #{filter_expr})]]")
|
109
|
-
).uniq.map do |event|
|
110
|
-
CocinaDisplay::Imprint.new(event)
|
108
|
+
def imprint_events
|
109
|
+
imprints = events.filter do |event|
|
110
|
+
event.has_any_type?("publication", "creation", "capture", "copyright")
|
111
|
+
end.map do |event|
|
112
|
+
CocinaDisplay::Events::Imprint.new(event.cocina)
|
111
113
|
end
|
112
114
|
|
113
115
|
imprints.reject(&:date_encoding?).presence || imprints
|
114
116
|
end
|
115
117
|
|
118
|
+
# All dates associated with the object via an event.
|
119
|
+
# @return [Array<CocinaDisplay::Dates::Date>]
|
120
|
+
def event_dates
|
121
|
+
@event_dates ||= events.flat_map(&:dates)
|
122
|
+
end
|
123
|
+
|
116
124
|
# The earliest preferred publication date as a CocinaDisplay::Dates::Date object.
|
117
125
|
# Considers publication, creation, and capture dates in that order.
|
118
126
|
# Prefers dates marked as primary and those with a declared encoding.
|
@@ -120,8 +128,12 @@ module CocinaDisplay
|
|
120
128
|
# @return [CocinaDisplay::Dates::Date] The earliest preferred date
|
121
129
|
# @return [nil] if no dates are left after filtering
|
122
130
|
def pub_date(ignore_qualified: false)
|
123
|
-
|
124
|
-
|
131
|
+
pub_event_dates = event_dates.filter { |date| date.type == "publication" }
|
132
|
+
creation_event_dates = event_dates.filter { |date| date.type == "creation" }
|
133
|
+
capture_event_dates = event_dates.filter { |date| date.type == "capture" }
|
134
|
+
|
135
|
+
[pub_event_dates, creation_event_dates, capture_event_dates].flat_map do |dates|
|
136
|
+
earliest_preferred_date(dates, ignore_qualified: ignore_qualified)
|
125
137
|
end.compact.first
|
126
138
|
end
|
127
139
|
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require "active_support/core_ext/enumerable"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for extracting format/genre information from a Cocina object
|
6
|
+
module Forms
|
7
|
+
# Resource types of the object, expressed in SearchWorks controlled vocabulary.
|
8
|
+
# @return [Array<String>]
|
9
|
+
def resource_types
|
10
|
+
mapped_values = resource_type_values.flat_map { |resource_type| searchworks_resource_type(resource_type) }
|
11
|
+
mapped_values << "Dataset" if dataset?
|
12
|
+
mapped_values.uniq
|
13
|
+
end
|
14
|
+
|
15
|
+
# Physical or digital forms of the object.
|
16
|
+
# @return [Array<String>]
|
17
|
+
# @example GIS dataset (nz187ct8959)
|
18
|
+
# record.forms #=> ["map", "optical disc", "electronic resource"]
|
19
|
+
def forms
|
20
|
+
path("$.description.form..[?@.type == 'form'].value").uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
# Extent of the object, such as "1 audiotape" or "1 map".
|
24
|
+
# @return [Array<String>]
|
25
|
+
# @example Oral history interview (sw705fr7011)
|
26
|
+
# record.extents #=> ["1 audiotape", "1 transcript"]
|
27
|
+
def extents
|
28
|
+
path("$.description.form..[?@.type == 'extent'].value").uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
# Genres of the object, capitalized for display.
|
32
|
+
# @return [Array<String>]
|
33
|
+
# @example GIS dataset (nz187ct8959)
|
34
|
+
# record.genres #=> ["Cartographic dataset", "Geospatial data", "Geographic information systems data"]
|
35
|
+
def genres
|
36
|
+
path("$.description.form..[?@.type == 'genre'].value").map(&:upcase_first).uniq
|
37
|
+
end
|
38
|
+
|
39
|
+
# Genres of the object, with additional values added for search/faceting.
|
40
|
+
# @note These values are added for discovery in SearchWorks but not for display.
|
41
|
+
# @return [Array<String>]
|
42
|
+
def genres_search
|
43
|
+
genres.tap do |values|
|
44
|
+
values << "Thesis/Dissertation" if values.include?("Thesis")
|
45
|
+
values << "Conference proceedings" if values.include?("Conference publication")
|
46
|
+
values << "Government document" if values.include?("Government publication")
|
47
|
+
end.uniq
|
48
|
+
end
|
49
|
+
|
50
|
+
# Is the object a periodical or serial?
|
51
|
+
# @return [Boolean]
|
52
|
+
def periodical?
|
53
|
+
issuance_terms.include?("periodical") || issuance_terms.include?("serial") || frequency.any?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Is the object a cartographic resource?
|
57
|
+
# @return [Boolean]
|
58
|
+
def cartographic?
|
59
|
+
resource_type_values.include?("cartographic")
|
60
|
+
end
|
61
|
+
|
62
|
+
# Is the object a web archive?
|
63
|
+
# @return [Boolean]
|
64
|
+
def archived_website?
|
65
|
+
genres.include?("Archived website")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Is the object a dataset?
|
69
|
+
# @return [Boolean]
|
70
|
+
def dataset?
|
71
|
+
genres.include?("Dataset")
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Map a resource type to SearchWorks format value(s).
|
77
|
+
# @param resource_type [String] The resource type to map.
|
78
|
+
# @return [Array<String>]
|
79
|
+
def searchworks_resource_type(resource_type)
|
80
|
+
values = []
|
81
|
+
|
82
|
+
case resource_type
|
83
|
+
when "cartographic"
|
84
|
+
values << "Map"
|
85
|
+
when "manuscript", "mixed material"
|
86
|
+
values << "Archive/Manuscript"
|
87
|
+
when "moving image"
|
88
|
+
values << "Video"
|
89
|
+
when "notated music"
|
90
|
+
values << "Music score"
|
91
|
+
when "software, multimedia"
|
92
|
+
# Prevent GIS datasets from being labeled as "Software"
|
93
|
+
values << "Software/Multimedia" unless cartographic? || dataset?
|
94
|
+
when "sound recording-musical"
|
95
|
+
values << "Music recording"
|
96
|
+
when "sound recording-nonmusical", "sound recording"
|
97
|
+
values << "Sound recording"
|
98
|
+
when "still image"
|
99
|
+
values << "Image"
|
100
|
+
when "text"
|
101
|
+
# Can potentially map to periodical AND website if both are true. Only
|
102
|
+
# 2 records currently (2025) in Searchworks do this, but it is real.
|
103
|
+
if periodical? || archived_website?
|
104
|
+
values << "Journal/Periodical" if periodical?
|
105
|
+
values << "Archived website" if archived_website?
|
106
|
+
else
|
107
|
+
values << "Book"
|
108
|
+
end
|
109
|
+
when "three dimensional object"
|
110
|
+
values << "Object"
|
111
|
+
end
|
112
|
+
|
113
|
+
values.compact_blank
|
114
|
+
end
|
115
|
+
|
116
|
+
# Issuance terms for a work, drawn from the event notes.
|
117
|
+
# @return [Array<String>]
|
118
|
+
def issuance_terms
|
119
|
+
path("$.description.event.*.note[?@.type == 'issuance'].value").map(&:downcase).uniq
|
120
|
+
end
|
121
|
+
|
122
|
+
# Frequency terms for a periodical, drawn from the event notes.
|
123
|
+
# @return [Array<String>]
|
124
|
+
def frequency
|
125
|
+
path("$.description.event.*.note[?@.type == 'frequency'].value").map(&:downcase).uniq
|
126
|
+
end
|
127
|
+
|
128
|
+
# Values of the resource type form field prior to mapping.
|
129
|
+
def resource_type_values
|
130
|
+
path("$.description.form..[?@.type == 'resource type'].value").uniq
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative "../language"
|
2
|
+
|
3
|
+
module CocinaDisplay
|
4
|
+
module Concerns
|
5
|
+
# Methods for extracting language information from a Cocina object.
|
6
|
+
module Languages
|
7
|
+
# Languages objects associated with the object.
|
8
|
+
# @return [Array<CocinaDisplay::Language>]
|
9
|
+
def languages
|
10
|
+
@languages ||= path("$.description.language.*").map { |lang| CocinaDisplay::Language.new(lang) }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Names of languages associated with the object, if recognized by Searchworks.
|
14
|
+
# @return [Array<String>]
|
15
|
+
def searchworks_language_names
|
16
|
+
languages.filter_map { |lang| lang.display_str if lang.searchworks_language? }.compact_blank.uniq
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|