cocina_display 1.1.3 → 1.2.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.standard.yml +1 -1
  4. data/README.md +21 -2
  5. data/config/i18n-tasks.yml +0 -0
  6. data/config/licenses.yml +59 -0
  7. data/config/locales/en.yml +109 -0
  8. data/lib/cocina_display/cocina_record.rb +27 -63
  9. data/lib/cocina_display/concerns/accesses.rb +78 -0
  10. data/lib/cocina_display/concerns/contributors.rb +32 -11
  11. data/lib/cocina_display/concerns/events.rb +19 -6
  12. data/lib/cocina_display/concerns/forms.rb +98 -11
  13. data/lib/cocina_display/concerns/geospatial.rb +9 -5
  14. data/lib/cocina_display/concerns/identifiers.rb +15 -4
  15. data/lib/cocina_display/concerns/languages.rb +6 -2
  16. data/lib/cocina_display/concerns/notes.rb +36 -0
  17. data/lib/cocina_display/concerns/related_resources.rb +20 -0
  18. data/lib/cocina_display/concerns/subjects.rb +25 -8
  19. data/lib/cocina_display/concerns/titles.rb +67 -25
  20. data/lib/cocina_display/concerns/{access.rb → url_helpers.rb} +3 -3
  21. data/lib/cocina_display/concerns.rb +6 -0
  22. data/lib/cocina_display/contributors/contributor.rb +47 -26
  23. data/lib/cocina_display/contributors/name.rb +18 -14
  24. data/lib/cocina_display/contributors/role.rb +20 -13
  25. data/lib/cocina_display/dates/date.rb +55 -14
  26. data/lib/cocina_display/dates/date_range.rb +0 -2
  27. data/lib/cocina_display/description/access.rb +41 -0
  28. data/lib/cocina_display/description/access_contact.rb +11 -0
  29. data/lib/cocina_display/description/url.rb +17 -0
  30. data/lib/cocina_display/display_data.rb +104 -0
  31. data/lib/cocina_display/events/event.rb +8 -4
  32. data/lib/cocina_display/events/imprint.rb +0 -10
  33. data/lib/cocina_display/events/location.rb +0 -2
  34. data/lib/cocina_display/events/note.rb +33 -0
  35. data/lib/cocina_display/forms/form.rb +71 -0
  36. data/lib/cocina_display/forms/genre.rb +12 -0
  37. data/lib/cocina_display/forms/resource_type.rb +38 -0
  38. data/lib/cocina_display/geospatial.rb +1 -1
  39. data/lib/cocina_display/identifier.rb +101 -0
  40. data/lib/cocina_display/json_backed_record.rb +27 -0
  41. data/lib/cocina_display/language.rb +9 -11
  42. data/lib/cocina_display/license.rb +32 -0
  43. data/lib/cocina_display/note.rb +103 -0
  44. data/lib/cocina_display/related_resource.rb +74 -0
  45. data/lib/cocina_display/subjects/subject.rb +32 -9
  46. data/lib/cocina_display/subjects/subject_value.rb +34 -16
  47. data/lib/cocina_display/title.rb +194 -0
  48. data/lib/cocina_display/utils.rb +4 -4
  49. data/lib/cocina_display/version.rb +1 -1
  50. data/lib/cocina_display.rb +30 -2
  51. metadata +45 -11
  52. data/lib/cocina_display/title_builder.rb +0 -397
  53. /data/lib/cocina_display/vocabularies/{marc_country_codes.rb → marc_country.rb} +0 -0
  54. /data/lib/cocina_display/vocabularies/{marc_relator_codes.rb → marc_relator.rb} +0 -0
@@ -6,18 +6,24 @@ module CocinaDisplay
6
6
  module Forms
7
7
  # Resource types of the object, expressed in SearchWorks controlled vocabulary.
8
8
  # @return [Array<String>]
9
- def resource_types
9
+ def searchworks_resource_types
10
10
  mapped_values = resource_type_values.flat_map { |resource_type| searchworks_resource_type(resource_type) }
11
11
  mapped_values << "Dataset" if dataset?
12
12
  mapped_values.uniq
13
13
  end
14
14
 
15
+ # Resource types of the object, expressed in SearchWorks controlled vocabulary.
16
+ # @return [Array<String>]
17
+ def mods_resource_types
18
+ all_resource_types.select { |type| type.mods? }.map(&:to_s)
19
+ end
20
+
15
21
  # Physical or digital forms of the object.
16
22
  # @return [Array<String>]
17
23
  # @example GIS dataset (nz187ct8959)
18
24
  # record.forms #=> ["map", "optical disc", "electronic resource"]
19
25
  def forms
20
- path("$.description.form..[?@.type == 'form'].value").uniq
26
+ form_forms.map(&:to_s).compact_blank.uniq
21
27
  end
22
28
 
23
29
  # Extent of the object, such as "1 audiotape" or "1 map".
@@ -25,7 +31,7 @@ module CocinaDisplay
25
31
  # @example Oral history interview (sw705fr7011)
26
32
  # record.extents #=> ["1 audiotape", "1 transcript"]
27
33
  def extents
28
- path("$.description.form..[?@.type == 'extent'].value").uniq
34
+ extent_forms.map(&:to_s).compact_blank.uniq
29
35
  end
30
36
 
31
37
  # Genres of the object, capitalized for display.
@@ -33,7 +39,7 @@ module CocinaDisplay
33
39
  # @example GIS dataset (nz187ct8959)
34
40
  # record.genres #=> ["Cartographic dataset", "Geospatial data", "Geographic information systems data"]
35
41
  def genres
36
- path("$.description.form..[?@.type == 'genre'].value").map(&:upcase_first).uniq
42
+ genre_forms.map(&:to_s).compact_blank.uniq
37
43
  end
38
44
 
39
45
  # Genres of the object, with additional values added for search/faceting.
@@ -47,6 +53,34 @@ module CocinaDisplay
47
53
  end.uniq
48
54
  end
49
55
 
56
+ # All form-related data to be rendered for display.
57
+ # Includes form, extent, resource type, etc. (but not self-deposit resource types).
58
+ # @return [Array<DisplayData>]
59
+ def form_display_data
60
+ CocinaDisplay::DisplayData.from_objects(all_forms - genre_forms - map_forms - media_forms - self_deposit_resource_types)
61
+ end
62
+
63
+ # All genre-related data to be rendered for display.
64
+ # Includes both form genres, subject genres, and self-deposit resource types.
65
+ # @return [Array<DisplayData>]
66
+ def genre_display_data
67
+ CocinaDisplay::DisplayData.from_objects(genre_forms + genre_subjects + self_deposit_resource_types)
68
+ end
69
+
70
+ # All map-related data to be rendered for display.
71
+ # Includes map scale, projection info, and geographic coordinate subjects.
72
+ # @return [Array<DisplayData>]
73
+ def map_display_data
74
+ CocinaDisplay::DisplayData.from_objects(map_forms + coordinate_subjects)
75
+ end
76
+
77
+ # All form notes to be rendered for display.
78
+ # @return [Array<DisplayData>]
79
+ def form_note_display_data
80
+ CocinaDisplay::DisplayData.from_cocina(path("$.description.form[*].note[*]"),
81
+ label: I18n.t("cocina_display.field_label.form.note"))
82
+ end
83
+
50
84
  # Is the object a periodical or serial?
51
85
  # @return [Boolean]
52
86
  def periodical?
@@ -73,6 +107,46 @@ module CocinaDisplay
73
107
 
74
108
  private
75
109
 
110
+ # Collapses all nested form values into an array of {Form} objects.
111
+ # Preserves resource type without flattening, since it can be structured.
112
+ # @return [Array<Form>]
113
+ def all_forms
114
+ @all_forms ||= path("$.description.form.*")
115
+ .flat_map { |form| Utils.flatten_nested_values(form, atomic_types: ["resource type"]) }
116
+ .map { |form| CocinaDisplay::Forms::Form.from_cocina(form) }
117
+ end
118
+
119
+ # {Form} objects with type "form".
120
+ # @return [Array<Form>]
121
+ def form_forms
122
+ all_forms.filter { |form| form.type == "form" }
123
+ end
124
+
125
+ # {Form} objects with type "genre".
126
+ # @return [Array<Form>]
127
+ def genre_forms
128
+ all_forms.filter { |form| form.type == "genre" }
129
+ end
130
+
131
+ # {Form} objects with type "extent".
132
+ # @return [Array<Form>]
133
+ def extent_forms
134
+ all_forms.filter { |form| form.type == "extent" }
135
+ end
136
+
137
+ # {Form} objects with types related to map data.
138
+ # @return [Array<Form>]
139
+ def map_forms
140
+ all_forms.filter { |form| ["map scale", "map projection"].include?(form.type) }
141
+ end
142
+
143
+ # {Form} objects with types that are media-related.
144
+ # @note These are excluded from the general form display data.
145
+ # @return [Array<Form>]
146
+ def media_forms
147
+ all_forms.filter { |form| ["reformatting quality", "media type"].include?(form.type) }
148
+ end
149
+
76
150
  # Map a resource type to SearchWorks format value(s).
77
151
  # @param resource_type [String] The resource type to map.
78
152
  # @return [Array<String>]
@@ -113,21 +187,34 @@ module CocinaDisplay
113
187
  values.compact_blank
114
188
  end
115
189
 
190
+ # All resource types forms, as {ResourceType}s.
191
+ # @return [Array<ResourceType>]
192
+ def all_resource_types
193
+ all_forms.filter { |form| form.is_a?(CocinaDisplay::Forms::ResourceType) }
194
+ end
195
+
196
+ # {ResourceType} objects that are Stanford self-deposit resource types.
197
+ # @return [Array<ResourceType>]
198
+ def self_deposit_resource_types
199
+ all_resource_types.filter { |resource_type| resource_type.stanford_self_deposit? }
200
+ end
201
+
202
+ # Display values of all resource types.
203
+ # @return [Array<String>]
204
+ def resource_type_values
205
+ all_resource_types.map(&:to_s).uniq
206
+ end
207
+
116
208
  # Issuance terms for a work, drawn from the event notes.
117
209
  # @return [Array<String>]
118
210
  def issuance_terms
119
- path("$.description.event.*.note[?@.type == 'issuance'].value").map(&:downcase).uniq
211
+ events.flat_map(&:notes).filter { |note| note.type == "issuance" }.map { |note| note.to_s.downcase }.uniq
120
212
  end
121
213
 
122
214
  # Frequency terms for a periodical, drawn from the event notes.
123
215
  # @return [Array<String>]
124
216
  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
217
+ events.flat_map(&:notes).filter { |note| note.type == "frequency" }.map { |note| note.to_s.downcase }.uniq
131
218
  end
132
219
  end
133
220
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../subjects/subject_value"
4
-
5
3
  module CocinaDisplay
6
4
  module Concerns
7
5
  # Methods for extracting geospatial metadata, such as coordinates.
@@ -10,7 +8,7 @@ module CocinaDisplay
10
8
  # @return [Array<String>]
11
9
  # @example ["34°03′08″N 118°14′37″W"]
12
10
  def coordinates
13
- coordinate_subjects.map(&:to_s).compact.uniq
11
+ coordinate_subject_values.map(&:to_s).compact.uniq
14
12
  end
15
13
 
16
14
  # All valid coordinate data formatted for indexing into a Solr RPT field.
@@ -49,15 +47,21 @@ module CocinaDisplay
49
47
 
50
48
  private
51
49
 
50
+ # {Subject} objects with types that could contain coordinate information.
51
+ # @return [Array<Subject>]
52
+ def coordinate_subjects
53
+ all_subjects.filter { |subject| subject.type&.include? "coordinates" }
54
+ end
55
+
52
56
  # Parsed coordinate values from the coordinate subject values.
53
57
  # @return [Array<Geospatial::Coordinates>]
54
58
  def coordinate_objects
55
- coordinate_subjects.filter_map(&:coordinates)
59
+ coordinate_subject_values.filter_map(&:coordinates)
56
60
  end
57
61
 
58
62
  # All subject values that could contain parsed coordinates.
59
63
  # @return [Array<Subjects::CoordinatesSubjectValue>]
60
- def coordinate_subjects
64
+ def coordinate_subject_values
61
65
  subject_values.filter { |s| s.is_a? CocinaDisplay::Subjects::CoordinatesSubjectValue }
62
66
  end
63
67
  end
@@ -26,11 +26,10 @@ module CocinaDisplay
26
26
  # @example
27
27
  # record.doi #=> "10.25740/ppax-bf07"
28
28
  def doi
29
- doi_id = path("$.identification.doi").first ||
30
- path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
31
- path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
29
+ doi_id = path("$.identification.doi").first
30
+ return URI(doi_id).path.delete_prefix("/") if doi_id.present?
32
31
 
33
- URI(doi_id).path.delete_prefix("/") if doi_id.present?
32
+ identifiers.find(&:doi?)&.identifier
34
33
  end
35
34
 
36
35
  # The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
@@ -66,6 +65,18 @@ module CocinaDisplay
66
65
  def searchworks_id
67
66
  folio_hrid || bare_druid
68
67
  end
68
+
69
+ # Identifier objects extracted from the Cocina descriptive metadata.
70
+ # @return [Array<Identifier>]
71
+ def identifiers
72
+ @identifiers ||= path("$.description.identifier[*]").map { |id| Identifier.new(id) }
73
+ end
74
+
75
+ # Labelled display data for identifiers.
76
+ # @return [Array<DisplayData>]
77
+ def identifier_display_data
78
+ CocinaDisplay::DisplayData.from_objects(identifiers)
79
+ end
69
80
  end
70
81
  end
71
82
  end
@@ -1,5 +1,3 @@
1
- require_relative "../language"
2
-
3
1
  module CocinaDisplay
4
2
  module Concerns
5
3
  # Methods for extracting language information from a Cocina object.
@@ -15,6 +13,12 @@ module CocinaDisplay
15
13
  def searchworks_language_names
16
14
  languages.filter_map { |lang| lang.to_s if lang.searchworks_language? }.compact_blank.uniq
17
15
  end
16
+
17
+ # Language information for display.
18
+ # @return [Array<CocinaDisplay::DisplayData>]
19
+ def language_display_data
20
+ CocinaDisplay::DisplayData.from_objects(languages)
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -0,0 +1,36 @@
1
+ module CocinaDisplay
2
+ module Concerns
3
+ # Methods for extracting note information from Cocina.
4
+ module Notes
5
+ # Note objects associated with the cocina record.
6
+ # @return [Array<CocinaDisplay::Note>]
7
+ def notes
8
+ @notes ||= path("$.description.note.*").map { |note| CocinaDisplay::Note.new(note) }
9
+ end
10
+
11
+ # Abstract metadata for display.
12
+ # @return [Array<CocinaDisplay::DisplayData>]
13
+ def abstract_display_data
14
+ CocinaDisplay::DisplayData.from_objects(notes.select(&:abstract?))
15
+ end
16
+
17
+ # General note metadata for display.
18
+ # @return [Array<CocinaDisplay::DisplayData>]
19
+ def general_note_display_data
20
+ CocinaDisplay::DisplayData.from_objects(notes.select(&:general_note?))
21
+ end
22
+
23
+ # Preferred citation metadata for display.
24
+ # @return [Array<CocinaDisplay::DisplayData>]
25
+ def preferred_citation_display_data
26
+ CocinaDisplay::DisplayData.from_objects(notes.select(&:preferred_citation?))
27
+ end
28
+
29
+ # Table of contents metadata for display.
30
+ # @return [Array<CocinaDisplay::DisplayData>]
31
+ def table_of_contents_display_data
32
+ CocinaDisplay::DisplayData.from_objects(notes.select(&:table_of_contents?))
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ module CocinaDisplay
2
+ module Concerns
3
+ # Methods for extracting and formatting related resources from Cocina records.
4
+ module RelatedResources
5
+ # Resources related to the object.
6
+ # @return [Array<CocinaDisplay::RelatedResource>]
7
+ def related_resources
8
+ @related_resources ||= path("$.description.relatedResource[*]").map { |res| RelatedResource.new(res) }
9
+ end
10
+
11
+ # Display data for related resources.
12
+ # @note Related resources also have their own nested display data.
13
+ # @see CocinaDisplay::RelatedResource#display_data
14
+ # @return [Array<DisplayData>]
15
+ def related_resource_display_data
16
+ DisplayData.from_objects(related_resources)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,6 +1,3 @@
1
- require_relative "../subjects/subject"
2
- require_relative "../subjects/subject_value"
3
-
4
1
  module CocinaDisplay
5
2
  module Concerns
6
3
  # Methods for extracting and formatting subject information.
@@ -87,25 +84,45 @@ module CocinaDisplay
87
84
  # @see Subject#to_s
88
85
  # @return [Array<String>]
89
86
  def subject_all_display
90
- subjects.map(&:to_s).uniq
87
+ all_subjects.map(&:to_s).uniq
88
+ end
89
+
90
+ # Subject data to be rendered for display.
91
+ # Uses the concatenated form for structured subject values.
92
+ # @see Subject#to_s
93
+ # @return [Array<DisplayData>]
94
+ def subject_display_data
95
+ CocinaDisplay::DisplayData.from_objects(all_subjects - classification_subjects - genre_subjects - coordinate_subjects)
91
96
  end
92
97
 
93
98
  private
94
99
 
95
- # All subjects, accessible as Subject objects.
100
+ # All subjects, accessible as {Subject} objects.
96
101
  # Checks both description.subject and description.geographic.subject.
97
102
  # @return [Array<Subject>]
98
- def subjects
99
- @subjects ||= Enumerator::Chain.new(
103
+ def all_subjects
104
+ @all_subjects ||= Enumerator::Chain.new(
100
105
  path("$.description.subject[*]"),
101
106
  path("$.description.geographic.*.subject[*]")
102
107
  ).map { |s| CocinaDisplay::Subjects::Subject.new(s) }
103
108
  end
104
109
 
110
+ # {Subject} objects with type "genre".
111
+ # @return [Array<Subject>]
112
+ def genre_subjects
113
+ all_subjects.filter { |subject| subject.type == "genre" }
114
+ end
115
+
116
+ # {Subject} objects with type "classification".
117
+ # @return [Array<Subject>]
118
+ def classification_subjects
119
+ all_subjects.filter { |subject| subject.type == "classification" }
120
+ end
121
+
105
122
  # All subject values, flattened from all subjects.
106
123
  # @return [Array<SubjectValue>]
107
124
  def subject_values
108
- @subject_values ||= subjects.flat_map(&:subject_values)
125
+ @subject_values ||= all_subjects.flat_map(&:subject_values)
109
126
  end
110
127
 
111
128
  # All subject values that are named places.
@@ -1,48 +1,77 @@
1
- require_relative "../title_builder"
2
-
3
1
  module CocinaDisplay
4
2
  module Concerns
5
3
  # Methods for finding and formatting titles.
6
4
  module Titles
7
5
  # 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]
6
+ # If there are multiple primary titles, uses the first.
7
+ # @see CocinaDisplay::Title#main_title
8
+ # @return [String, nil]
12
9
  def main_title
13
- CocinaDisplay::TitleBuilder.main_title(cocina_titles).first
10
+ primary_title&.short_title
14
11
  end
15
12
 
16
13
  # 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]
14
+ # If there are multiple primary titles, uses the first.
15
+ # @see CocinaDisplay::Title#full_title
16
+ # @return [String, nil]
21
17
  def full_title
22
- CocinaDisplay::TitleBuilder.full_title(cocina_titles, catalog_links: catalog_links).first
18
+ primary_title&.full_title
23
19
  end
24
20
 
25
21
  # 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]
22
+ # If there are multiple primary titles, uses the first.
23
+ # @see CocinaDisplay::Title#display_title
24
+ # @return [String, nil]
29
25
  def display_title
30
- CocinaDisplay::TitleBuilder.build(cocina_titles, catalog_links: catalog_links)
26
+ primary_title&.display_title
27
+ end
28
+
29
+ # A string value for sorting by title that sorts missing values last.
30
+ # If there are multiple primary titles, uses the first.
31
+ # @see CocinaDisplay::Title#sort_title
32
+ # @return [String]
33
+ def sort_title
34
+ primary_title&.sort_title || "\u{10FFFF}"
31
35
  end
32
36
 
33
- # Any additional titles for the object excluding the main title.
37
+ # Any additional titles for the object excluding the primary title.
34
38
  # @return [Array<String>]
35
- # @see CocinaDisplay::TitleBuilder#additional_titles
39
+ # @see CocinaDisplay::Title#display_title
36
40
  def additional_titles
37
- CocinaDisplay::TitleBuilder.additional_titles(cocina_titles)
41
+ secondary_titles.map(&:display_title).compact_blank
38
42
  end
39
43
 
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}"
44
+ # All {Title} objects, grouped by their label for display.
45
+ # @note All primary titles are included under "Title", not just the first.
46
+ # @return [Array<DisplayData>]
47
+ def title_display_data
48
+ DisplayData.from_objects(all_titles)
49
+ end
50
+
51
+ # The first title marked primary, or the first without a type.
52
+ # @return [Array<Title>]
53
+ def primary_title
54
+ all_titles.find { |title| title.primary? }.presence || all_titles.find { |title| !title.type? }
55
+ end
56
+
57
+ # All titles except the primary title.
58
+ # @return [Array<Title>]
59
+ def secondary_titles
60
+ all_titles - [primary_title]
61
+ end
62
+
63
+ # All {Title} objects built from the Cocina titles.
64
+ # Flattens parallel values into separate titles.
65
+ # @return [Array<Title>]
66
+ def all_titles
67
+ @all_titles ||= cocina_titles.flat_map do |cocina_title|
68
+ (Array(cocina_title["parallelValue"]).presence || [cocina_title]).map do |value|
69
+ Title.new(value, part_label: part_label, part_numbers: part_numbers).tap do |title|
70
+ title.type ||= cocina_title["type"]
71
+ title.status ||= cocina_title["status"]
72
+ end
73
+ end
74
+ end
46
75
  end
47
76
 
48
77
  private
@@ -59,6 +88,19 @@ module CocinaDisplay
59
88
  def catalog_links
60
89
  @catalog_links ||= Array(cocina_doc.dig("identification", "catalogLinks"))
61
90
  end
91
+
92
+ # Part label for digital serials display from FOLIO, if any.
93
+ # @return [String, nil]
94
+ def part_label
95
+ catalog_links.find { |link| link["catalog"] == "folio" }&.fetch("partLabel", nil)
96
+ end
97
+
98
+ # Part numbers from notes, if any.
99
+ # @note This is used for RelatedResource title display.
100
+ # @return [Array<String>]
101
+ def part_numbers
102
+ notes.filter(&:part?).flat_map { |note| note.values_by_type["number"] }.compact_blank
103
+ end
62
104
  end
63
105
  end
64
106
  end
@@ -1,13 +1,13 @@
1
1
  module CocinaDisplay
2
2
  module Concerns
3
3
  # Methods that generate URLs to access an object.
4
- module Access
4
+ module UrlHelpers
5
5
  # The PURL URL for this object.
6
6
  # @return [String]
7
7
  # @example
8
8
  # record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
9
9
  def purl_url
10
- cocina_doc.dig("description", "purl") || ("https://purl.stanford.edu/#{bare_druid}" if bare_druid)
10
+ cocina_doc.dig("description", "purl")
11
11
  end
12
12
 
13
13
  # The oEmbed URL for the object, optionally with additional parameters.
@@ -18,7 +18,7 @@ module CocinaDisplay
18
18
  # @example Generate an oEmbed URL for the viewer and hide the title
19
19
  # record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
20
20
  def oembed_url(params: {})
21
- return if collection? || purl_url.blank?
21
+ return if (!is_a?(CocinaDisplay::RelatedResource) && collection?) || purl_url.blank?
22
22
 
23
23
  params[:url] ||= purl_url
24
24
  "#{purl_base_url}/embed.json?#{params.to_query}"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ module Concerns
5
+ end
6
+ end
@@ -1,13 +1,5 @@
1
1
  # frozen_string_literal: true
2
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
- require_relative "name"
9
- require_relative "role"
10
-
11
3
  module CocinaDisplay
12
4
  module Contributors
13
5
  # A contributor to a work, such as an author or publisher.
@@ -20,11 +12,22 @@ module CocinaDisplay
20
12
  @cocina = cocina
21
13
  end
22
14
 
23
- # String representation of the contributor, including name and role.
24
- # Used for debugging and logging.
25
- # @return [String]
15
+ # String representation of the contributor, using display name with date.
16
+ # @return [String, nil]
26
17
  def to_s
27
- Utils.compact_and_join([display_name, display_role], delimiter: ": ")
18
+ display_name(with_date: true)
19
+ end
20
+
21
+ # Support equality based on the underlying Cocina data.
22
+ # @param other [Object]
23
+ def ==(other)
24
+ other.is_a?(Contributor) && other.cocina == cocina
25
+ end
26
+
27
+ # Identifiers for the contributor.
28
+ # @return [Array<Identifier>]
29
+ def identifiers
30
+ Array(cocina["identifier"]).map { |id| Identifier.new(id) }
28
31
  end
29
32
 
30
33
  # Is this contributor a human?
@@ -75,39 +78,57 @@ module CocinaDisplay
75
78
  roles.any?
76
79
  end
77
80
 
78
- # The display name for the contributor as a string.
79
- # Uses the first name if multiple names are present.
81
+ # The primary display name for the contributor as a string.
80
82
  # @param with_date [Boolean] Include life dates, if present
81
- # @return [String]
83
+ # @return [String, nil]
82
84
  def display_name(with_date: false)
83
- names.map { |name| name.to_s(with_date: with_date) }.compact_blank.first
85
+ primary_name&.to_s(with_date: with_date)
84
86
  end
85
87
 
86
- # The full forename for the contributor from the first available name.
88
+ # String renderings of all names for the contributor.
89
+ # @param with_date [Boolean] Include life dates, if present
90
+ # @return [Array<String>]
91
+ def display_names(with_date: false)
92
+ names.map { |name| name.to_s(with_date: with_date) }.compact_blank
93
+ end
94
+
95
+ # A single primary name for the contributor.
96
+ # Prefers a name of type "display" or one marked primary.
97
+ # @return [Contributor::Name, nil]
98
+ def primary_name
99
+ names.find { |name| name.type == "display" }.presence ||
100
+ names.find(&:primary?).presence ||
101
+ names.first
102
+ end
103
+
104
+ # The forename for the contributor, if structured name info is available.
87
105
  # @see Contributor::Name::forename_str
88
106
  # @return [String, nil]
89
107
  def forename
90
108
  names.map(&:forename_str).first.presence
91
109
  end
92
110
 
93
- # The full surname for the contributor from the first available name.
111
+ # The surname for the contributor, if structured name info is available.
94
112
  # @see Contributor::Name::surname_str
95
113
  # @return [String, nil]
96
114
  def surname
97
115
  names.map(&:surname_str).first.presence
98
116
  end
99
117
 
100
- # A string representation of the contributor's roles, formatted for display.
101
- # If there are multiple roles, they are joined with commas.
102
- # @return [String]
103
- def display_role
104
- roles.map(&:to_s).to_sentence
105
- end
106
-
107
118
  # All names in the Cocina as Name objects.
119
+ # Flattens parallel values into separate Name objects.
108
120
  # @return [Array<Name>]
109
121
  def names
110
- @names ||= Array(cocina["name"]).map { |name| Name.new(name) }
122
+ @names ||= Array(cocina["name"]).flat_map do |name|
123
+ (Array(name["parallelValue"]).presence || [name]).filter_map do |name_value|
124
+ unless name_value.blank?
125
+ Name.new(name_value).tap do |name_obj|
126
+ name_obj.type ||= name["type"]
127
+ name_obj.status ||= name["status"]
128
+ end
129
+ end
130
+ end
131
+ end
111
132
  end
112
133
 
113
134
  # All roles in the Cocina structured data.