cocina_display 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e86f2100a204e574eaf48f86dccf3b9bdc4985285e72e05292f9dd722d9d87e9
4
- data.tar.gz: 5730dd6764fa87f39dc6d00dc66c8ea72f10685c7e3aef0b92864c16b1186cb6
3
+ metadata.gz: 575cbfb8b11f3cf71950bfb5234ada4a916a5e55d21116422de84361854f0713
4
+ data.tar.gz: 4e06ee33d664822bdb6a3057ad8c40539f1f8363350f4648a463c7be0cb533ad
5
5
  SHA512:
6
- metadata.gz: fcfa03dd80c3673045803c11c1d7fa3f9c5284f75806c7c76b5ccb52cb765631670696fe3777a2a5cfbcd90b209ba5e898a618a92d9ecf0186670fdddb09cb6e
7
- data.tar.gz: acbcb5d312ac4755cfff3a74119b68e316d929da0d0d91f397afdd51480f3a90332166f2ccc052e53c2d675d749f62a12f814513b58413611475e36d732c5041
6
+ metadata.gz: 76ecb30d8a60bbb42bab880b0c3c5288d8daa7504cc044fb06c157e1b5e2fe21fd3e56668c3479d291f1e683eab4d719c11b2ac5d185b6e98a062e917f5ea74a
7
+ data.tar.gz: ce4c61a927f9bc034aeebcd9cfc0cb56c8cd1b3c4ee13f7d8d0fcd0cc83290e8d2d1fe31ead879bdb4b1d0090ee60164158ba24d9c8eeb4a7499dfd315b9cf07
@@ -14,6 +14,7 @@ require_relative "concerns/titles"
14
14
  require_relative "concerns/access"
15
15
  require_relative "concerns/subjects"
16
16
  require_relative "concerns/forms"
17
+ require_relative "concerns/languages"
17
18
  require_relative "utils"
18
19
 
19
20
  module CocinaDisplay
@@ -26,6 +27,7 @@ module CocinaDisplay
26
27
  include CocinaDisplay::Concerns::Access
27
28
  include CocinaDisplay::Concerns::Subjects
28
29
  include CocinaDisplay::Concerns::Forms
30
+ include CocinaDisplay::Concerns::Languages
29
31
 
30
32
  # Fetch a public Cocina document from PURL and create a CocinaRecord.
31
33
  # @note This is intended to be used in development or testing only.
@@ -79,9 +79,13 @@ module CocinaDisplay
79
79
  end
80
80
 
81
81
  # All contributors for the object, including authors, editors, etc.
82
+ # Checks both description.contributor and description.event.contributor.
82
83
  # @return [Array<Contributor>]
83
84
  def contributors
84
- @contributors ||= path("$.description.contributor.*").map { |c| Contributor.new(c) }
85
+ @contributors ||= Enumerator::Chain.new(
86
+ path("$.description.contributor.*"),
87
+ path("$.description.event.*.contributor.*")
88
+ ).map { |c| Contributor.new(c) }
85
89
  end
86
90
 
87
91
  # All contributors with a "publisher" role.
@@ -0,0 +1,20 @@
1
+ require_relative "../language"
2
+
3
+ module CocinaDisplay
4
+ module Concerns
5
+ # Methods for extracting language information from a Cocina object.
6
+ module Languages
7
+ # Languages objects associated with the object.
8
+ # @return [Array<CocinaDisplay::Language>]
9
+ def languages
10
+ @languages ||= path("$.description.language.*").map { |lang| CocinaDisplay::Language.new(lang) }
11
+ end
12
+
13
+ # Names of languages associated with the object, if recognized by Searchworks.
14
+ # @return [Array<String>]
15
+ def searchworks_language_names
16
+ languages.filter_map { |lang| lang.display_str if lang.searchworks_language? }.compact_blank.uniq
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,45 +1,46 @@
1
- require_relative "../subject"
1
+ require_relative "../subjects/subject"
2
+ require_relative "../subjects/subject_value"
2
3
 
3
4
  module CocinaDisplay
4
5
  module Concerns
5
6
  # Methods for extracting and formatting subject information.
6
7
  module Subjects
7
- # All unique subjects that are topics, formatted as strings for display.
8
+ # All unique subject values that are topics.
8
9
  # @return [Array<String>]
9
10
  def subject_topics
10
- subjects.filter { |s| s.type == "topic" }.map(&:display_str).uniq
11
+ subject_values.filter { |s| s.type == "topic" }.map(&:display_str).uniq
11
12
  end
12
13
 
13
- # All unique subjects that are genres, formatted as strings for display.
14
+ # All unique subject values that are genres.
14
15
  # @return [Array<String>]
15
16
  def subject_genres
16
- subjects.filter { |s| s.type == "genre" }.map(&:display_str).uniq
17
+ subject_values.filter { |s| s.type == "genre" }.map(&:display_str).uniq
17
18
  end
18
19
 
19
- # All unique subjects that are titles, formatted as strings for display.
20
+ # All unique subject values that are titles.
20
21
  # @return [Array<String>]
21
22
  def subject_titles
22
- subjects.filter { |s| s.type == "title" }.map(&:display_str).uniq
23
+ subject_values.filter { |s| s.type == "title" }.map(&:display_str).uniq
23
24
  end
24
25
 
25
- # All unique subjects that are date/time info, formatted as strings for display.
26
+ # All unique subject values that are date/time info.
26
27
  # @return [Array<String>]
27
28
  def subject_temporal
28
- subjects.filter { |s| s.type == "time" }.map(&:display_str).uniq
29
+ subject_values.filter { |s| s.type == "time" }.map(&:display_str).uniq
29
30
  end
30
31
 
31
- # All unique subjects that are occupations, formatted as strings for display.
32
+ # All unique subject values that are occupations.
32
33
  # @return [Array<String>]
33
34
  def subject_occupations
34
- subjects.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
35
+ subject_values.filter { |s| s.type == "occupation" }.map(&:display_str).uniq
35
36
  end
36
37
 
37
- # All unique subjects that are names of entities, formatted as strings for display.
38
+ # All unique subject values that are names of entities.
38
39
  # @note Multiple types are handled: person, family, organization, conference, etc.
39
- # @see CocinaDisplay::NameSubject
40
+ # @see CocinaDisplay::NameSubjectValue
40
41
  # @return [Array<String>]
41
42
  def subject_names
42
- subjects.filter { |s| s.is_a? NameSubject }.map(&:display_str).uniq
43
+ subject_values.filter { |s| s.is_a? CocinaDisplay::Subjects::NameSubjectValue }.map(&:display_str).uniq
43
44
  end
44
45
 
45
46
  # Combination of all subject values for searching.
@@ -75,6 +76,13 @@ module CocinaDisplay
75
76
  subject_temporal + subject_genres
76
77
  end
77
78
 
79
+ # Combination of all subjects with nested values concatenated for display.
80
+ # @see Subject#display_str
81
+ # @return [Array<String>]
82
+ def subject_all_display
83
+ subjects.map(&:display_str).uniq
84
+ end
85
+
78
86
  private
79
87
 
80
88
  # All subjects, accessible as Subject objects.
@@ -84,7 +92,13 @@ module CocinaDisplay
84
92
  @subjects ||= Enumerator::Chain.new(
85
93
  path("$.description.subject[*]"),
86
94
  path("$.description.geographic.*.subject[*]")
87
- ).map { |s| Subject.from_cocina(s) }
95
+ ).map { |s| CocinaDisplay::Subjects::Subject.new(s) }
96
+ end
97
+
98
+ # All subject values, flattened from all subjects.
99
+ # @return [Array<SubjectValue>]
100
+ def subject_values
101
+ @subject_values ||= subjects.flat_map(&:subject_values)
88
102
  end
89
103
  end
90
104
  end
@@ -5,7 +5,7 @@ require "active_support/core_ext/object/blank"
5
5
  require "active_support/core_ext/array/conversions"
6
6
 
7
7
  require_relative "utils"
8
- require_relative "marc_relator_codes"
8
+ require_relative "vocabularies/marc_relator_codes"
9
9
 
10
10
  module CocinaDisplay
11
11
  # A contributor to a work, such as an author or publisher.
@@ -129,10 +129,10 @@ module CocinaDisplay
129
129
 
130
130
  private
131
131
 
132
- # The full name as a string, combining all name components.
132
+ # The full name as a string, combining all name components and terms of address.
133
133
  # @return [String]
134
134
  def full_name_str
135
- Utils.compact_and_join(name_components, delimiter: ", ")
135
+ Utils.compact_and_join(name_components.push(terms_of_address_str), delimiter: ", ")
136
136
  end
137
137
 
138
138
  # Flattened form of any names explicitly marked as "display name".
@@ -146,7 +146,7 @@ module CocinaDisplay
146
146
  # Otherwise, fall back to any names explicitly marked as "name" or untyped.
147
147
  # @return [Array<String>]
148
148
  def name_components
149
- [surname_str, forename_ordinal_str, terms_of_address_str].compact_blank.presence || Array(name_values["name"])
149
+ [surname_str, forename_ordinal_str].compact_blank.presence || Array(name_values["name"])
150
150
  end
151
151
 
152
152
  # Flatten all forenames and ordinals into a single string.
@@ -201,7 +201,7 @@ module CocinaDisplay
201
201
  # Translates the MARC relator code if no value was present.
202
202
  # @return [String, nil]
203
203
  def display_str
204
- cocina["value"] || (MARC_RELATOR[code] if marc_relator?)
204
+ cocina["value"] || (Vocabularies::MARC_RELATOR[code] if marc_relator?)
205
205
  end
206
206
 
207
207
  # A code associated with the role, e.g. a MARC relator code.
@@ -63,7 +63,18 @@ module CocinaDisplay
63
63
  start&.encoding || stop&.encoding || super
64
64
  end
65
65
 
66
- # Is either date in the range qualified in any way?
66
+ # The qualifier for the entire range.
67
+ # If both qualifiers match, uses that qualifier. If both are empty, falls
68
+ # back to the top level qualifier, if any.
69
+ # @see CocinaDisplay::Date#qualifier
70
+ # @return [String, nil]
71
+ def qualifier
72
+ if start&.qualifier == stop&.qualifier
73
+ start&.qualifier || stop&.qualifier || super
74
+ end
75
+ end
76
+
77
+ # Is either date in the range, or the range itself, qualified?
67
78
  # @see CocinaDisplay::Date#qualified?
68
79
  # @return [Boolean]
69
80
  def qualified?
@@ -105,14 +116,15 @@ module CocinaDisplay
105
116
  # @see CocinaDisplay::Date#qualified_value
106
117
  # @return [String]
107
118
  def qualified_value
108
- if start&.qualifier == stop&.qualifier
109
- qualifier = start&.qualifier || stop&.qualifier
110
- date = decoded_value
111
- return "[ca. #{date}]" if qualifier == "approximate"
112
- return "[#{date}?]" if qualifier == "questionable"
113
- return "[#{date}]" if qualifier == "inferred"
114
-
115
- date
119
+ if qualifier
120
+ case qualifier
121
+ when "approximate"
122
+ "[ca. #{decoded_value}]"
123
+ when "questionable"
124
+ "[#{decoded_value}?]"
125
+ when "inferred"
126
+ "[#{decoded_value}]"
127
+ end
116
128
  else
117
129
  "#{start&.qualified_value} - #{stop&.qualified_value}"
118
130
  end
@@ -7,7 +7,6 @@ require "active_support/core_ext/object/blank"
7
7
 
8
8
  require_relative "event"
9
9
  require_relative "../utils"
10
- require_relative "../marc_country_codes"
11
10
  require_relative "../dates/date"
12
11
  require_relative "../dates/date_range"
13
12
 
@@ -1,4 +1,4 @@
1
- require_relative "../marc_country_codes"
1
+ require_relative "../vocabularies/marc_country_codes"
2
2
 
3
3
  module CocinaDisplay
4
4
  module Events
@@ -36,7 +36,7 @@ module CocinaDisplay
36
36
  # Decoded country name if the location is encoded with a MARC country code.
37
37
  # @return [String, nil]
38
38
  def decoded_country
39
- MARC_COUNTRY[code] if marc_country? && valid_country_code?
39
+ Vocabularies::MARC_COUNTRY[code] if marc_country? && valid_country_code?
40
40
  end
41
41
 
42
42
  # Is this a decodable country code?
@@ -0,0 +1,47 @@
1
+ require "iso639"
2
+ require_relative "vocabularies/searchworks_languages"
3
+
4
+ module CocinaDisplay
5
+ # A language associated with part or all of a Cocina object.
6
+ class Language
7
+ attr_reader :cocina
8
+
9
+ # Create a Language object from Cocina structured data.
10
+ # @param cocina [Hash]
11
+ def initialize(cocina)
12
+ @cocina = cocina
13
+ end
14
+
15
+ # The language name for display.
16
+ # @return [String, nil]
17
+ def display_str
18
+ cocina["value"] || decoded_value
19
+ end
20
+
21
+ # The language code, e.g. an ISO 639 code like "eng" or "spa".
22
+ # @return [String, nil]
23
+ def code
24
+ cocina["code"]
25
+ end
26
+
27
+ # Decoded name of the language based on the code, if present.
28
+ # @return [String, nil]
29
+ def decoded_value
30
+ Vocabularies::SEARCHWORKS_LANGUAGES[code] || (Iso639[code] if iso_639?)
31
+ end
32
+
33
+ # True if the language is recognized by Searchworks.
34
+ # @see CocinaDisplay::Vocabularies::SEARCHWORKS_LANGUAGES
35
+ # @return [Boolean]
36
+ def searchworks_language?
37
+ Vocabularies::SEARCHWORKS_LANGUAGES.value?(display_str)
38
+ end
39
+
40
+ # True if the language has a code sourced from the ISO 639 vocabulary.
41
+ # @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
42
+ # @return [Boolean]
43
+ def iso_639?
44
+ cocina.dig("source", "code")&.start_with? "iso639"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ require_relative "../utils"
2
+ require_relative "subject_value"
3
+
4
+ module CocinaDisplay
5
+ module Subjects
6
+ # Base class for subjects in Cocina structured data.
7
+ class Subject
8
+ attr_reader :cocina
9
+
10
+ # Initialize a Subject object with Cocina structured data.
11
+ # @param cocina [Hash] The Cocina structured data for the subject.
12
+ def initialize(cocina)
13
+ @cocina = cocina
14
+ end
15
+
16
+ # The top-level type of the subject.
17
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#subject-types
18
+ # @return [String, nil]
19
+ def type
20
+ cocina["type"]
21
+ end
22
+
23
+ # Array of display strings for each value in the subject.
24
+ # Used for search, where each value should be indexed separately.
25
+ # @return [Array<String>]
26
+ def display_values
27
+ subject_values.map(&:display_str).compact_blank
28
+ end
29
+
30
+ # A string representation of the entire subject, formatted for display.
31
+ # Concatenates the values with an appropriate delimiter.
32
+ # @return [String]
33
+ def display_str
34
+ Utils.compact_and_join(display_values, delimiter: delimiter)
35
+ end
36
+
37
+ # Individual values composing this subject.
38
+ # Can be multiple if the Cocina featured nested data.
39
+ # If no type was specified on a value, uses the top-level subject type.
40
+ # @return [Array<SubjectValue>]
41
+ def subject_values
42
+ @subject_values ||= Utils.flatten_nested_values(cocina, atomic_types: SubjectValue.atomic_types).map do |value|
43
+ subject_value = SubjectValue.from_cocina(value)
44
+ subject_value.type ||= type
45
+ subject_value
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Delimiter to use for joining structured subject values.
52
+ # LCSH uses a comma (the default); catalog headings use " > ".
53
+ # @return [String]
54
+ def delimiter
55
+ if cocina["displayLabel"]&.downcase == "catalog heading"
56
+ " > "
57
+ else
58
+ ", "
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,104 @@
1
+ require_relative "subject"
2
+ require_relative "../contributor"
3
+ require_relative "../title_builder"
4
+ require_relative "../dates/date"
5
+
6
+ module CocinaDisplay
7
+ module Subjects
8
+ # A descriptive value that can be part of a Subject.
9
+ class SubjectValue
10
+ attr_reader :cocina
11
+
12
+ # The type of the subject value, like "person", "title", or "time".
13
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#subject-part-types-for-structured-value
14
+ attr_accessor :type
15
+
16
+ # Create a SubjectValue from Cocina structured data.
17
+ # @param cocina [Hash] The Cocina structured data for the subject.
18
+ # @return [SubjectValue]
19
+ def self.from_cocina(cocina)
20
+ SUBJECT_VALUE_TYPES.fetch(cocina["type"], SubjectValue).new(cocina)
21
+ end
22
+
23
+ # All subject value types that should not be further destructured.
24
+ # @return [Array<String>]
25
+ def self.atomic_types
26
+ SUBJECT_VALUE_TYPES.keys
27
+ end
28
+
29
+ # Initialize a SubjectValue object with Cocina structured data.
30
+ # @param cocina [Hash] The Cocina structured data for the subject value.
31
+ def initialize(cocina)
32
+ @cocina = cocina
33
+ @type = cocina["type"]
34
+ end
35
+
36
+ # The display string for the subject value.
37
+ # Subclasses should override this method to provide specific formatting.
38
+ # @return [String]
39
+ def display_str
40
+ cocina["value"]
41
+ end
42
+ end
43
+
44
+ # A subject value representing a named entity.
45
+ class NameSubjectValue < SubjectValue
46
+ attr_reader :name
47
+
48
+ # Initialize a NameSubjectValue object with Cocina structured data.
49
+ # @param cocina [Hash] The Cocina structured data for the subject.
50
+ def initialize(cocina)
51
+ super
52
+ @name = Contributor::Name.new(cocina)
53
+ end
54
+
55
+ # Use the contributor name formatting rules for display.
56
+ # @return [String] The formatted name string, including life dates
57
+ # @see CocinaDisplay::Contributor::Name#display_str
58
+ def display_str
59
+ @name.display_str(with_date: true)
60
+ end
61
+ end
62
+
63
+ # A subject value representing an entity with a title.
64
+ class TitleSubjectValue < SubjectValue
65
+ # Construct a title string to use for display.
66
+ # @see CocinaDisplay::TitleBuilder.build
67
+ # @note Unclear how often structured title subjects occur "in the wild".
68
+ # @return [String]
69
+ def display_str
70
+ TitleBuilder.build([cocina])
71
+ end
72
+ end
73
+
74
+ # A subject value representing a date and/or time.
75
+ class TemporalSubjectValue < SubjectValue
76
+ attr_reader :date
77
+
78
+ def initialize(cocina)
79
+ super
80
+ @date = Dates::Date.from_cocina(cocina)
81
+ end
82
+
83
+ # @return [String] The formatted date/time string for display
84
+ def display_str
85
+ @date.qualified_value
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Map Cocina subject types to specific SubjectValue classes for rendering.
92
+ # @see SubjectValue#type
93
+ SUBJECT_VALUE_TYPES = {
94
+ "person" => CocinaDisplay::Subjects::NameSubjectValue,
95
+ "family" => CocinaDisplay::Subjects::NameSubjectValue,
96
+ "organization" => CocinaDisplay::Subjects::NameSubjectValue,
97
+ "conference" => CocinaDisplay::Subjects::NameSubjectValue,
98
+ "event" => CocinaDisplay::Subjects::NameSubjectValue,
99
+ "name" => CocinaDisplay::Subjects::NameSubjectValue,
100
+ "title" => CocinaDisplay::Subjects::TitleSubjectValue,
101
+ "time" => CocinaDisplay::Subjects::TemporalSubjectValue
102
+ # TODO: special handling for geospatial subjects
103
+ # "map coordinates", "bounding box coordinates", "point coordinates"
104
+ }.freeze
@@ -24,6 +24,7 @@ module CocinaDisplay
24
24
  # @return [Array<Hash>] List of node hashes with "value" present
25
25
  # @param cocina [Hash] The Cocina structured data to flatten
26
26
  # @param output [Array] Used for recursion, should be empty on first call
27
+ # @param atomic_types [Array<String>] Types considered atomic; will not be flattened
27
28
  # @example simple value
28
29
  # cocina = { "value" => "John Doe", "type" => "name" }
29
30
  # Utils.flatten_nested_values(cocina)
@@ -36,14 +37,15 @@ module CocinaDisplay
36
37
  # cocina = { "parallelValue" => [{"value" => "foo" }, { "structuredValue" => [{"value" => "bar"}, {"value" => "baz"}] }] }
37
38
  # Utils.flatten_nested_values(cocina)
38
39
  # #=> [{"value" => "foo"}, {"value" => "foo"}, {"value" => "baz"}]
39
- def self.flatten_nested_values(cocina, output = [])
40
+ def self.flatten_nested_values(cocina, output = [], atomic_types: [])
40
41
  return [cocina] if cocina["value"].present?
41
- return cocina.flat_map { |node| flatten_nested_values(node, output) } if cocina.is_a?(Array)
42
+ return [cocina] if atomic_types.include?(cocina["type"])
43
+ return cocina.flat_map { |node| flatten_nested_values(node, output, atomic_types: atomic_types) } if cocina.is_a?(Array)
42
44
 
43
45
  nested_values = Array(cocina["structuredValue"]) + Array(cocina["parallelValue"]) + Array(cocina["groupedValue"])
44
46
  return output unless nested_values.any?
45
47
 
46
- nested_values.flat_map { |node| flatten_nested_values(node, output) }
48
+ nested_values.flat_map { |node| flatten_nested_values(node, output, atomic_types: atomic_types) }
47
49
  end
48
50
 
49
51
  # Recursively remove empty values from a hash, including nested hashes and arrays.
@@ -2,5 +2,5 @@
2
2
 
3
3
  # :nodoc:
4
4
  module CocinaDisplay
5
- VERSION = "0.6.0" # :nodoc:
5
+ VERSION = "0.7.0" # :nodoc:
6
6
  end