cocina_display 0.3.0 → 0.5.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: 6ef43e2d573c99c10d6db87974ff32091ff7e7d312bf2e812ef1043f513f291c
4
- data.tar.gz: f04b35117201aaebff5e8d041d2776525dcd18a46bc408baf9d8649376d90618
3
+ metadata.gz: 5662a1512975323d0324c31021fcef9cd4ff337ea8acfb5f6a3352d4abf0e303
4
+ data.tar.gz: fe0444d5c104e393718a996260346f98691f2e84585483bb7a2062acab347bfb
5
5
  SHA512:
6
- metadata.gz: c36dd129d129ce44c7c516b670295b59f68732d0270664dda63ce119fe37ec60e68c6fd615e37ada5ecef06777db7be30cd708d0f0c461c18c97085e98eb553f
7
- data.tar.gz: cddd56237670d908a6be7951e24fe62a0fbd3076fd0eff96693c7debc03fa9ab8e01157a330923938143ec2067a4d97d4ead339f44fd2b00d0bcf99ea92b6af0
6
+ metadata.gz: 8ff64dadfe8b2492f23d38bcc320e8de4bc6100af09adfd93d25df0aeb84c91585072dbcbb6cafc297bd503d773ee7b6bf35281fdda1d573e65f94fcb866a5a0
7
+ data.tar.gz: 29c42ee65d59dbac907159435f8c9924742cc4d598e656a787dee8cbfceaab8231f2bbf8cb87d14ee180ed586fc51b4d498e8009b6fd5442442d8b0932ebd30d
data/README.md CHANGED
@@ -40,7 +40,7 @@ There is also a helper method to fetch the Cocina JSON for a given DRUID and imm
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
- > record.title
43
+ > record.main_title
44
44
  => "Bugatti Type 51A. Road & Track Salon January 1957"
45
45
  > record.content_type
46
46
  => "image"
@@ -7,13 +7,22 @@ require "active_support"
7
7
  require "active_support/core_ext/object/blank"
8
8
  require "active_support/core_ext/hash/conversions"
9
9
 
10
- require_relative "title_builder"
11
10
  require_relative "concerns/events"
11
+ require_relative "concerns/contributors"
12
+ require_relative "concerns/identifiers"
13
+ require_relative "concerns/titles"
14
+ require_relative "concerns/access"
15
+ require_relative "concerns/subjects"
12
16
 
13
17
  module CocinaDisplay
14
18
  # Public Cocina metadata for an SDR object, as fetched from PURL.
15
19
  class CocinaRecord
16
20
  include CocinaDisplay::Concerns::Events
21
+ include CocinaDisplay::Concerns::Contributors
22
+ include CocinaDisplay::Concerns::Identifiers
23
+ include CocinaDisplay::Concerns::Titles
24
+ include CocinaDisplay::Concerns::Access
25
+ include CocinaDisplay::Concerns::Subjects
17
26
 
18
27
  # Fetch a public Cocina document from PURL and create a CocinaRecord.
19
28
  # @note This is intended to be used in development or testing only.
@@ -45,60 +54,6 @@ module CocinaDisplay
45
54
  Janeway.enum_for(path_expression, cocina_doc)
46
55
  end
47
56
 
48
- # The DRUID for the object, with the +druid:+ prefix.
49
- # @return [String]
50
- # @example
51
- # record.druid #=> "druid:bb099mt5053"
52
- def druid
53
- cocina_doc["externalIdentifier"]
54
- end
55
-
56
- # The DRUID for the object, without the +druid:+ prefix.
57
- # @return [String]
58
- # @example
59
- # record.bare_druid #=> "bb099mt5053"
60
- def bare_druid
61
- druid.delete_prefix("druid:")
62
- end
63
-
64
- # The DOI for the object, if there is one – just the identifier part.
65
- # @return [String, nil]
66
- # @example
67
- # record.doi #=> "10.25740/ppax-bf07"
68
- def doi
69
- doi_id = path("$.identification.doi").first ||
70
- path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
71
- path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
72
-
73
- URI(doi_id).path.delete_prefix("/") if doi_id.present?
74
- end
75
-
76
- # The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
77
- # @return [String, nil]
78
- # @example
79
- # record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
80
- def doi_url
81
- URI.join("https://doi.org", doi).to_s if doi.present?
82
- end
83
-
84
- # The HRID of the item in FOLIO, if defined.
85
- # @note This doesn't imply the object is available in Searchworks at this ID.
86
- # @return [String, nil]
87
- # @example
88
- # record.folio_hrid #=> "a12845814"
89
- def folio_hrid
90
- path("$.identification.catalogLinks[?(@.catalog == 'folio')].catalogRecordId").first
91
- end
92
-
93
- # The FOLIO HRID if defined, otherwise the bare DRUID.
94
- # @note This doesn't imply the object is available in Searchworks at this ID.
95
- # @see folio_hrid
96
- # @see bare_druid
97
- # @return [String]
98
- def searchworks_id
99
- folio_hrid || bare_druid
100
- end
101
-
102
57
  # Timestamp when the Cocina was created.
103
58
  # @note This is for the metadata itself, not the object.
104
59
  # @return [Time]
@@ -122,34 +77,19 @@ module CocinaDisplay
122
77
  cocina_doc["type"].split("/").last
123
78
  end
124
79
 
80
+ # Primary processing label for the object.
81
+ # @note This may or may not be the same as the title.
82
+ # @return [String, nil]
83
+ def label
84
+ cocina_doc["label"]
85
+ end
86
+
125
87
  # True if the object is a collection.
126
88
  # @return [Boolean]
127
89
  def collection?
128
90
  content_type == "collection"
129
91
  end
130
92
 
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
-
153
93
  # Traverse nested FileSets and return an enumerator over their files.
154
94
  # Each file is a +Hash+.
155
95
  # @return [Enumerator] Enumerator over file hashes
@@ -161,70 +101,5 @@ module CocinaDisplay
161
101
  def files
162
102
  path("$.structural.contains[*].structural.contains[*]")
163
103
  end
164
-
165
- # The PURL URL for this object.
166
- # @return [String]
167
- # @example
168
- # record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
169
- def purl_url
170
- cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
171
- end
172
-
173
- # The URL to the PURL environment this object is from.
174
- # @note Objects accessed via UAT will still have a production PURL base URL.
175
- # @return [String]
176
- # @example
177
- # record.purl_base_url #=> "https://purl.stanford.edu"
178
- def purl_base_url
179
- URI(purl_url).origin
180
- end
181
-
182
- # The URL to the stacks environment this object is shelved in.
183
- # Corresponds to the PURL environment.
184
- # @see purl_base_url
185
- # @return [String]
186
- # @example
187
- # record.stacks_base_url #=> "https://stacks.stanford.edu"
188
- def stacks_base_url
189
- if purl_base_url == "https://sul-purl-stage.stanford.edu"
190
- "https://sul-stacks-stage.stanford.edu"
191
- else
192
- "https://stacks.stanford.edu"
193
- end
194
- end
195
-
196
- # The oEmbed URL for the object, optionally with additional parameters.
197
- # Corresponds to the PURL environment.
198
- # @param params [Hash] Additional parameters to include in the oEmbed URL.
199
- # @return [String]
200
- # @return [nil] if the object is a collection.
201
- # @example Generate an oEmbed URL for the viewer and hide the title
202
- # record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
203
- def oembed_url(params: {})
204
- return if collection?
205
-
206
- params[:url] ||= purl_url
207
- "#{purl_base_url}/embed.json?#{params.to_query}"
208
- end
209
-
210
- # The download URL to get the entire object as a .zip file.
211
- # Stacks generates the .zip for the object on request.
212
- # @return [String]
213
- # @example
214
- # record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
215
- def download_url
216
- "#{stacks_base_url}/object/#{bare_druid}"
217
- end
218
-
219
- # The IIIF manifest URL for the object.
220
- # PURL generates the IIIF manifest.
221
- # @param version [Integer] The IIIF presentation spec version to use (3 or 2).
222
- # @return [String]
223
- # @example
224
- # record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
225
- def iiif_manifest_url(version: 3)
226
- iiif_path = (version == 3) ? "iiif3" : "iiif"
227
- "#{purl_url}/#{iiif_path}/manifest"
228
- end
229
104
  end
230
105
  end
@@ -0,0 +1,71 @@
1
+ module CocinaDisplay
2
+ module Concerns
3
+ # Methods that generate URLs to access an object.
4
+ module Access
5
+ # The PURL URL for this object.
6
+ # @return [String]
7
+ # @example
8
+ # record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
9
+ def purl_url
10
+ cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
11
+ end
12
+
13
+ # The URL to the PURL environment this object is from.
14
+ # @note Objects accessed via UAT will still have a production PURL base URL.
15
+ # @return [String]
16
+ # @example
17
+ # record.purl_base_url #=> "https://purl.stanford.edu"
18
+ def purl_base_url
19
+ URI(purl_url).origin
20
+ end
21
+
22
+ # The URL to the stacks environment this object is shelved in.
23
+ # Corresponds to the PURL environment.
24
+ # @see purl_base_url
25
+ # @return [String]
26
+ # @example
27
+ # record.stacks_base_url #=> "https://stacks.stanford.edu"
28
+ def stacks_base_url
29
+ if purl_base_url == "https://sul-purl-stage.stanford.edu"
30
+ "https://sul-stacks-stage.stanford.edu"
31
+ else
32
+ "https://stacks.stanford.edu"
33
+ end
34
+ end
35
+
36
+ # The oEmbed URL for the object, optionally with additional parameters.
37
+ # Corresponds to the PURL environment.
38
+ # @param params [Hash] Additional parameters to include in the oEmbed URL.
39
+ # @return [String]
40
+ # @return [nil] if the object is a collection.
41
+ # @example Generate an oEmbed URL for the viewer and hide the title
42
+ # record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
43
+ def oembed_url(params: {})
44
+ return if collection?
45
+
46
+ params[:url] ||= purl_url
47
+ "#{purl_base_url}/embed.json?#{params.to_query}"
48
+ end
49
+
50
+ # The download URL to get the entire object as a .zip file.
51
+ # Stacks generates the .zip for the object on request.
52
+ # @return [String]
53
+ # @example
54
+ # record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
55
+ def download_url
56
+ "#{stacks_base_url}/object/#{bare_druid}"
57
+ end
58
+
59
+ # The IIIF manifest URL for the object.
60
+ # PURL generates the IIIF manifest.
61
+ # @param version [Integer] The IIIF presentation spec version to use (3 or 2).
62
+ # @return [String]
63
+ # @example
64
+ # record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
65
+ def iiif_manifest_url(version: 3)
66
+ iiif_path = (version == 3) ? "iiif3" : "iiif"
67
+ "#{purl_url}/#{iiif_path}/manifest"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,94 @@
1
+ require_relative "../contributor"
2
+
3
+ module CocinaDisplay
4
+ module Concerns
5
+ # Methods for finding and formatting names for contributors
6
+ module Contributors
7
+ # The main author's name, formatted for display.
8
+ # @param with_date [Boolean] Include life dates, if present
9
+ # @return [String]
10
+ # @return [nil] if no main author is found
11
+ # @example
12
+ # record.main_author #=> "Smith, John"
13
+ # @example with date
14
+ # record.main_author(with_date: true) #=> "Smith, John, 1970-2020"
15
+ def main_author(with_date: false)
16
+ main_author_contributor&.display_name(with_date: with_date)
17
+ end
18
+
19
+ # All author names except the main one, formatted for display.
20
+ # @param with_date [Boolean] Include life dates, if present
21
+ # @return [Array<String>]
22
+ def additional_authors(with_date: false)
23
+ additional_author_contributors.map { |c| c.display_name(with_date: with_date) }
24
+ end
25
+
26
+ # All names of authors who are people, formatted for display.
27
+ # @param with_date [Boolean] Include life dates, if present
28
+ # @return [Array<String>]
29
+ def person_authors(with_date: false)
30
+ authors.filter(&:person?).map { |c| c.display_name(with_date: with_date) }
31
+ end
32
+
33
+ # All names of non-person authors, formatted for display.
34
+ # This includes organizations, conferences, families, etc.
35
+ # @return [Array<String>]
36
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-types
37
+ def impersonal_authors
38
+ authors.reject(&:person?).map(&:display_name)
39
+ end
40
+
41
+ # All names of authors that are organizations, formatted for display.
42
+ # @return [Array<String>]
43
+ def organization_authors
44
+ authors.filter(&:organization?).map(&:display_name)
45
+ end
46
+
47
+ # All names of authors that are conferences, formatted for display.
48
+ # @return [Array<String>]
49
+ def conference_authors
50
+ authors.filter(&:conference?).map(&:display_name)
51
+ end
52
+
53
+ # A string value for sorting by author that sorts missing values last.
54
+ # Ignores punctuation and leading/trailing spaces.
55
+ # @return [String]
56
+ def sort_author
57
+ (main_author_contributor&.display_name || "\u{10FFFF}").gsub(/[[:punct:]]*/, "").strip
58
+ end
59
+
60
+ private
61
+
62
+ # All contributors for the object, including authors, editors, etc.
63
+ # @return [Array<Contributor>]
64
+ def contributors
65
+ @contributors ||= path("$.description.contributor[*]").map { |c| Contributor.new(c) }
66
+ end
67
+
68
+ # All contributors with a "creator" or "author" role.
69
+ # @return [Array<Contributor>]
70
+ # @see Contributor#author?
71
+ def authors
72
+ contributors.filter(&:author?)
73
+ end
74
+
75
+ # Contributor object representing the primary author.
76
+ # Selected according to the following rules:
77
+ # 1. If there is a primary author or creator, use that.
78
+ # 2. If there are no primary authors or creators, use the first one.
79
+ # 3. If there are none at all, use the first contributor without any role.
80
+ # @return [Contributor]
81
+ # @return [nil] if no suitable contributor is found
82
+ def main_author_contributor
83
+ authors.find(&:primary?).presence || authors.first || contributors.find { |c| !c.role? }.presence
84
+ end
85
+
86
+ # All author/creator contributors except the main one.
87
+ # @return [Array<Contributor>]
88
+ def additional_author_contributors
89
+ return [] if authors.empty? || authors.one? || !authors.include?(main_author_contributor)
90
+ authors - [main_author_contributor]
91
+ end
92
+ end
93
+ end
94
+ end
@@ -41,6 +41,20 @@ module CocinaDisplay
41
41
  pub_date_edtf(ignore_qualified: ignore_qualified)&.year
42
42
  end
43
43
 
44
+ # The range of preferred publication years as an array of integers.
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 [Array<Integer>, nil]
49
+ # @note 6 BCE will appear as -5; 4 CE will appear as 4.
50
+ def pub_year_int_range(ignore_qualified: false)
51
+ date = pub_date(ignore_qualified: ignore_qualified)
52
+ return unless date
53
+
54
+ date = date.as_interval if date.is_a? CocinaDisplay::Dates::DateRange
55
+ date.to_a.map(&:year).compact.uniq.sort
56
+ end
57
+
44
58
  # String for displaying the earliest preferred publication year or range.
45
59
  # Considers publication, creation, and capture dates in that order.
46
60
  # Prefers dates marked as primary and those with a declared encoding.
@@ -0,0 +1,68 @@
1
+ module CocinaDisplay
2
+ module Concerns
3
+ # Methods for extracting and formatting identifiers from Cocina records.
4
+ module Identifiers
5
+ # The DRUID for the object, with the +druid:+ prefix.
6
+ # @return [String]
7
+ # @example
8
+ # record.druid #=> "druid:bb099mt5053"
9
+ def druid
10
+ cocina_doc["externalIdentifier"]
11
+ end
12
+
13
+ # The DRUID for the object, without the +druid:+ prefix.
14
+ # @return [String]
15
+ # @example
16
+ # record.bare_druid #=> "bb099mt5053"
17
+ def bare_druid
18
+ druid.delete_prefix("druid:")
19
+ end
20
+
21
+ # The DOI for the object, if there is one – just the identifier part.
22
+ # @return [String, nil]
23
+ # @example
24
+ # record.doi #=> "10.25740/ppax-bf07"
25
+ def doi
26
+ doi_id = path("$.identification.doi").first ||
27
+ path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
28
+ path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
29
+
30
+ URI(doi_id).path.delete_prefix("/") if doi_id.present?
31
+ end
32
+
33
+ # The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
34
+ # @return [String, nil]
35
+ # @example
36
+ # record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
37
+ def doi_url
38
+ URI.join("https://doi.org", doi).to_s if doi.present?
39
+ end
40
+
41
+ # The HRID of the item in FOLIO, if defined.
42
+ # @note This doesn't imply the object is available in Searchworks at this ID.
43
+ # @param [refresh] [Boolean] Filter to links with refresh set to this value.
44
+ # @return [String, nil]
45
+ # @example With a link regardless of refresh:
46
+ # record.folio_hrid #=> "a12845814"
47
+ # @example With a link that is not refreshed:
48
+ # record.folio_hrid(refresh: true) #=> nil
49
+ def folio_hrid(refresh: nil)
50
+ link = path("$.identification.catalogLinks[?(@.catalog == 'folio')]").first
51
+ hrid = link&.dig("catalogRecordId")
52
+ return if hrid.blank?
53
+ return hrid if refresh.nil?
54
+
55
+ (link["refresh"] == refresh) ? hrid : nil
56
+ end
57
+
58
+ # The FOLIO HRID if defined, otherwise the bare DRUID.
59
+ # @note This doesn't imply the object is available in Searchworks at this ID.
60
+ # @see folio_hrid
61
+ # @see bare_druid
62
+ # @return [String]
63
+ def searchworks_id
64
+ folio_hrid || bare_druid
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ require_relative "../subject"
2
+
3
+ module CocinaDisplay
4
+ module Concerns
5
+ # Methods for extracting and formatting subject information.
6
+ module Subjects
7
+ # All unique subjects that are topics, formatted as strings for display.
8
+ # @return [Array<String>]
9
+ def subject_topics
10
+ subjects.filter { |s| s.type == "topic" }.map(&:display_str).uniq
11
+ end
12
+
13
+ # All unique subjects that are genres, formatted as strings for display.
14
+ # @return [Array<String>]
15
+ def subject_genres
16
+ subjects.filter { |s| s.type == "genre" }.map(&:display_str).uniq
17
+ end
18
+
19
+ # All unique subjects that are titles, formatted as strings for display.
20
+ # @return [Array<String>]
21
+ def subject_titles
22
+ subjects.filter { |s| s.type == "title" }.map(&:display_str).uniq
23
+ end
24
+
25
+ # All unique subjects that are date/time info, formatted as strings for display.
26
+ # @return [Array<String>]
27
+ def subject_temporal
28
+ subjects.filter { |s| s.type == "time" }.map(&:display_str).uniq
29
+ end
30
+
31
+ # All unique subjects that are occupations, formatted as strings for display.
32
+ # @return [Array<String>]
33
+ def subject_occupations
34
+ subjects.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
35
+ end
36
+
37
+ # All unique subjects that are names of entities, formatted as strings for display.
38
+ # @note Multiple types are handled: person, family, organization, conference, etc.
39
+ # @see CocinaDisplay::NameSubject
40
+ # @return [Array<String>]
41
+ def subject_names
42
+ subjects.filter { |s| s.is_a? NameSubject }.map(&:display_str).uniq
43
+ end
44
+
45
+ private
46
+
47
+ # All subjects, accessible as Subject objects.
48
+ # Checks both description.subject and description.geographic.subject.
49
+ # @return [Array<Subject>]
50
+ def subjects
51
+ @subjects ||= Enumerator::Chain.new(
52
+ path("$.description.subject[*]"),
53
+ path("$.description.geographic[*].subject[*]")
54
+ ).map { |s| Subject.from_cocina(s) }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ require_relative "../title_builder"
2
+
3
+ module CocinaDisplay
4
+ module Concerns
5
+ # Methods for finding and formatting titles.
6
+ module Titles
7
+ # The main title for the object, without subtitle, part name, etc.
8
+ # If there are multiple titles, uses the first.
9
+ # @see CocinaDisplay::TitleBuilder#main_title
10
+ # @note This corresponds to the "short title" in MODS XML, or MARC 245$a only.
11
+ # @return [String]
12
+ def main_title
13
+ CocinaDisplay::TitleBuilder.main_title(cocina_titles).first
14
+ end
15
+
16
+ # The full title for the object, including subtitle, part name, etc.
17
+ # If there are multiple titles, uses the first.
18
+ # @see CocinaDisplay::TitleBuilder#full_title
19
+ # @note This corresponds to the entire MARC 245 field.
20
+ # @return [String]
21
+ def full_title
22
+ CocinaDisplay::TitleBuilder.full_title(cocina_titles, catalog_links: catalog_links).first
23
+ end
24
+
25
+ # The full title, joined together with additional punctuation.
26
+ # If there are multiple titles, uses the first.
27
+ # @see CocinaDisplay::TitleBuilder#build
28
+ # @return [String]
29
+ def display_title
30
+ CocinaDisplay::TitleBuilder.build(cocina_titles, catalog_links: catalog_links)
31
+ end
32
+
33
+ # Any additional titles for the object excluding the main title.
34
+ # @return [Array<String>]
35
+ # @see CocinaDisplay::TitleBuilder#additional_titles
36
+ def additional_titles
37
+ CocinaDisplay::TitleBuilder.additional_titles(cocina_titles)
38
+ end
39
+
40
+ # A string value for sorting by title that sorts missing values last.
41
+ # Ignores punctuation, leading/trailing spaces, and non-sorting characters.
42
+ # @see CocinaDisplay::TitleBuilder#sort_title
43
+ # @return [String]
44
+ def sort_title
45
+ CocinaDisplay::TitleBuilder.sort_title(cocina_titles, catalog_links: catalog_links).first || "\u{10FFFF}"
46
+ end
47
+
48
+ private
49
+
50
+ # The titles from the Cocina document, as an array of hashes.
51
+ # @return [Array<Hash>]
52
+ def cocina_titles
53
+ @cocina_titles ||= Array(cocina_doc.dig("description", "title"))
54
+ end
55
+
56
+ # The catalog links from the Cocina document, as an array of hashes.
57
+ # These link to FOLIO and can include part labels used to construct titles.
58
+ # @return [Array<Hash>]
59
+ def catalog_links
60
+ @catalog_links ||= Array(cocina_doc.dig("identification", "catalogLinks"))
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext/object/blank"
5
+ require "active_support/core_ext/array/conversions"
6
+
7
+ require_relative "utils"
8
+
9
+ module CocinaDisplay
10
+ # A contributor to a work, such as an author or publisher.
11
+ class Contributor
12
+ attr_reader :cocina
13
+
14
+ # Initialize a Contributor object with Cocina structured data.
15
+ # @param cocina [Hash] The Cocina structured data for the contributor.
16
+ def initialize(cocina)
17
+ @cocina = cocina
18
+ end
19
+
20
+ # String representation of the contributor, including name and role.
21
+ # Used for debugging and logging.
22
+ # @return [String]
23
+ def to_s
24
+ Utils.compact_and_join([display_name, display_role], delimiter: ": ")
25
+ end
26
+
27
+ # Is this contributor a human?
28
+ # @return [Boolean]
29
+ def person?
30
+ cocina["type"] == "person"
31
+ end
32
+
33
+ # Is this contributor an organization?
34
+ # @return [Boolean]
35
+ def organization?
36
+ cocina["type"] == "organization"
37
+ end
38
+
39
+ # Is this contributor a conference?
40
+ # @return [Boolean]
41
+ def conference?
42
+ cocina["type"] == "conference"
43
+ end
44
+
45
+ # Is this contributor marked as primary?
46
+ # @return [Boolean]
47
+ def primary?
48
+ cocina["status"] == "primary"
49
+ end
50
+
51
+ # Does this contributor have a role that indicates they are an author?
52
+ # @return [Boolean]
53
+ def author?
54
+ roles.any? { |role| role["value"] =~ /(author|creator)/i }
55
+ end
56
+
57
+ # Does this contributor have any roles defined?
58
+ # @return [Boolean]
59
+ def role?
60
+ roles.any?
61
+ end
62
+
63
+ # The display name for the contributor as a string.
64
+ # Uses the first name if multiple names are present.
65
+ # @param with_date [Boolean] Include life dates, if present
66
+ # @return [String]
67
+ def display_name(with_date: false)
68
+ names.map { |name| name.display_str(with_date: with_date) }.first
69
+ end
70
+
71
+ # A string representation of the contributor's roles, formatted for display.
72
+ # If there are multiple roles, they are joined with commas.
73
+ # @return [String]
74
+ def display_role
75
+ roles.map { |role| role["value"] }.to_sentence
76
+ end
77
+
78
+ private
79
+
80
+ # All names in the Cocina as Name objects.
81
+ # @return [Array<Name>]
82
+ def names
83
+ @names ||= Array(cocina["name"]).map { |name| Name.new(name) }
84
+ end
85
+
86
+ # All roles in the Cocina structured data.
87
+ # @return [Array<Hash>]
88
+ def roles
89
+ Array(cocina["role"])
90
+ end
91
+
92
+ # A name associated with a contributor.
93
+ class Name
94
+ attr_reader :cocina
95
+
96
+ # Initialize a Name object with Cocina structured data.
97
+ # @param cocina [Hash] The Cocina structured data for the name.
98
+ def initialize(cocina)
99
+ @cocina = cocina
100
+ end
101
+
102
+ # The display string for the name, optionally including life dates.
103
+ # Uses these values in order, if present:
104
+ # 1. Unstructured value
105
+ # 2. Any structured/parallel values marked as "display"
106
+ # 3. Joined structured values, optionally with life dates
107
+ # @param with_date [Boolean] Include life dates, if present
108
+ # @return [String]
109
+ # @example no dates
110
+ # name.display_name # => "King, Martin Luther, Jr."
111
+ # @example with dates
112
+ # name.display_name(with_date: true) # => "King, Martin Luther, Jr., 1929-1968"
113
+ def display_str(with_date: false)
114
+ if cocina["value"].present?
115
+ cocina["value"]
116
+ elsif display_name_str.present?
117
+ display_name_str
118
+ elsif dates_str.present? && with_date
119
+ Utils.compact_and_join([full_name_str, dates_str], delimiter: ", ")
120
+ else
121
+ full_name_str
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ # The full name as a string, combining all name components.
128
+ # @return [String]
129
+ def full_name_str
130
+ Utils.compact_and_join(name_components, delimiter: ", ")
131
+ end
132
+
133
+ # Flattened form of any names explicitly marked as "display name".
134
+ # @return [String]
135
+ def display_name_str
136
+ Utils.compact_and_join(Array(name_values["display"]), delimiter: ", ")
137
+ end
138
+
139
+ # List of all name components.
140
+ # If any of forename, surname, or term of address are present, those are used.
141
+ # Otherwise, fall back to any names explicitly marked as "name" or untyped.
142
+ # @return [Array<String>]
143
+ def name_components
144
+ [surname_str, forename_ordinal_str, terms_of_address_str].compact_blank.presence || Array(name_values["name"])
145
+ end
146
+
147
+ # Flatten all forenames and ordinals into a single string.
148
+ # @return [String]
149
+ def forename_ordinal_str
150
+ Utils.compact_and_join(Array(name_values["forename"]) + Array(name_values["ordinal"]), delimiter: " ")
151
+ end
152
+
153
+ # Flatten all terms of address into a single string.
154
+ # @return [String]
155
+ def terms_of_address_str
156
+ Utils.compact_and_join(Array(name_values["term of address"]), delimiter: ", ")
157
+ end
158
+
159
+ # Flatten all surnames into a single string.
160
+ # @return [String]
161
+ def surname_str
162
+ Utils.compact_and_join(Array(name_values["surname"]), delimiter: " ")
163
+ end
164
+
165
+ # Flatten all life and activity dates into a single string.
166
+ # @return [String]
167
+ def dates_str
168
+ Utils.compact_and_join(Array(name_values["life dates"]) + Array(name_values["activity dates"]), delimiter: ", ")
169
+ end
170
+
171
+ # A hash mapping destructured name types to their values.
172
+ # Name values with no type are grouped under "name".
173
+ # @return [Hash<String, Array<String>>]
174
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-name-part-types-for-structured-value
175
+ # @note Currently we do nothing with "alternative", "inverted full name", "pseudonym", and "transliteration" types.
176
+ def name_values
177
+ Utils.flatten_nested_values(cocina).each_with_object({}) do |node, hash|
178
+ type = node["type"] || "name"
179
+ hash[type] ||= []
180
+ hash[type] << node["value"]
181
+ end.compact_blank
182
+ end
183
+ end
184
+ end
185
+ end
@@ -5,9 +5,10 @@ require "active_support"
5
5
  require "active_support/core_ext/enumerable"
6
6
  require "active_support/core_ext/object/blank"
7
7
 
8
+ require_relative "utils"
9
+ require_relative "marc_country_codes"
8
10
  require_relative "dates/date"
9
11
  require_relative "dates/date_range"
10
- require_relative "marc_country_codes"
11
12
 
12
13
  module CocinaDisplay
13
14
  # Wrapper for Cocina events used to generate an imprint statement for display.
@@ -20,23 +21,6 @@ module CocinaDisplay
20
21
  cocina_dates.map { |cd| CocinaDisplay::Dates::Date.from_cocina(cd) }.filter(&:parsable?).compact
21
22
  end
22
23
 
23
- # Join non-empty values into a string using provided delimiter.
24
- # If values already end in delimiter (ignoring whitespace), join with a space instead.
25
- # @param values [Array<String>] The values to compact and join
26
- # @param delimiter [String] The delimiter to use for joining, default is space
27
- def self.compact_and_join(values, delimiter: " ")
28
- compacted_values = values.compact_blank.map(&:strip)
29
- return compacted_values.first if compacted_values.one?
30
-
31
- compacted_values.reduce(+"") do |result, value|
32
- result << if value.end_with?(delimiter.strip)
33
- value + " "
34
- else
35
- value + delimiter
36
- end
37
- end.delete_suffix(delimiter)
38
- end
39
-
40
24
  attr_reader :cocina, :dates
41
25
 
42
26
  # Initialize the imprint with Cocina event data.
@@ -49,9 +33,9 @@ module CocinaDisplay
49
33
  # The entire imprint statement formatted as a string for display.
50
34
  # @return [String]
51
35
  def display_str
52
- place_pub = self.class.compact_and_join([place_str, publisher_str], delimiter: " : ")
53
- edition_place_pub = self.class.compact_and_join([edition_str, place_pub], delimiter: " - ")
54
- self.class.compact_and_join([edition_place_pub, date_str], delimiter: ", ")
36
+ place_pub = Utils.compact_and_join([place_str, publisher_str], delimiter: " : ")
37
+ edition_place_pub = Utils.compact_and_join([edition_str, place_pub], delimiter: " - ")
38
+ Utils.compact_and_join([edition_place_pub, date_str], delimiter: ", ")
55
39
  end
56
40
 
57
41
  # Were any of the dates encoded?
@@ -65,25 +49,25 @@ module CocinaDisplay
65
49
  # The date portion of the imprint statement, comprising all unique dates.
66
50
  # @return [String]
67
51
  def date_str
68
- self.class.compact_and_join(unique_dates_for_display.map(&:qualified_value))
52
+ Utils.compact_and_join(unique_dates_for_display.map(&:qualified_value))
69
53
  end
70
54
 
71
55
  # The editions portion of the imprint statement, combining all edition notes.
72
56
  # @return [String]
73
57
  def edition_str
74
- self.class.compact_and_join(Janeway.enum_for("$.note[?@.type == 'edition'].value", cocina))
58
+ Utils.compact_and_join(Janeway.enum_for("$.note[?@.type == 'edition'].value", cocina))
75
59
  end
76
60
 
77
61
  # The place of publication, combining all location values.
78
62
  # @return [String]
79
63
  def place_str
80
- self.class.compact_and_join(locations_for_display, delimiter: " : ")
64
+ Utils.compact_and_join(locations_for_display, delimiter: " : ")
81
65
  end
82
66
 
83
67
  # The publisher information, combining all name values for publishers.
84
68
  # @return [String]
85
69
  def publisher_str
86
- self.class.compact_and_join(Janeway.enum_for("$.contributor[?@.role[?@.value == 'publisher']].name[*].value", cocina), delimiter: " : ")
70
+ Utils.compact_and_join(Janeway.enum_for("$.contributor[?@.role[?@.value == 'publisher']].name[*].value", cocina), delimiter: " : ")
87
71
  end
88
72
 
89
73
  # Get the place name for a location, decoding from MARC if necessary.
@@ -0,0 +1,127 @@
1
+ require_relative "utils"
2
+ require_relative "contributor"
3
+ require_relative "title_builder"
4
+ require_relative "dates/date"
5
+
6
+ module CocinaDisplay
7
+ # Base class for subjects in Cocina structured data.
8
+ class Subject
9
+ attr_reader :cocina
10
+
11
+ # Extract the type of the subject from the Cocina structured data.
12
+ # If no top-level type, uses the first structuredValue type.
13
+ # @param cocina [Hash] The Cocina structured data for the subject.
14
+ # @return [String, nil] The type of the subject, or nil if none
15
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#subject-types
16
+ def self.detect_type(cocina)
17
+ cocina["type"] || Utils.flatten_nested_values(cocina).pick("type")
18
+ end
19
+
20
+ # Choose and create the appropriate Subject subclass based on type.
21
+ # @param cocina [Hash] The Cocina structured data for the subject.
22
+ # @return [Subject]
23
+ # @see detect_type
24
+ def self.from_cocina(cocina)
25
+ case detect_type(cocina)
26
+ when "person", "family", "organization", "conference", "event", "name"
27
+ NameSubject.new(cocina)
28
+ when "title"
29
+ TitleSubject.new(cocina)
30
+ when "time"
31
+ TemporalSubject.new(cocina)
32
+ # TODO: special handling for geospatial subjects
33
+ # when "map coordinates", "bounding box coordinates", "point coordinates"
34
+ else
35
+ Subject.new(cocina)
36
+ end
37
+ end
38
+
39
+ # Initialize a Subject object with Cocina structured data.
40
+ # @param cocina [Hash] The Cocina structured data for the subject.
41
+ def initialize(cocina)
42
+ @cocina = cocina
43
+ end
44
+
45
+ # The type of the subject.
46
+ # If no top-level type, uses the first structuredValue type.
47
+ # @return [String, nil] The type of the subject, or nil if none
48
+ # @see detect_type
49
+ def type
50
+ self.class.detect_type(cocina)
51
+ end
52
+
53
+ # A string representation of the subject, formatted for display.
54
+ # Concatenates any structured values with an appropriate delimiter.
55
+ # Subclasses may override this for more specific formatting.
56
+ # @return [String]
57
+ def display_str
58
+ Utils.compact_and_join(descriptive_values, delimiter: delimiter)
59
+ end
60
+
61
+ private
62
+
63
+ # Flatten any structured values into an array of Hashes with "value" keys.
64
+ # If no structured values, will return the top-level cocina data.
65
+ # @see Utils.flatten_nested_values
66
+ # @return [Array<Hash>] An array of Hashes representing all values.
67
+ def descriptive_values
68
+ Utils.flatten_nested_values(cocina).pluck("value")
69
+ end
70
+
71
+ # Delimiter to use for joining structured subject values.
72
+ # LCSH uses a comma (the default); catalog headings use " > ".
73
+ # @return [String]
74
+ def delimiter
75
+ if cocina["displayLabel"]&.downcase == "catalog heading"
76
+ " > "
77
+ else
78
+ ", "
79
+ end
80
+ end
81
+ end
82
+
83
+ # A subject representing a named entity.
84
+ class NameSubject < Subject
85
+ attr_reader :name
86
+
87
+ # Initialize a NameSubject object with Cocina structured data.
88
+ # @param cocina [Hash] The Cocina structured data for the subject.
89
+ def initialize(cocina)
90
+ super
91
+ @name = Contributor::Name.new(cocina)
92
+ end
93
+
94
+ # Use the contributor name formatting rules for display.
95
+ # @return [String] The formatted name string, including life dates
96
+ # @see CocinaDisplay::Contributor::Name#display_str
97
+ def display_str
98
+ @name.display_str(with_date: true)
99
+ end
100
+ end
101
+
102
+ # A subject representing an entity with a title.
103
+ class TitleSubject < Subject
104
+ # Construct a title string to use for display.
105
+ # @see CocinaDisplay::TitleBuilder.build
106
+ # @note Unclear how often structured title subjects occur "in the wild".
107
+ # @return [String]
108
+ def display_str
109
+ TitleBuilder.build([cocina])
110
+ end
111
+ end
112
+
113
+ # A subject representing a date and/or time.
114
+ class TemporalSubject < Subject
115
+ attr_reader :date
116
+
117
+ def initialize(cocina)
118
+ super
119
+ @date = Dates::Date.from_cocina(cocina)
120
+ end
121
+
122
+ # @return [String] The formatted date/time string for display
123
+ def display_str
124
+ @date.qualified_value
125
+ end
126
+ end
127
+ end
@@ -46,6 +46,16 @@ module CocinaDisplay
46
46
  [new(strategy: :all, add_punctuation: false).build(titles)].flatten - full_title(titles)
47
47
  end
48
48
 
49
+ # Like the full title, but with any non-sorting characters and punctuation removed.
50
+ # @param titles [Array<Hash>] The titles to consider.
51
+ # @param catalog_links [Array<Hash>] The folio catalog links to check for digital serials part labels.
52
+ # @return [Array<String>] The sort title value(s) for Solr - array due to possible parallelValue
53
+ def self.sort_title(titles, catalog_links: [])
54
+ part_label = catalog_links.find { |link| link["catalog"] == "folio" }&.fetch("partLabel", nil)
55
+ [new(strategy: :first, add_punctuation: false, only_one_parallel_value: false, part_label: part_label, sortable: true).build(titles)]
56
+ .flatten.compact.map { |title| title.gsub(/[[:punct:]]*/, "").strip }
57
+ end
58
+
49
59
  # @param strategy [Symbol] ":first" selects a single title value based on precedence of
50
60
  # primary, untyped, first occurrence. ":all" returns an array containing all the values.
51
61
  # @param add_punctuation [boolean] whether the title should be formatted with punctuation (think of a structured
@@ -54,11 +64,13 @@ module CocinaDisplay
54
64
  # of primary, untyped, first occurrence. When false, return an array containing all the parallel values.
55
65
  # Why? Think of e.g. title displayed in blacklight search results vs boosting values for ranking of search results
56
66
  # @param part_label [String] the partLabel to add for digital serials display
57
- def initialize(strategy:, add_punctuation:, only_one_parallel_value: true, part_label: nil)
67
+ # @param sortable [boolean] whether the title is intended for sorting, and should have non-sorting parts removed
68
+ def initialize(strategy:, add_punctuation:, only_one_parallel_value: true, part_label: nil, sortable: false)
58
69
  @strategy = strategy
59
70
  @add_punctuation = add_punctuation
60
71
  @only_one_parallel_value = only_one_parallel_value
61
72
  @part_label = part_label
73
+ @sortable = sortable
62
74
  end
63
75
 
64
76
  # @param [Array<Hash>] cocina_titles the titles to consider
@@ -161,6 +173,10 @@ module CocinaDisplay
161
173
  @only_one_parallel_value
162
174
  end
163
175
 
176
+ def sortable?
177
+ @sortable
178
+ end
179
+
164
180
  # @return [Hash, nil] title that has status=primary
165
181
  def primary_title(cocina_titles)
166
182
  primary_title = cocina_titles.find { |title| title["status"] == "primary" }
@@ -221,7 +237,7 @@ module CocinaDisplay
221
237
  # rubocop:disable Metrics/CyclomaticComplexity
222
238
  # rubocop:disable Metrics/MethodLength
223
239
  # rubocop:disable Metrics/PerceivedComplexity
224
- def rebuild_structured_value(cocina_title)
240
+ def rebuild_structured_value(cocina_title, sortable: false)
225
241
  result = ""
226
242
  part_name_number = ""
227
243
  cocina_title["structuredValue"].each do |structured_value| # rubocop:disable Metrics/BlockLength
@@ -235,8 +251,10 @@ module CocinaDisplay
235
251
  # additional types ignored here, e.g. name, uniform ...
236
252
  case structured_value["type"]&.downcase
237
253
  when "nonsorting characters"
238
- padding = non_sorting_padding(cocina_title, value)
239
- result = add_non_sorting_value(result, value, padding)
254
+ unless sortable?
255
+ padding = non_sorting_padding(cocina_title, value)
256
+ result = add_non_sorting_value(result, value, padding)
257
+ end
240
258
  when "part name", "part number"
241
259
  # even if there is a partLabel, use any existing structuredValue
242
260
  # part name/number that remains for non-digital serials purposes
@@ -286,7 +304,7 @@ module CocinaDisplay
286
304
  # rubocop:disable Metrics/PerceivedComplexity
287
305
  # rubocop:disable Metrics/AbcSize
288
306
  # rubocop:disable Metrics/CyclomaticComplexity
289
- def main_title_from_structured_values(cocina_title)
307
+ def main_title_from_structured_values(cocina_title, sortable: false)
290
308
  result = ""
291
309
  # combine pieces of the cocina structuredValue into a single title
292
310
  cocina_title["structuredValue"].each do |structured_value|
@@ -299,8 +317,10 @@ module CocinaDisplay
299
317
 
300
318
  case structured_value["type"]&.downcase
301
319
  when "nonsorting characters"
302
- padding = non_sorting_padding(cocina_title, value)
303
- result = add_non_sorting_value(result, value, padding)
320
+ unless sortable?
321
+ padding = non_sorting_padding(cocina_title, value)
322
+ result = add_non_sorting_value(result, value, padding)
323
+ end
304
324
  when "main title", "title"
305
325
  result = if ["'", "-"].include?(result.last)
306
326
  [result, value].join
@@ -0,0 +1,49 @@
1
+ module CocinaDisplay
2
+ # Helper methods for string formatting, etc.
3
+ module Utils
4
+ # Join non-empty values into a string using provided delimiter.
5
+ # If values already end in delimiter (ignoring whitespace), join with a space instead.
6
+ # @param values [Array<String>] The values to compact and join
7
+ # @param delimiter [String] The delimiter to use for joining, default is space
8
+ # @return [String] The compacted and joined string
9
+ def self.compact_and_join(values, delimiter: " ")
10
+ compacted_values = values.compact_blank.map(&:strip)
11
+ return compacted_values.first if compacted_values.one?
12
+
13
+ compacted_values.reduce(+"") do |result, value|
14
+ result << if value.end_with?(delimiter.strip)
15
+ value + " "
16
+ else
17
+ value + delimiter
18
+ end
19
+ end.delete_suffix(delimiter)
20
+ end
21
+
22
+ # Recursively flatten structured and parallel values in Cocina metadata.
23
+ # Returns a list of hashes representing the "leaf" nodes with +value+ key.
24
+ # @return [Array<Hash>] List of node hashes with "value" present
25
+ # @param cocina [Hash] The Cocina structured data to flatten
26
+ # @param output [Array] Used for recursion, should be empty on first call
27
+ # @example simple value
28
+ # cocina = { "value" => "John Doe", "type" => "name" }
29
+ # Utils.flatten_nested_values(cocina)
30
+ # #=> [{"value" => "John Doe", "type" => "name"}]
31
+ # @example structured values
32
+ # cocina = { "structuredValue" => [{"value" => "foo"}, {"value" => "bar"}] }
33
+ # Utils.flatten_nested_values(cocina)
34
+ # #=> [{"value" => "foo"}, {"value" => "bar"}]
35
+ # @example parallel structured and simple values
36
+ # cocina = { "parallelValue" => [{"value" => "foo" }, { "structuredValue" => [{"value" => "bar"}, {"value" => "baz"}] }] }
37
+ # Utils.flatten_nested_values(cocina)
38
+ # #=> [{"value" => "foo"}, {"value" => "foo"}, {"value" => "baz"}]
39
+ def self.flatten_nested_values(cocina, output = [])
40
+ return [cocina] if cocina["value"].present?
41
+ return cocina.flat_map { |node| flatten_nested_values(node, output) } if cocina.is_a?(Array)
42
+
43
+ nested_values = Array(cocina["structuredValue"]) + Array(cocina["parallelValue"])
44
+ return output unless nested_values.any?
45
+
46
+ nested_values.flat_map { |node| flatten_nested_values(node, output) }
47
+ end
48
+ end
49
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # :nodoc:
4
4
  module CocinaDisplay
5
- VERSION = "0.3.0" # :nodoc:
5
+ VERSION = "0.5.0" # :nodoc:
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cocina_display
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Budak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
11
+ date: 2025-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: janeway-jsonpath
@@ -28,22 +28,16 @@ dependencies:
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '8.0'
34
31
  - - ">="
35
32
  - !ruby/object:Gem::Version
36
- version: 8.0.2
33
+ version: '7'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
- - - "~>"
42
- - !ruby/object:Gem::Version
43
- version: '8.0'
44
38
  - - ">="
45
39
  - !ruby/object:Gem::Version
46
- version: 8.0.2
40
+ version: '7'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: edtf
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -176,12 +170,20 @@ files:
176
170
  - Rakefile
177
171
  - lib/cocina_display.rb
178
172
  - lib/cocina_display/cocina_record.rb
173
+ - lib/cocina_display/concerns/access.rb
174
+ - lib/cocina_display/concerns/contributors.rb
179
175
  - lib/cocina_display/concerns/events.rb
176
+ - lib/cocina_display/concerns/identifiers.rb
177
+ - lib/cocina_display/concerns/subjects.rb
178
+ - lib/cocina_display/concerns/titles.rb
179
+ - lib/cocina_display/contributor.rb
180
180
  - lib/cocina_display/dates/date.rb
181
181
  - lib/cocina_display/dates/date_range.rb
182
182
  - lib/cocina_display/imprint.rb
183
183
  - lib/cocina_display/marc_country_codes.rb
184
+ - lib/cocina_display/subject.rb
184
185
  - lib/cocina_display/title_builder.rb
186
+ - lib/cocina_display/utils.rb
185
187
  - lib/cocina_display/version.rb
186
188
  - sig/cocina_display.rbs
187
189
  homepage: https://sul-dlss.github.io/cocina_display/