cocina-models 0.74.1 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/README.md +9 -0
- data/description_types.yml +1 -1
- data/lib/cocina/generator/generator.rb +0 -3
- data/lib/cocina/models/builders/dro_rights_description_builder.rb +69 -0
- data/lib/cocina/models/builders/name_title_group_builder.rb +134 -0
- data/lib/cocina/models/builders/rights_description_builder.rb +83 -0
- data/lib/cocina/models/builders/title_builder.rb +213 -0
- data/lib/cocina/models/validators/associated_name_validator.rb +77 -0
- data/lib/cocina/models/validators/validator.rb +1 -0
- data/lib/cocina/models/version.rb +1 -1
- metadata +7 -5
- data/lib/cocina/models/dro_rights_description_builder.rb +0 -67
- data/lib/cocina/models/rights_description_builder.rb +0 -81
- data/lib/cocina/models/title_builder.rb +0 -208
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ec67ece77b9025a117182d0213342e180e7b87dcd003a44e3d9dffeb03d08e1
|
|
4
|
+
data.tar.gz: 3ee4eee501728b92c356760c660514789082da70c41c990fc3822f5833a7f47d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b0b94409167d640cfd84c7202468664ec32887405b17973e05ebdd77a55f608e53ad2c2956aabc85ac09dd9edecb6281eaafea7822d77bdea1075301e04aae6
|
|
7
|
+
data.tar.gz: 821ba48bca0b9c92031eb6ba2dbde666f2116c6c7f9076663730636f1c61f11e098551f2e81b975dea06289bd9ba09aa4f498ef67b36dbfa76d6581f29c515c4
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
|
@@ -42,6 +42,15 @@ The generator is tested via its output when run against `openapi.yml`, viz., the
|
|
|
42
42
|
|
|
43
43
|
Beyond what is necessary to test the generator, the Cocina model classes are not tested, i.e., they are assumed to be as specified in `openapi.yml`.
|
|
44
44
|
|
|
45
|
+
## Testing validation changes
|
|
46
|
+
|
|
47
|
+
If there is a possibility that a model or validation change will conflict with some existing objects then [validate-cocina](https://github.com/sul-dlss/dor-services-app/blob/main/bin/validate-cocina) should be used for testing. This must be run on sdr-deploy since it requires deploying a branch of cocina-models.
|
|
48
|
+
|
|
49
|
+
1. Create a cocina-models branch containing the proposed change and push to Github.
|
|
50
|
+
2. On sdr-deploy, check out `main`, update the `Gemfile` so that cocina-models references the branch, and `bundle install`.
|
|
51
|
+
3. Run `bin/validate-cocina`.
|
|
52
|
+
4. Check `validate-cocina.csv` for validation errors.
|
|
53
|
+
|
|
45
54
|
## Releasing
|
|
46
55
|
|
|
47
56
|
### Step 0: Share intent to change the models
|
data/description_types.yml
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cocina
|
|
4
|
+
module Models
|
|
5
|
+
module Builders
|
|
6
|
+
# Rights description builder for items
|
|
7
|
+
class DroRightsDescriptionBuilder < RightsDescriptionBuilder
|
|
8
|
+
# @param [Cocina::Models::DRO] cocina_item
|
|
9
|
+
|
|
10
|
+
# This overrides the superclass
|
|
11
|
+
# @return [Cocina::Models::DROAccess]
|
|
12
|
+
def object_access
|
|
13
|
+
@object_access ||= cocina.access
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def object_level_access
|
|
19
|
+
super + access_level_from_files.uniq.map { |str| "#{str} (file)" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def access_level_from_files
|
|
23
|
+
# dark access doesn't permit any file access
|
|
24
|
+
return [] if object_access.view == 'dark'
|
|
25
|
+
|
|
26
|
+
file_access_nodes.reject { |fa| same_as_object_access?(fa) }.flat_map do |fa|
|
|
27
|
+
file_access_from_file(fa)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# rubocop:disable Metrics/MethodLength
|
|
32
|
+
def file_access_from_file(file_access)
|
|
33
|
+
basic_access = if file_access[:view] == 'location-based'
|
|
34
|
+
"location: #{file_access[:location]}"
|
|
35
|
+
else
|
|
36
|
+
file_access[:view]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return [basic_access] if file_access[:view] == file_access[:download]
|
|
40
|
+
|
|
41
|
+
basic_access += ' (no-download)' if file_access[:view] != 'dark'
|
|
42
|
+
|
|
43
|
+
case file_access[:download]
|
|
44
|
+
when 'stanford'
|
|
45
|
+
[basic_access, 'stanford']
|
|
46
|
+
when 'location-based'
|
|
47
|
+
# Here we're using location to mean download location.
|
|
48
|
+
[basic_access, "location: #{file_access[:location]}"]
|
|
49
|
+
else
|
|
50
|
+
[basic_access]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
# rubocop:enable Metrics/MethodLength
|
|
54
|
+
|
|
55
|
+
def same_as_object_access?(file_access)
|
|
56
|
+
(file_access[:view] == object_access.view && file_access[:download] == object_access.download) ||
|
|
57
|
+
(object_access.view == 'citation-only' && file_access[:view] == 'dark')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def file_access_nodes
|
|
61
|
+
Array(cocina.structural.contains)
|
|
62
|
+
.flat_map { |fs| Array(fs.structural.contains) }
|
|
63
|
+
.map { |file| file.access.to_h }
|
|
64
|
+
.uniq
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cocina
|
|
4
|
+
module Models
|
|
5
|
+
module Builders
|
|
6
|
+
# Helpers for MODS nameTitleGroups.
|
|
7
|
+
# MODS titles need to know if they match a contributor and thus need a nameTitleGroup
|
|
8
|
+
# MODS contributors need to know if they match a title and thus need a nameTitleGroup
|
|
9
|
+
# If there is a match, the nameTitleGroup number has to be consistent for the matching title(s)
|
|
10
|
+
# and the contributor(s)
|
|
11
|
+
class NameTitleGroupBuilder
|
|
12
|
+
# When to assign nameTitleGroup to MODS from cocina:
|
|
13
|
+
# for cocina title of type "uniform",
|
|
14
|
+
# look for cocina title properties :value or :structuredValue (recurse down through :parallelValue
|
|
15
|
+
# as needed), and look for associated :note with :type of "associated name" at the level of the
|
|
16
|
+
# non-empty title [value|structuredValue]
|
|
17
|
+
# The note of type "associated name" will have [value|structuredValue] which will match
|
|
18
|
+
# [value|structuredValue] for a contributor (possibly after recursing through :parallelValue).
|
|
19
|
+
# Thus, a title [value|structuredValue] and a contributor [value|structuredValue] are associated in
|
|
20
|
+
# cocina.
|
|
21
|
+
#
|
|
22
|
+
# If those criteria not met in Cocina, do not assign nameTitleGroup in MODS
|
|
23
|
+
#
|
|
24
|
+
# @params [Cocina::Models::Title] title
|
|
25
|
+
# @return [Hash<Hash, Hash>] key: hash of value or structuredValue property for title
|
|
26
|
+
# value: hash of value or structuredValue property for contributor
|
|
27
|
+
# e.g. {{:value=>"Portrait of the artist as a young man"}=>{:value=>"James Joyce"}}
|
|
28
|
+
# e.g. {{:value=>"Portrait of the artist as a young man"}=>{:structuredValue=>
|
|
29
|
+
# [{:value=>"Joyce, James", :type=>"name"},{:value=>"1882-1941", :type=>"life dates"}]}}
|
|
30
|
+
# e.g. {{:structuredValue=>[{:value=>"Demanding Food", :type=>"main"},
|
|
31
|
+
# {:value=>"A Cat's Life", :type=>"subtitle"}]}=>{:value=>"James Joyce"}}
|
|
32
|
+
# this complexity is needed for multilingual titles mapping to multilingual names.
|
|
33
|
+
def self.build_title_values_to_contributor_name_values(title)
|
|
34
|
+
result = {}
|
|
35
|
+
return result if title.blank?
|
|
36
|
+
|
|
37
|
+
# pair title value with contributor name value
|
|
38
|
+
title_value_note_slices(title).each do |value_note_slice|
|
|
39
|
+
title_val_slice = slice_of_value_or_structured_value(value_note_slice)
|
|
40
|
+
next if title_val_slice.blank?
|
|
41
|
+
|
|
42
|
+
associated_name_note = value_note_slice[:note]&.detect { |note| note[:type] == 'associated name' }
|
|
43
|
+
next if associated_name_note.blank?
|
|
44
|
+
|
|
45
|
+
# relevant note will be Array of either
|
|
46
|
+
# {
|
|
47
|
+
# value: 'string contributor name',
|
|
48
|
+
# type: 'associated name'
|
|
49
|
+
# }
|
|
50
|
+
# OR
|
|
51
|
+
# {
|
|
52
|
+
# structuredValue: [ structuredValue contributor name ],
|
|
53
|
+
# type: 'associated name'
|
|
54
|
+
# }
|
|
55
|
+
# and we want the hash without the :type attribute
|
|
56
|
+
result[title_val_slice] = slice_of_value_or_structured_value(associated_name_note)
|
|
57
|
+
end
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.contributor_for_contributor_name_value_slice(contributor_name_value_slice:, contributors:)
|
|
62
|
+
Array(contributors).find do |contributor|
|
|
63
|
+
contrib_name_value_slices = contributor_name_value_slices(contributor)
|
|
64
|
+
contrib_name_value_slices.include?(contributor_name_value_slice)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @params [Cocina::Models::Contributor] contributor
|
|
69
|
+
# @return [Hash] where we are only interested in
|
|
70
|
+
# hashes containing (either :value or :structureValue)
|
|
71
|
+
def self.contributor_name_value_slices(contributor)
|
|
72
|
+
return if contributor&.name.blank?
|
|
73
|
+
|
|
74
|
+
slices = []
|
|
75
|
+
Array(contributor.name).each do |contrib_name|
|
|
76
|
+
slices << value_slices(contrib_name)
|
|
77
|
+
end
|
|
78
|
+
slices.flatten
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @params [Cocina::Models::DescriptiveValue] desc_value
|
|
82
|
+
# @return [Array<Cocina::Models::DescriptiveValue>] where we are only interested in
|
|
83
|
+
# hashes containing (either :value or :structuredValue)
|
|
84
|
+
# rubocop:disable Metrics/AbcSize
|
|
85
|
+
def self.value_slices(desc_value)
|
|
86
|
+
slices = []
|
|
87
|
+
desc_value_slice = desc_value.to_h.slice(:value, :structuredValue, :parallelValue)
|
|
88
|
+
if desc_value_slice[:value].present? || desc_value_slice[:structuredValue].present?
|
|
89
|
+
slices << desc_value_slice.select { |_k, value| value.present? }
|
|
90
|
+
elsif desc_value_slice[:parallelValue].present?
|
|
91
|
+
desc_value_slice[:parallelValue].each { |parallel_val| slices << value_slices(parallel_val) }
|
|
92
|
+
end
|
|
93
|
+
# ignoring groupedValue
|
|
94
|
+
slices.flatten
|
|
95
|
+
end
|
|
96
|
+
# rubocop:enable Metrics/AbcSize
|
|
97
|
+
# private_class_method :value_slices
|
|
98
|
+
|
|
99
|
+
# for a given Hash (from a Cocina DescriptiveValue or Title or Name or ...)
|
|
100
|
+
# result will be either
|
|
101
|
+
# { value: 'string value' }
|
|
102
|
+
# OR
|
|
103
|
+
# { structuredValue: [ some structuredValue ] }
|
|
104
|
+
def self.slice_of_value_or_structured_value(hash)
|
|
105
|
+
if hash[:value].present?
|
|
106
|
+
hash.slice(:value).select { |_k, value| value.present? }
|
|
107
|
+
elsif hash[:structuredValue].present?
|
|
108
|
+
hash.slice(:structuredValue).select { |_k, value| value.present? }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# reduce parallelValues down to value or structuredValue for these slices
|
|
113
|
+
# @params [Cocina::Models::Title] title
|
|
114
|
+
# @return [Array<Cocina::Models::DescriptiveValue>] where we are only interested in
|
|
115
|
+
# hashes containing (either :value or :structureValue) and :note if present
|
|
116
|
+
# rubocop:disable Metrics/AbcSize
|
|
117
|
+
def self.title_value_note_slices(title)
|
|
118
|
+
slices = []
|
|
119
|
+
title_slice = title.to_h.slice(:value, :structuredValue, :parallelValue, :note)
|
|
120
|
+
if title_slice[:value].present? || title_slice[:structuredValue].present?
|
|
121
|
+
slices << title_slice.select { |_k, value| value.present? }
|
|
122
|
+
elsif title_slice[:parallelValue].present?
|
|
123
|
+
title_slice[:parallelValue].each do |parallel_val|
|
|
124
|
+
slices << title_value_note_slices(parallel_val)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
# ignoring groupedValue
|
|
128
|
+
slices.flatten
|
|
129
|
+
end
|
|
130
|
+
# rubocop:enable Metrics/AbcSize
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cocina
|
|
4
|
+
module Models
|
|
5
|
+
module Builders
|
|
6
|
+
# RightsDescriptionBuilder
|
|
7
|
+
class RightsDescriptionBuilder
|
|
8
|
+
# @param [Cocina::Models::AdminPolicy, Cocina::Models::DRO] cocina_object
|
|
9
|
+
def self.build(cocina_object)
|
|
10
|
+
new(cocina_object).build
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(cocina_object)
|
|
14
|
+
@cocina = cocina_object
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# This is set up to work for APOs, but this method is to be overridden on sub classes
|
|
18
|
+
# @return [Cocina::Models::AdminPolicyDefaultAccess]
|
|
19
|
+
def object_access
|
|
20
|
+
@object_access ||= cocina.administrative.accessTemplate
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build
|
|
24
|
+
return 'controlled digital lending' if object_access.controlledDigitalLending
|
|
25
|
+
|
|
26
|
+
return ['dark'] if object_access.view == 'dark'
|
|
27
|
+
|
|
28
|
+
object_level_access
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :cocina
|
|
34
|
+
|
|
35
|
+
# rubocop:disable Metrics/MethodLength
|
|
36
|
+
def object_level_access
|
|
37
|
+
case object_access.view
|
|
38
|
+
when 'citation-only'
|
|
39
|
+
['citation']
|
|
40
|
+
when 'world'
|
|
41
|
+
world_object_access
|
|
42
|
+
when 'location-based'
|
|
43
|
+
case object_access.download
|
|
44
|
+
when 'none'
|
|
45
|
+
["location: #{object_access.location} (no-download)"]
|
|
46
|
+
else
|
|
47
|
+
["location: #{object_access.location}"]
|
|
48
|
+
end
|
|
49
|
+
when 'stanford'
|
|
50
|
+
stanford_object_access
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
# rubocop:enable Metrics/MethodLength
|
|
54
|
+
|
|
55
|
+
def stanford_object_access
|
|
56
|
+
case object_access.download
|
|
57
|
+
when 'none'
|
|
58
|
+
['stanford (no-download)']
|
|
59
|
+
when 'location-based'
|
|
60
|
+
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
|
61
|
+
['stanford (no-download)', "location: #{object_access.location}"]
|
|
62
|
+
else
|
|
63
|
+
['stanford']
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def world_object_access
|
|
68
|
+
case object_access.download
|
|
69
|
+
when 'stanford'
|
|
70
|
+
['stanford', 'world (no-download)']
|
|
71
|
+
when 'none'
|
|
72
|
+
['world (no-download)']
|
|
73
|
+
when 'world'
|
|
74
|
+
['world']
|
|
75
|
+
when 'location-based'
|
|
76
|
+
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
|
77
|
+
['world (no-download)', "location: #{object_access.location}"]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'deprecation'
|
|
4
|
+
|
|
5
|
+
module Cocina
|
|
6
|
+
module Models
|
|
7
|
+
module Builders
|
|
8
|
+
# TitleBuilder selects the prefered title from the cocina object for solr indexing
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class TitleBuilder
|
|
11
|
+
extend Deprecation
|
|
12
|
+
# @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
|
|
13
|
+
# @param [Symbol] strategy ":first" is the strategy for selection when primary or display
|
|
14
|
+
# title are missing
|
|
15
|
+
# @param [Boolean] add_punctuation determines if the title should be formmated with punctuation
|
|
16
|
+
# @return [String] the title value for Solr
|
|
17
|
+
def self.build(titles, strategy: :first, add_punctuation: true)
|
|
18
|
+
if titles.respond_to?(:description)
|
|
19
|
+
Deprecation.warn(self,
|
|
20
|
+
"Calling TitleBuilder.build with a #{titles.class} is deprecated. " \
|
|
21
|
+
'It must be called with an array of titles')
|
|
22
|
+
titles = titles.description.title
|
|
23
|
+
end
|
|
24
|
+
new(strategy: strategy, add_punctuation: add_punctuation).build(titles)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(strategy:, add_punctuation:)
|
|
28
|
+
@strategy = strategy
|
|
29
|
+
@add_punctuation = add_punctuation
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param [[Array<Cocina::Models::Title>] titles the titles to consider
|
|
33
|
+
# @return [String] the title value for Solr
|
|
34
|
+
def build(titles)
|
|
35
|
+
cocina_title = primary_title(titles) || untyped_title(titles)
|
|
36
|
+
cocina_title = other_title(titles) if cocina_title.blank?
|
|
37
|
+
|
|
38
|
+
if strategy == :first
|
|
39
|
+
extract_title(cocina_title)
|
|
40
|
+
else
|
|
41
|
+
cocina_title.map { |one| extract_title(one) }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :strategy
|
|
48
|
+
|
|
49
|
+
def extract_title(cocina_title)
|
|
50
|
+
result = if cocina_title.value
|
|
51
|
+
cocina_title.value
|
|
52
|
+
elsif cocina_title.structuredValue.present?
|
|
53
|
+
title_from_structured_values(cocina_title)
|
|
54
|
+
elsif cocina_title.parallelValue.present?
|
|
55
|
+
return build(cocina_title.parallelValue)
|
|
56
|
+
end
|
|
57
|
+
remove_trailing_punctuation(result.strip) if result.present?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def add_punctuation?
|
|
61
|
+
@add_punctuation
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Cocina::Models::Title, nil] title that has status=primary
|
|
65
|
+
def primary_title(titles)
|
|
66
|
+
primary_title = titles.find do |title|
|
|
67
|
+
title.status == 'primary'
|
|
68
|
+
end
|
|
69
|
+
return primary_title if primary_title.present?
|
|
70
|
+
|
|
71
|
+
# NOTE: structuredValues would only have status primary assigned as a sibling, not as an attribute
|
|
72
|
+
|
|
73
|
+
titles.find do |title|
|
|
74
|
+
title.parallelValue&.find do |parallel_title|
|
|
75
|
+
parallel_title.status == 'primary'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def untyped_title(titles)
|
|
81
|
+
method = strategy == :first ? :find : :select
|
|
82
|
+
untyped_title_for(titles.public_send(method))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Array[Cocina::Models::Title]] first title that has no type attribute
|
|
86
|
+
def untyped_title_for(titles)
|
|
87
|
+
titles.each do |title|
|
|
88
|
+
if title.parallelValue.present?
|
|
89
|
+
untyped_title_for(title.parallelValue)
|
|
90
|
+
else
|
|
91
|
+
title.type.nil? || title.type == 'title'
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# This handles 'main title', 'uniform' or 'translated'
|
|
97
|
+
def other_title(titles)
|
|
98
|
+
if strategy == :first
|
|
99
|
+
titles.first
|
|
100
|
+
else
|
|
101
|
+
titles
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# rubocop:disable Metrics/BlockLength
|
|
106
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
107
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
108
|
+
# rubocop:disable Metrics/MethodLength
|
|
109
|
+
# rubocop:disable Metrics/AbcSize
|
|
110
|
+
# @param [Cocina::Models::Title] title with structured values
|
|
111
|
+
# @return [String] the title value from combining the pieces of the structured_values by type and order
|
|
112
|
+
# with desired punctuation per specs
|
|
113
|
+
def title_from_structured_values(title)
|
|
114
|
+
structured_title = ''
|
|
115
|
+
part_name_number = ''
|
|
116
|
+
# combine pieces of the cocina structuredValue into a single title
|
|
117
|
+
title.structuredValue.each do |structured_value|
|
|
118
|
+
# There can be a structuredValue inside a structuredValue. For example,
|
|
119
|
+
# a uniform title where both the name and the title have internal StructuredValue
|
|
120
|
+
return title_from_structured_values(structured_value) if structured_value.structuredValue.present?
|
|
121
|
+
|
|
122
|
+
value = structured_value.value&.strip
|
|
123
|
+
next unless value
|
|
124
|
+
|
|
125
|
+
# additional types: name, uniform ...
|
|
126
|
+
case structured_value.type&.downcase
|
|
127
|
+
when 'nonsorting characters'
|
|
128
|
+
non_sort_value = "#{value}#{non_sorting_padding(title, value)}"
|
|
129
|
+
structured_title = if structured_title.present?
|
|
130
|
+
"#{structured_title}#{non_sort_value}"
|
|
131
|
+
else
|
|
132
|
+
non_sort_value
|
|
133
|
+
end
|
|
134
|
+
when 'part name', 'part number'
|
|
135
|
+
if part_name_number.blank?
|
|
136
|
+
part_name_number = part_name_number(title.structuredValue)
|
|
137
|
+
structured_title = if !add_punctuation?
|
|
138
|
+
[structured_title, part_name_number].join(' ')
|
|
139
|
+
elsif structured_title.present?
|
|
140
|
+
"#{structured_title.sub(/[ .,]*$/, '')}. #{part_name_number}. "
|
|
141
|
+
else
|
|
142
|
+
"#{part_name_number}. "
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
when 'main title', 'title'
|
|
146
|
+
structured_title = "#{structured_title}#{value}"
|
|
147
|
+
when 'subtitle'
|
|
148
|
+
# subtitle is preceded by space colon space, unless it is at the beginning of the title string
|
|
149
|
+
structured_title = if !add_punctuation?
|
|
150
|
+
[structured_title, value].join(' ')
|
|
151
|
+
elsif structured_title.present?
|
|
152
|
+
"#{structured_title.sub(/[. :]+$/, '')} : #{value.sub(/^:/, '').strip}"
|
|
153
|
+
else
|
|
154
|
+
structured_title = value.sub(/^:/, '').strip
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
structured_title
|
|
159
|
+
end
|
|
160
|
+
# rubocop:enable Metrics/AbcSize
|
|
161
|
+
# rubocop:enable Metrics/MethodLength
|
|
162
|
+
# rubocop:enable Metrics/BlockLength
|
|
163
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
164
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
165
|
+
|
|
166
|
+
def remove_trailing_punctuation(title)
|
|
167
|
+
title.sub(%r{[ .,;:/\\]+$}, '')
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def non_sorting_padding(title, non_sorting_value)
|
|
171
|
+
non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
|
|
172
|
+
if non_sort_note
|
|
173
|
+
padding_count = [non_sort_note.value.to_i - non_sorting_value.length, 0].max
|
|
174
|
+
' ' * padding_count
|
|
175
|
+
elsif ['\'', '-'].include?(non_sorting_value.last)
|
|
176
|
+
''
|
|
177
|
+
else
|
|
178
|
+
' '
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# combine part name and part number:
|
|
183
|
+
# respect order of occurrence
|
|
184
|
+
# separated from each other by comma space
|
|
185
|
+
def part_name_number(structured_values)
|
|
186
|
+
title_from_part = ''
|
|
187
|
+
structured_values.each do |structured_value|
|
|
188
|
+
case structured_value.type&.downcase
|
|
189
|
+
when 'part name', 'part number'
|
|
190
|
+
value = structured_value.value&.strip
|
|
191
|
+
next unless value
|
|
192
|
+
|
|
193
|
+
title_from_part = append_part_to_title(title_from_part, value)
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
title_from_part
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def append_part_to_title(title_from_part, value)
|
|
201
|
+
if !add_punctuation?
|
|
202
|
+
[title_from_part, value].select(&:presence).join(' ')
|
|
203
|
+
elsif title_from_part.strip.present?
|
|
204
|
+
"#{title_from_part.sub(/[ .,]*$/, '')}, #{value}"
|
|
205
|
+
else
|
|
206
|
+
value
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
# rubocop:enable Metrics/ClassLength
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cocina
|
|
4
|
+
module Models
|
|
5
|
+
module Validators
|
|
6
|
+
# Validates that when title.note with type "associated name" has a value, it must match a contributor name.
|
|
7
|
+
class AssociatedNameValidator
|
|
8
|
+
def self.validate(clazz, attributes)
|
|
9
|
+
new(clazz, attributes).validate
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(clazz, attributes)
|
|
13
|
+
@clazz = clazz
|
|
14
|
+
@attributes = attributes.deep_symbolize_keys
|
|
15
|
+
@error_paths = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate
|
|
19
|
+
return unless meets_preconditions?
|
|
20
|
+
|
|
21
|
+
return if resources.all? { |resource| valid?(resource) }
|
|
22
|
+
|
|
23
|
+
raise ValidationError,
|
|
24
|
+
'Missing data: Name associated with uniform title does not match any contributor.'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :clazz, :attributes, :error_paths
|
|
30
|
+
|
|
31
|
+
def meets_preconditions?
|
|
32
|
+
resources.any? do |resource|
|
|
33
|
+
titles_with_associated_name_note_for(resource).present?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def valid?(resource)
|
|
38
|
+
titles_with_associated_name_note_for(resource).all? do |title|
|
|
39
|
+
contributor_name_value_slices = Builders::NameTitleGroupBuilder
|
|
40
|
+
.build_title_values_to_contributor_name_values(
|
|
41
|
+
Cocina::Models::Title.new(title)
|
|
42
|
+
).values
|
|
43
|
+
contributor_name_value_slices.all? do |contributor_name_value_slice|
|
|
44
|
+
contributors = Array(resource[:contributor]).map do |contributor|
|
|
45
|
+
Cocina::Models::Contributor.new(contributor)
|
|
46
|
+
end
|
|
47
|
+
Builders::NameTitleGroupBuilder.contributor_for_contributor_name_value_slice(
|
|
48
|
+
contributor_name_value_slice: contributor_name_value_slice, contributors: contributors
|
|
49
|
+
).present?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def resources
|
|
55
|
+
@resources ||= [description_attributes] + Array(description_attributes[:relatedResource])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def description_attributes
|
|
59
|
+
@description_attributes ||= if [Cocina::Models::Description,
|
|
60
|
+
Cocina::Models::RequestDescription].include?(clazz)
|
|
61
|
+
attributes
|
|
62
|
+
else
|
|
63
|
+
attributes[:description] || {}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def associated_name_note_for(title)
|
|
68
|
+
Array(title[:note]).detect { |note| note[:type] == 'associated name' }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def titles_with_associated_name_note_for(resource)
|
|
72
|
+
Array(resource[:title]).select { |title| associated_name_note_for(title).present? }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cocina-models
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.75.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Coyne
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-04-
|
|
11
|
+
date: 2022-04-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -313,6 +313,10 @@ files:
|
|
|
313
313
|
- lib/cocina/models/admin_policy_with_metadata.rb
|
|
314
314
|
- lib/cocina/models/administrative.rb
|
|
315
315
|
- lib/cocina/models/applies_to.rb
|
|
316
|
+
- lib/cocina/models/builders/dro_rights_description_builder.rb
|
|
317
|
+
- lib/cocina/models/builders/name_title_group_builder.rb
|
|
318
|
+
- lib/cocina/models/builders/rights_description_builder.rb
|
|
319
|
+
- lib/cocina/models/builders/title_builder.rb
|
|
316
320
|
- lib/cocina/models/business_barcode.rb
|
|
317
321
|
- lib/cocina/models/catalog_link.rb
|
|
318
322
|
- lib/cocina/models/catkey_barcode.rb
|
|
@@ -341,7 +345,6 @@ files:
|
|
|
341
345
|
- lib/cocina/models/doi.rb
|
|
342
346
|
- lib/cocina/models/dro.rb
|
|
343
347
|
- lib/cocina/models/dro_access.rb
|
|
344
|
-
- lib/cocina/models/dro_rights_description_builder.rb
|
|
345
348
|
- lib/cocina/models/dro_structural.rb
|
|
346
349
|
- lib/cocina/models/dro_with_metadata.rb
|
|
347
350
|
- lib/cocina/models/druid.rb
|
|
@@ -377,7 +380,6 @@ files:
|
|
|
377
380
|
- lib/cocina/models/request_file_set.rb
|
|
378
381
|
- lib/cocina/models/request_file_set_structural.rb
|
|
379
382
|
- lib/cocina/models/request_identification.rb
|
|
380
|
-
- lib/cocina/models/rights_description_builder.rb
|
|
381
383
|
- lib/cocina/models/sequence.rb
|
|
382
384
|
- lib/cocina/models/source.rb
|
|
383
385
|
- lib/cocina/models/source_id.rb
|
|
@@ -385,8 +387,8 @@ files:
|
|
|
385
387
|
- lib/cocina/models/standard_barcode.rb
|
|
386
388
|
- lib/cocina/models/stanford_access.rb
|
|
387
389
|
- lib/cocina/models/title.rb
|
|
388
|
-
- lib/cocina/models/title_builder.rb
|
|
389
390
|
- lib/cocina/models/validatable.rb
|
|
391
|
+
- lib/cocina/models/validators/associated_name_validator.rb
|
|
390
392
|
- lib/cocina/models/validators/catalog_links_validator.rb
|
|
391
393
|
- lib/cocina/models/validators/dark_validator.rb
|
|
392
394
|
- lib/cocina/models/validators/description_types_validator.rb
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Cocina
|
|
4
|
-
module Models
|
|
5
|
-
# Rights description builder for items
|
|
6
|
-
class DroRightsDescriptionBuilder < RightsDescriptionBuilder
|
|
7
|
-
# @param [Cocina::Models::DRO] cocina_item
|
|
8
|
-
|
|
9
|
-
# This overrides the superclass
|
|
10
|
-
# @return [Cocina::Models::DROAccess]
|
|
11
|
-
def object_access
|
|
12
|
-
@object_access ||= cocina.access
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def object_level_access
|
|
18
|
-
super + access_level_from_files.uniq.map { |str| "#{str} (file)" }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def access_level_from_files
|
|
22
|
-
# dark access doesn't permit any file access
|
|
23
|
-
return [] if object_access.view == 'dark'
|
|
24
|
-
|
|
25
|
-
file_access_nodes.reject { |fa| same_as_object_access?(fa) }.flat_map do |fa|
|
|
26
|
-
file_access_from_file(fa)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# rubocop:disable Metrics/MethodLength
|
|
31
|
-
def file_access_from_file(file_access)
|
|
32
|
-
basic_access = if file_access[:view] == 'location-based'
|
|
33
|
-
"location: #{file_access[:location]}"
|
|
34
|
-
else
|
|
35
|
-
file_access[:view]
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
return [basic_access] if file_access[:view] == file_access[:download]
|
|
39
|
-
|
|
40
|
-
basic_access += ' (no-download)' if file_access[:view] != 'dark'
|
|
41
|
-
|
|
42
|
-
case file_access[:download]
|
|
43
|
-
when 'stanford'
|
|
44
|
-
[basic_access, 'stanford']
|
|
45
|
-
when 'location-based'
|
|
46
|
-
# Here we're using location to mean download location.
|
|
47
|
-
[basic_access, "location: #{file_access[:location]}"]
|
|
48
|
-
else
|
|
49
|
-
[basic_access]
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
# rubocop:enable Metrics/MethodLength
|
|
53
|
-
|
|
54
|
-
def same_as_object_access?(file_access)
|
|
55
|
-
(file_access[:view] == object_access.view && file_access[:download] == object_access.download) ||
|
|
56
|
-
(object_access.view == 'citation-only' && file_access[:view] == 'dark')
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def file_access_nodes
|
|
60
|
-
Array(cocina.structural.contains)
|
|
61
|
-
.flat_map { |fs| Array(fs.structural.contains) }
|
|
62
|
-
.map { |file| file.access.to_h }
|
|
63
|
-
.uniq
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Cocina
|
|
4
|
-
module Models
|
|
5
|
-
# RightsDescriptionBuilder
|
|
6
|
-
class RightsDescriptionBuilder
|
|
7
|
-
# @param [Cocina::Models::AdminPolicy, Cocina::Models::DRO] cocina_object
|
|
8
|
-
def self.build(cocina_object)
|
|
9
|
-
new(cocina_object).build
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def initialize(cocina_object)
|
|
13
|
-
@cocina = cocina_object
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# This is set up to work for APOs, but this method is to be overridden on sub classes
|
|
17
|
-
# @return [Cocina::Models::AdminPolicyDefaultAccess]
|
|
18
|
-
def object_access
|
|
19
|
-
@object_access ||= cocina.administrative.accessTemplate
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def build
|
|
23
|
-
return 'controlled digital lending' if object_access.controlledDigitalLending
|
|
24
|
-
|
|
25
|
-
return ['dark'] if object_access.view == 'dark'
|
|
26
|
-
|
|
27
|
-
object_level_access
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
attr_reader :cocina
|
|
33
|
-
|
|
34
|
-
# rubocop:disable Metrics/MethodLength
|
|
35
|
-
def object_level_access
|
|
36
|
-
case object_access.view
|
|
37
|
-
when 'citation-only'
|
|
38
|
-
['citation']
|
|
39
|
-
when 'world'
|
|
40
|
-
world_object_access
|
|
41
|
-
when 'location-based'
|
|
42
|
-
case object_access.download
|
|
43
|
-
when 'none'
|
|
44
|
-
["location: #{object_access.location} (no-download)"]
|
|
45
|
-
else
|
|
46
|
-
["location: #{object_access.location}"]
|
|
47
|
-
end
|
|
48
|
-
when 'stanford'
|
|
49
|
-
stanford_object_access
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
# rubocop:enable Metrics/MethodLength
|
|
53
|
-
|
|
54
|
-
def stanford_object_access
|
|
55
|
-
case object_access.download
|
|
56
|
-
when 'none'
|
|
57
|
-
['stanford (no-download)']
|
|
58
|
-
when 'location-based'
|
|
59
|
-
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
|
60
|
-
['stanford (no-download)', "location: #{object_access.location}"]
|
|
61
|
-
else
|
|
62
|
-
['stanford']
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def world_object_access
|
|
67
|
-
case object_access.download
|
|
68
|
-
when 'stanford'
|
|
69
|
-
['stanford', 'world (no-download)']
|
|
70
|
-
when 'none'
|
|
71
|
-
['world (no-download)']
|
|
72
|
-
when 'world'
|
|
73
|
-
['world']
|
|
74
|
-
when 'location-based'
|
|
75
|
-
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
|
76
|
-
['world (no-download)', "location: #{object_access.location}"]
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'deprecation'
|
|
4
|
-
|
|
5
|
-
module Cocina
|
|
6
|
-
module Models
|
|
7
|
-
# TitleBuilder selects the prefered title from the cocina object for solr indexing
|
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
|
9
|
-
class TitleBuilder
|
|
10
|
-
extend Deprecation
|
|
11
|
-
# @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
|
|
12
|
-
# @param [Symbol] strategy ":first" is the strategy for selection when primary or display title are missing
|
|
13
|
-
# @param [Boolean] add_punctuation determines if the title should be formmated with punctuation
|
|
14
|
-
# @return [String] the title value for Solr
|
|
15
|
-
def self.build(titles, strategy: :first, add_punctuation: true)
|
|
16
|
-
if titles.respond_to?(:description)
|
|
17
|
-
Deprecation.warn(self, "Calling TitleBuilder.build with a #{titles.class} is deprecated. It must be called with an array of titles")
|
|
18
|
-
titles = titles.description.title
|
|
19
|
-
end
|
|
20
|
-
new(strategy: strategy, add_punctuation: add_punctuation).build(titles)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def initialize(strategy:, add_punctuation:)
|
|
24
|
-
@strategy = strategy
|
|
25
|
-
@add_punctuation = add_punctuation
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# @param [[Array<Cocina::Models::Title>] titles the titles to consider
|
|
29
|
-
# @return [String] the title value for Solr
|
|
30
|
-
def build(titles)
|
|
31
|
-
cocina_title = primary_title(titles) || untyped_title(titles)
|
|
32
|
-
cocina_title = other_title(titles) if cocina_title.blank?
|
|
33
|
-
|
|
34
|
-
if strategy == :first
|
|
35
|
-
extract_title(cocina_title)
|
|
36
|
-
else
|
|
37
|
-
cocina_title.map { |one| extract_title(one) }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
attr_reader :strategy
|
|
44
|
-
|
|
45
|
-
def extract_title(cocina_title)
|
|
46
|
-
result = if cocina_title.value
|
|
47
|
-
cocina_title.value
|
|
48
|
-
elsif cocina_title.structuredValue.present?
|
|
49
|
-
title_from_structured_values(cocina_title)
|
|
50
|
-
elsif cocina_title.parallelValue.present?
|
|
51
|
-
return build(cocina_title.parallelValue)
|
|
52
|
-
end
|
|
53
|
-
remove_trailing_punctuation(result.strip) if result.present?
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def add_punctuation?
|
|
57
|
-
@add_punctuation
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# @return [Cocina::Models::Title, nil] title that has status=primary
|
|
61
|
-
def primary_title(titles)
|
|
62
|
-
primary_title = titles.find do |title|
|
|
63
|
-
title.status == 'primary'
|
|
64
|
-
end
|
|
65
|
-
return primary_title if primary_title.present?
|
|
66
|
-
|
|
67
|
-
# NOTE: structuredValues would only have status primary assigned as a sibling, not as an attribute
|
|
68
|
-
|
|
69
|
-
titles.find do |title|
|
|
70
|
-
title.parallelValue&.find do |parallel_title|
|
|
71
|
-
parallel_title.status == 'primary'
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def untyped_title(titles)
|
|
77
|
-
method = strategy == :first ? :find : :select
|
|
78
|
-
untyped_title_for(titles.public_send(method))
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# @return [Array[Cocina::Models::Title]] first title that has no type attribute
|
|
82
|
-
def untyped_title_for(titles)
|
|
83
|
-
titles.each do |title|
|
|
84
|
-
if title.parallelValue.present?
|
|
85
|
-
untyped_title_for(title.parallelValue)
|
|
86
|
-
else
|
|
87
|
-
title.type.nil? || title.type == 'title'
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# This handles 'main title', 'uniform' or 'translated'
|
|
93
|
-
def other_title(titles)
|
|
94
|
-
if strategy == :first
|
|
95
|
-
titles.first
|
|
96
|
-
else
|
|
97
|
-
titles
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# rubocop:disable Metrics/BlockLength
|
|
102
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
103
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
104
|
-
# rubocop:disable Metrics/MethodLength
|
|
105
|
-
# rubocop:disable Metrics/AbcSize
|
|
106
|
-
# @param [Cocina::Models::Title] title with structured values
|
|
107
|
-
# @return [String] the title value from combining the pieces of the structured_values by type and order
|
|
108
|
-
# with desired punctuation per specs
|
|
109
|
-
def title_from_structured_values(title)
|
|
110
|
-
structured_title = ''
|
|
111
|
-
part_name_number = ''
|
|
112
|
-
# combine pieces of the cocina structuredValue into a single title
|
|
113
|
-
title.structuredValue.each do |structured_value|
|
|
114
|
-
# There can be a structuredValue inside a structuredValue. For example,
|
|
115
|
-
# a uniform title where both the name and the title have internal StructuredValue
|
|
116
|
-
return title_from_structured_values(structured_value) if structured_value.structuredValue.present?
|
|
117
|
-
|
|
118
|
-
value = structured_value.value&.strip
|
|
119
|
-
next unless value
|
|
120
|
-
|
|
121
|
-
# additional types: name, uniform ...
|
|
122
|
-
case structured_value.type&.downcase
|
|
123
|
-
when 'nonsorting characters'
|
|
124
|
-
non_sort_value = "#{value}#{non_sorting_padding(title, value)}"
|
|
125
|
-
structured_title = if structured_title.present?
|
|
126
|
-
"#{structured_title}#{non_sort_value}"
|
|
127
|
-
else
|
|
128
|
-
non_sort_value
|
|
129
|
-
end
|
|
130
|
-
when 'part name', 'part number'
|
|
131
|
-
if part_name_number.blank?
|
|
132
|
-
part_name_number = part_name_number(title.structuredValue)
|
|
133
|
-
structured_title = if !add_punctuation?
|
|
134
|
-
[structured_title, part_name_number].join(' ')
|
|
135
|
-
elsif structured_title.present?
|
|
136
|
-
"#{structured_title.sub(/[ .,]*$/, '')}. #{part_name_number}. "
|
|
137
|
-
else
|
|
138
|
-
"#{part_name_number}. "
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
when 'main title', 'title'
|
|
142
|
-
structured_title = "#{structured_title}#{value}"
|
|
143
|
-
when 'subtitle'
|
|
144
|
-
# subtitle is preceded by space colon space, unless it is at the beginning of the title string
|
|
145
|
-
structured_title = if !add_punctuation?
|
|
146
|
-
[structured_title, value].join(' ')
|
|
147
|
-
elsif structured_title.present?
|
|
148
|
-
"#{structured_title.sub(/[. :]+$/, '')} : #{value.sub(/^:/, '').strip}"
|
|
149
|
-
else
|
|
150
|
-
structured_title = value.sub(/^:/, '').strip
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
structured_title
|
|
155
|
-
end
|
|
156
|
-
# rubocop:enable Metrics/AbcSize
|
|
157
|
-
# rubocop:enable Metrics/MethodLength
|
|
158
|
-
# rubocop:enable Metrics/BlockLength
|
|
159
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
160
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
161
|
-
|
|
162
|
-
def remove_trailing_punctuation(title)
|
|
163
|
-
title.sub(%r{[ .,;:/\\]+$}, '')
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def non_sorting_padding(title, non_sorting_value)
|
|
167
|
-
non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
|
|
168
|
-
if non_sort_note
|
|
169
|
-
padding_count = [non_sort_note.value.to_i - non_sorting_value.length, 0].max
|
|
170
|
-
' ' * padding_count
|
|
171
|
-
elsif ['\'', '-'].include?(non_sorting_value.last)
|
|
172
|
-
''
|
|
173
|
-
else
|
|
174
|
-
' '
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# combine part name and part number:
|
|
179
|
-
# respect order of occurrence
|
|
180
|
-
# separated from each other by comma space
|
|
181
|
-
def part_name_number(structured_values)
|
|
182
|
-
title_from_part = ''
|
|
183
|
-
structured_values.each do |structured_value|
|
|
184
|
-
case structured_value.type&.downcase
|
|
185
|
-
when 'part name', 'part number'
|
|
186
|
-
value = structured_value.value&.strip
|
|
187
|
-
next unless value
|
|
188
|
-
|
|
189
|
-
title_from_part = append_part_to_title(title_from_part, value)
|
|
190
|
-
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
title_from_part
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def append_part_to_title(title_from_part, value)
|
|
197
|
-
if !add_punctuation?
|
|
198
|
-
[title_from_part, value].select(&:presence).join(' ')
|
|
199
|
-
elsif title_from_part.strip.present?
|
|
200
|
-
"#{title_from_part.sub(/[ .,]*$/, '')}, #{value}"
|
|
201
|
-
else
|
|
202
|
-
value
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
# rubocop:enable Metrics/ClassLength
|
|
207
|
-
end
|
|
208
|
-
end
|