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
@@ -1,24 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../utils"
4
-
5
3
  module CocinaDisplay
6
4
  module Contributors
7
5
  # A name associated with a contributor.
8
6
  class Name
7
+ # The underlying Cocina structured data for the name.
8
+ # @return [Hash]
9
9
  attr_reader :cocina
10
10
 
11
+ # The type of name, if any (e.g., "personal", "corporate", etc.).
12
+ # @return [String, nil]
13
+ attr_accessor :type
14
+
15
+ # The status of the name, if any (e.g., "primary").
16
+ # @return [String, nil]
17
+ attr_accessor :status
18
+
11
19
  # Initialize a Name object with Cocina structured data.
12
20
  # @param cocina [Hash] The Cocina structured data for the name.
13
21
  def initialize(cocina)
14
22
  @cocina = cocina
23
+ @type = cocina["type"]
24
+ @status = cocina["status"]
15
25
  end
16
26
 
17
27
  # The display string for the name, optionally including life dates.
18
- # Uses these values in order, if present:
19
- # 1. Unstructured value
20
- # 2. Any structured/parallel values marked as "display"
21
- # 3. Joined structured values, optionally with life dates
22
28
  # @param with_date [Boolean] Include life dates, if present
23
29
  # @return [String]
24
30
  # @example no dates
@@ -28,8 +34,6 @@ module CocinaDisplay
28
34
  def to_s(with_date: false)
29
35
  if cocina["value"].present?
30
36
  cocina["value"]
31
- elsif display_name_str.present?
32
- display_name_str
33
37
  elsif dates_str.present? && with_date
34
38
  Utils.compact_and_join([full_name_str, dates_str], delimiter: ", ")
35
39
  else
@@ -43,12 +47,6 @@ module CocinaDisplay
43
47
  Utils.compact_and_join(name_components.push(terms_of_address_str), delimiter: ", ")
44
48
  end
45
49
 
46
- # Flattened form of any names explicitly marked as "display name".
47
- # @return [String]
48
- def display_name_str
49
- Utils.compact_and_join(Array(name_values["display"]), delimiter: ", ")
50
- end
51
-
52
50
  # List of all name components.
53
51
  # If any of forename, surname, or term of address are present, those are used.
54
52
  # Otherwise, fall back to any names explicitly marked as "name" or untyped.
@@ -81,6 +79,12 @@ module CocinaDisplay
81
79
  Utils.compact_and_join(Array(name_values["life dates"]) + Array(name_values["activity dates"]), delimiter: ", ")
82
80
  end
83
81
 
82
+ # Is this a primary name for the contributor?
83
+ # @return [Boolean]
84
+ def primary?
85
+ status == "primary"
86
+ end
87
+
84
88
  private
85
89
 
86
90
  # A hash mapping destructured name types to their values.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../vocabularies/marc_relator_codes"
4
-
5
3
  module CocinaDisplay
6
4
  module Contributors
7
5
  # A role associated with a contributor.
@@ -16,37 +14,46 @@ module CocinaDisplay
16
14
 
17
15
  # The name of the role.
18
16
  # Translates the MARC relator code if no value was present.
19
- # @return [String, nil]
17
+ # @return [String, nil] A nil role is typically displayed in the UI as an "Associated with" relationship
20
18
  def to_s
21
- cocina["value"] || (Vocabularies::MARC_RELATOR[code] if marc_relator?)
22
- end
23
-
24
- # A code associated with the role, e.g. a MARC relator code.
25
- # @return [String, nil]
26
- def code
27
- cocina["code"]
19
+ cocina.fetch("value") { marc_value }
28
20
  end
29
21
 
30
22
  # Does this role indicate the contributor is an author?
31
23
  # @return [Boolean]
32
24
  def author?
33
- to_s =~ /^(author|creator|primary investigator)/i
25
+ /^(author|creator|primary investigator)/i.match? to_s
34
26
  end
35
27
 
36
28
  # Does this role indicate the contributor is a publisher?
37
29
  # @return [Boolean]
38
30
  def publisher?
39
- to_s =~ /^publisher/i
31
+ /^publisher/i.match? to_s
40
32
  end
41
33
 
42
34
  # Does this role indicate the contributor is a funder?
43
35
  # @return [Boolean]
44
36
  def funder?
45
- to_s =~ /^funder/i
37
+ /^funder/i.match? to_s
46
38
  end
47
39
 
48
40
  private
49
41
 
42
+ # The name of the MARC relator role
43
+ # @raises [KeyError] if the role is not valid
44
+ # @return [String, nil]
45
+ def marc_value
46
+ return unless marc_relator?
47
+
48
+ Vocabularies::MARC_RELATOR.fetch(code)
49
+ end
50
+
51
+ # A code associated with the role, e.g. a MARC relator code.
52
+ # @return [String, nil]
53
+ def code
54
+ cocina["code"]
55
+ end
56
+
50
57
  # Does this role have a MARC relator code?
51
58
  # @return [Boolean]
52
59
  def marc_relator?
@@ -11,6 +11,10 @@ module CocinaDisplay
11
11
  # List of values that we shouldn't even attempt to parse.
12
12
  UNPARSABLE_VALUES = ["0000-00-00", "9999", "uuuu", "[uuuu]"].freeze
13
13
 
14
+ def self.notifier
15
+ CocinaDisplay.notifier
16
+ end
17
+
14
18
  # Construct a Date from parsed Cocina data.
15
19
  # @param cocina [Hash] Cocina date data
16
20
  # @return [CocinaDisplay::Date]
@@ -75,6 +79,11 @@ module CocinaDisplay
75
79
  # @param value [String] the date value to modify
76
80
  # @return [String]
77
81
  def self.normalize_to_edtf(value)
82
+ unless value
83
+ notifier&.notify("Invalid date value: #{value}")
84
+ return
85
+ end
86
+
78
87
  sanitized = value.gsub(/^[\[]+/, "").gsub(/[\.\]]+$/, "")
79
88
  sanitized = value.rjust(4, "0") if /^\d{3}$/.match?(value)
80
89
 
@@ -110,6 +119,18 @@ module CocinaDisplay
110
119
  cocina["value"]
111
120
  end
112
121
 
122
+ # The string representation of the date for display.
123
+ # @return [String]
124
+ def to_s
125
+ qualified_value
126
+ end
127
+
128
+ # Label used to group the date for display.
129
+ # @return [String]
130
+ def label
131
+ cocina["displayLabel"].presence || type_label
132
+ end
133
+
113
134
  # The qualifier for this date, if any, such as "approximate", "inferred", etc.
114
135
  # @return [String, nil]
115
136
  def qualifier
@@ -183,14 +204,18 @@ module CocinaDisplay
183
204
  # @return [Symbol] :year, :month, :day, :decade, :century, or :unknown
184
205
  def precision
185
206
  return :unknown unless date_range || date
186
-
187
207
  if date_range.is_a? EDTF::Century
188
- :century
208
+ return :century
189
209
  elsif date_range.is_a? EDTF::Decade
190
- :decade
191
- elsif date.is_a? EDTF::Season
210
+ return :decade
211
+ end
212
+
213
+ case date
214
+ when EDTF::Season
192
215
  :month
193
- elsif date.is_a? EDTF::Interval
216
+ when EDTF::Unknown
217
+ :unknown
218
+ when EDTF::Interval
194
219
  date.precision
195
220
  else
196
221
  case date.precision
@@ -265,11 +290,11 @@ module CocinaDisplay
265
290
 
266
291
  # Decoded version of the date with "BCE" or "CE". Strips leading zeroes.
267
292
  # @param allowed_precisions [Array<Symbol>] List of allowed precisions for the output.
268
- # Defaults to [:day, :month, :year, :decade, :century].
293
+ # Defaults to [:day, :month, :year, :decade, :century, :unknown].
269
294
  # @param ignore_unparseable [Boolean] Return nil instead of the original value if it couldn't be parsed
270
295
  # @param display_original_value [Boolean] Return the original value if it was not encoded
271
296
  # @return [String]
272
- def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century], ignore_unparseable: false, display_original_value: true)
297
+ def decoded_value(allowed_precisions: [:day, :month, :year, :decade, :century, :unknown], ignore_unparseable: false, display_original_value: true)
273
298
  return if ignore_unparseable && !parsed_date?
274
299
  return value.strip unless parsed_date?
275
300
 
@@ -340,12 +365,13 @@ module CocinaDisplay
340
365
  # @param date [Date] The date to format.
341
366
  # @param precision [Symbol] The precision to format the date at, e.g. :month
342
367
  # @param allowed_precisions [Array<Symbol>] List of allowed precisions for the output.
343
- # Options are [:day, :month, :year, :decade, :century].
368
+ # Options are [:day, :month, :year, :decade, :century, :unknown].
344
369
  # @note allowed_precisions should be ordered by granularity, with most specific first.
345
370
  def format_date(date, precision, allowed_precisions)
346
371
  precision = allowed_precisions.first unless allowed_precisions.include?(precision)
347
-
348
372
  case precision
373
+ when :unknown
374
+ "Unknown"
349
375
  when :day
350
376
  date.strftime("%B %e, %Y")
351
377
  when :month
@@ -378,8 +404,6 @@ module CocinaDisplay
378
404
  return nil if date.nil?
379
405
 
380
406
  case date_range
381
- when EDTF::Unknown
382
- nil
383
407
  when EDTF::Epoch, EDTF::Interval, EDTF::Season
384
408
  date_range.min
385
409
  when EDTF::Set
@@ -400,8 +424,6 @@ module CocinaDisplay
400
424
  return nil if date.nil?
401
425
 
402
426
  case date_range
403
- when EDTF::Unknown
404
- nil
405
427
  when EDTF::Epoch, EDTF::Interval, EDTF::Season
406
428
  date_range.max
407
429
  when EDTF::Set
@@ -439,18 +461,37 @@ module CocinaDisplay
439
461
  [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
440
462
  end
441
463
  end
464
+
465
+ # Label for the date based on its type.
466
+ # @example "publication" becomes "Publication date"
467
+ # @return [String]
468
+ def type_label
469
+ I18n.t(
470
+ type || "untyped",
471
+ scope: "cocina_display.field_label.event.date",
472
+ type: type&.capitalize,
473
+ default: [:default]
474
+ )
475
+ end
442
476
  end
443
477
 
444
478
  # Strict ISO8601-encoded date parser.
445
479
  class Iso8601Format < Date
446
480
  def self.parse_date(value)
447
481
  ::Date.parse(normalize_to_edtf(value))
482
+ rescue ::Date::Error
483
+ notifier&.notify("Invalid date value \"#{value}\" for iso8601 encoding")
484
+ nil
448
485
  end
449
486
  end
450
487
 
451
488
  # Less strict W3CDTF-encoded date parser.
452
489
  class W3cdtfFormat < Date
453
490
  def self.normalize_to_edtf(value)
491
+ unless value
492
+ notifier&.notify("Invalid date value: #{value}")
493
+ return
494
+ end
454
495
  super.gsub("-00", "")
455
496
  end
456
497
  end
@@ -503,7 +544,7 @@ module CocinaDisplay
503
544
  # Base class for date formats that match using a regex.
504
545
  class ExtractorDateFormat < Date
505
546
  def self.supports?(value)
506
- value.match self::REGEX
547
+ self::REGEX.match?(value)
507
548
  end
508
549
  end
509
550
 
@@ -1,5 +1,3 @@
1
- require_relative "date"
2
-
3
1
  module CocinaDisplay
4
2
  module Dates
5
3
  # A date range parsed from Cocina structuredValues.
@@ -0,0 +1,41 @@
1
+ module CocinaDisplay
2
+ module Description
3
+ # Access information for a Cocina object.
4
+ class Access
5
+ attr_reader :cocina
6
+
7
+ # Create an Access object from Cocina structured data.
8
+ # @param cocina [Hash]
9
+ def initialize(cocina)
10
+ @cocina = cocina
11
+ end
12
+
13
+ # String representation of the access metadata.
14
+ # @return [String, nil]
15
+ def to_s
16
+ cocina["value"].presence
17
+ end
18
+
19
+ # The type of the access metadata, e.g. "repository".
20
+ # @return [String, nil]
21
+ def type
22
+ cocina["type"].presence
23
+ end
24
+
25
+ # The display label for the access metadata.
26
+ # @return [String]
27
+ def label
28
+ cocina["displayLabel"].presence ||
29
+ I18n.t(type&.parameterize&.underscore, default: :access, scope: "cocina_display.field_label.access")
30
+ end
31
+
32
+ # Whether the access info is a contact email.
33
+ # Always false, see CocinaDisplay::Description::AccessContact
34
+ # for cases when this is true
35
+ # @return [Boolean]
36
+ def contact_email?
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module CocinaDisplay
2
+ module Description
3
+ class AccessContact < Access
4
+ # Whether the access contact info is a contact email.
5
+ # @return [Boolean]
6
+ def contact_email?
7
+ type == "email"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module CocinaDisplay
2
+ module Description
3
+ class Url < Access
4
+ # The display label for the URL access metadata.
5
+ # @return [String]
6
+ def label
7
+ I18n.t(:url, scope: "cocina_display.field_label.access")
8
+ end
9
+
10
+ # The link text for the URL access metadata.
11
+ # @return [String, nil]
12
+ def link_text
13
+ cocina["displayLabel"].presence
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ # A data structure to be rendered into HTML by the consumer.
5
+ class DisplayData
6
+ class << self
7
+ # Given objects that support #to_s and #label, group them into {DisplayData}.
8
+ # Groups by each object's +label+ and keeps unique, non-blank values.
9
+ # @param objects [Array<Object>]
10
+ # @return [Array<DisplayData>]
11
+ def from_objects(objects)
12
+ objects.group_by(&:label)
13
+ .map { |label, objs| new(label: label, objects: objs) }
14
+ .reject { |data| data.values.empty? }
15
+ end
16
+
17
+ # Given an array of Cocina hashes, group them into {DisplayData}.
18
+ # Uses +label+ as the label if provided, but honors +displayLabel+ if set.
19
+ # Keeps the unique, non-blank values under each label.
20
+ # @param cocina [Array<Hash>]
21
+ # @param label [String]
22
+ # @return [Array<DisplayData>]
23
+ def from_cocina(cocina, label: nil)
24
+ from_objects(descriptive_values_from_cocina(cocina, label: label))
25
+ end
26
+
27
+ # Create display data from a string value.
28
+ # @param value [String] The string value to display
29
+ # @param label [String] The label for the display data
30
+ # @return [Array<DisplayData>] The display data
31
+ def from_string(value, label: nil)
32
+ from_objects(descriptive_values_from_string(value, label: label))
33
+ end
34
+
35
+ # Create an array containing a descriptive object from a string value.
36
+ # Can be used to combine a string derived value with other metadata objects.
37
+ # @param string [String] The string value to display
38
+ # @param label [String] The label for the display data
39
+ # @return [Array<DescriptiveValue>] The descriptive values
40
+ def descriptive_values_from_string(string, label: nil)
41
+ [DescriptiveValue.new(label: label, value: string)]
42
+ end
43
+
44
+ # Take one or several DisplayData and merge into a single hash.
45
+ # Keys are labels; values are the merged array of values for that label.
46
+ # @param display_data [DisplayData, Array<DisplayData>]
47
+ # @return [Hash{String => Array<String>}] The merged hash
48
+ def to_hash(display_data)
49
+ Array(display_data).map(&:to_h).reduce(:merge)
50
+ end
51
+
52
+ private
53
+
54
+ # Wrap Cocina nodes into {DescriptiveValue} so they are labelled.
55
+ # Uses +displayLabel+ from the node if present, otherwise uses the provided label.
56
+ # @param cocina [Array<Hash>]
57
+ # @param label [String]
58
+ # @return [Array<DescriptiveValue>]
59
+ def descriptive_values_from_cocina(cocina, label: nil)
60
+ cocina.map { |node| DescriptiveValue.new(label: node["displayLabel"] || label, value: node["value"]) }
61
+ end
62
+
63
+ # Wrapper to make Cocina descriptive values respond to #to_s and #label.
64
+ # @attr [String] label
65
+ # @attr [String] value
66
+ DescriptiveValue = Data.define(:label, :value) do
67
+ def to_s
68
+ value
69
+ end
70
+ end
71
+ end
72
+
73
+ # Create a DisplayData object from a list of objects that share a label
74
+ # @param label [String]
75
+ # @param objects [Array<Object>]
76
+ def initialize(label:, objects:)
77
+ @label = label
78
+ @objects = objects
79
+ end
80
+
81
+ attr_reader :label, :objects
82
+
83
+ # The unique, non-blank values for display
84
+ # @return [Array<String>]
85
+ def values
86
+ objects.flat_map { |object| split_string_on_newlines(object.to_s) }.compact_blank.uniq
87
+ end
88
+
89
+ # Express the display data as a hash mapping the label to its values.
90
+ # @return [Hash{String => Array<String>}] The label and values
91
+ def to_h
92
+ {label => values}
93
+ end
94
+
95
+ private
96
+
97
+ # Split a string on newlines (including HTML-encoded newlines) and strip whitespace.
98
+ # @param string [String] The string to split
99
+ # @return [Array<String>]
100
+ def split_string_on_newlines(string)
101
+ string&.gsub("&#10;", "\n")&.split("\n")&.map(&:strip)
102
+ end
103
+ end
104
+ end
@@ -1,7 +1,3 @@
1
- require_relative "location"
2
- require_relative "../dates/date"
3
- require_relative "../contributors/contributor"
4
-
5
1
  module CocinaDisplay
6
2
  module Events
7
3
  # An event associated with an object, like publication.
@@ -73,6 +69,14 @@ module CocinaDisplay
73
69
  CocinaDisplay::Events::Location.new(location)
74
70
  end
75
71
  end
72
+
73
+ # All notes associated with this event.
74
+ # @return [Array<CocinaDisplay::Events::Note>]
75
+ def notes
76
+ @notes ||= Array(cocina["note"]).map do |note|
77
+ CocinaDisplay::Events::Note.new(note)
78
+ end
79
+ end
76
80
  end
77
81
  end
78
82
  end
@@ -1,15 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "edtf"
4
- require "active_support"
5
- require "active_support/core_ext/enumerable"
6
- require "active_support/core_ext/object/blank"
7
-
8
- require_relative "event"
9
- require_relative "../utils"
10
- require_relative "../dates/date"
11
- require_relative "../dates/date_range"
12
-
13
3
  module CocinaDisplay
14
4
  module Events
15
5
  # Wrapper for Cocina events used to generate an imprint statement for display.
@@ -1,5 +1,3 @@
1
- require_relative "../vocabularies/marc_country_codes"
2
-
3
1
  module CocinaDisplay
4
2
  module Events
5
3
  # A single location represented in a Cocina event, like a publication place.
@@ -0,0 +1,33 @@
1
+ module CocinaDisplay
2
+ module Events
3
+ # A single note represented in a Cocina event, like an issuance or edition note.
4
+ class Note
5
+ attr_reader :cocina
6
+
7
+ # Initialize a Note object with Cocina structured data.
8
+ # @param cocina [Hash] The Cocina structured data for the note.
9
+ def initialize(cocina)
10
+ @cocina = cocina
11
+ end
12
+
13
+ # The value of the note.
14
+ # @return [String, nil]
15
+ def to_s
16
+ cocina["value"].presence
17
+ end
18
+
19
+ # The type of the note, like "issuance" or "edition".
20
+ # @return [String, nil]
21
+ def type
22
+ cocina["type"].presence
23
+ end
24
+
25
+ # The display label for the note.
26
+ # @return [String]
27
+ def label
28
+ cocina["displayLabel"].presence ||
29
+ I18n.t(type&.parameterize&.underscore, default: :default, scope: "cocina_display.field_label.event.note")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CocinaDisplay
4
+ # Classes for extracting format/genre information from a Cocina object.
5
+ module Forms
6
+ # A form associated with part or all of a Cocina object.
7
+ class Form
8
+ attr_reader :cocina
9
+
10
+ # Create a Form object from Cocina structured data.
11
+ # Delegates to subclasses for specific types.
12
+ # @param cocina [Hash]
13
+ # @return [Form]
14
+ def self.from_cocina(cocina)
15
+ case cocina["type"]
16
+ when "genre"
17
+ Genre.new(cocina)
18
+ when "resource type"
19
+ ResourceType.new(cocina)
20
+ else
21
+ new(cocina)
22
+ end
23
+ end
24
+
25
+ # Create a Form object from Cocina structured data.
26
+ # @param cocina [Hash]
27
+ def initialize(cocina)
28
+ @cocina = cocina
29
+ end
30
+
31
+ # The value to use for display.
32
+ # @return [String]
33
+ def to_s
34
+ flat_value
35
+ end
36
+
37
+ # Single concatenated string value for the form.
38
+ # @return [String]
39
+ def flat_value
40
+ Utils.compact_and_join(values, delimiter: " > ")
41
+ end
42
+
43
+ # The raw values from the Cocina data, flattened if nested.
44
+ # @return [String]
45
+ def values
46
+ Utils.flatten_nested_values(cocina).pluck("value").compact_blank
47
+ end
48
+
49
+ # The label to use for display.
50
+ # Uses a displayLabel if available, otherwise looks up via type.
51
+ # @return [String]
52
+ def label
53
+ cocina["displayLabel"].presence || type_label
54
+ end
55
+
56
+ # The type of form, such as "genre", "extent", etc.
57
+ # @return [String, nil]
58
+ def type
59
+ cocina["type"]
60
+ end
61
+
62
+ private
63
+
64
+ # Type-specific label for this form value.
65
+ # @return [String]
66
+ def type_label
67
+ I18n.t(type&.parameterize&.underscore, default: :form, scope: "cocina_display.field_label.form")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,12 @@
1
+ module CocinaDisplay
2
+ module Forms
3
+ # A Genre form associated with part or all of a Cocina object.
4
+ class Genre < Form
5
+ # Genres are capitalized for display.
6
+ # @return [String]
7
+ def to_s
8
+ super&.upcase_first
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module CocinaDisplay
2
+ module Forms
3
+ # A Resource Type form associated with part or all of a Cocina object.
4
+ class ResourceType < Form
5
+ # Resource types are lowercased for display.
6
+ # @return [String]
7
+ def to_s
8
+ super&.downcase
9
+ end
10
+
11
+ # Is this a Stanford self-deposit resource type?
12
+ # @note These are handled separately when displayed.
13
+ # @return [Boolean]
14
+ def stanford_self_deposit?
15
+ source == "Stanford self-deposit resource types"
16
+ end
17
+
18
+ # Is this a MODS resource type?
19
+ # @return [Boolean]
20
+ def mods?
21
+ source == "MODS resource types"
22
+ end
23
+
24
+ private
25
+
26
+ # @return [String]
27
+ def source
28
+ cocina.dig("source", "value")
29
+ end
30
+
31
+ # Stanford self-deposit resource types are labeled "Genre".
32
+ # @return [String]
33
+ def type_label
34
+ (I18n.t("cocina_display.field_label.form.genre") if stanford_self_deposit?) || super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,4 +1,4 @@
1
- require "geo/coord"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module CocinaDisplay
4
4
  module Geospatial