cocina-models 0.68.0 → 0.69.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/README.md +14 -0
- data/cocina-models.gemspec +1 -0
- data/lib/cocina/generator/generator.rb +14 -6
- data/lib/cocina/generator/vocab.rb +15 -8
- data/lib/cocina/models/dro_rights_description_builder.rb +67 -0
- data/lib/cocina/models/file_set_type.rb +17 -64
- data/lib/cocina/models/license.rb +10 -0
- data/lib/cocina/models/object_type.rb +23 -88
- data/lib/cocina/models/rights_description_builder.rb +81 -0
- data/lib/cocina/models/title_builder.rb +203 -0
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/models/vocabulary.rb +30 -0
- data/lib/cocina/models.rb +14 -0
- data/lib/cocina/rspec/matchers.rb +103 -0
- data/lib/cocina/rspec.rb +10 -0
- metadata +27 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 191508cff01be391223c83aba3e7c085e99b581a0f12a95dcd627572fbe5be36
|
4
|
+
data.tar.gz: b4eb3927706c3a5316f30d03d0352e742b3b0a72bf966cb9b0ed6bb593a0d48d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a4e9c462003e94e13992d20e2adc1a88515185820c11f851b0e709913addb541efadb35d11a0c841cb563e26d0b98cea15b0c0cc9d14aeddeb93f5bc953b147
|
7
|
+
data.tar.gz: 501f90b85f9386ff832b4147483d6e46fd6e8735fe87de6b868e7f68e2c6a02f1cf71759cb649103bcff93fb9e5830c11cd3ac0387e85336821237f8add0ee71
|
data/.rubocop.yml
CHANGED
@@ -78,6 +78,7 @@ Metrics/BlockLength:
|
|
78
78
|
Exclude:
|
79
79
|
- cocina-models.gemspec
|
80
80
|
- spec/cocina/**/*
|
81
|
+
- lib/cocina/rspec/matchers.rb
|
81
82
|
|
82
83
|
Metrics/MethodLength:
|
83
84
|
Max: 14
|
@@ -105,6 +106,9 @@ RSpec/MultipleExpectations:
|
|
105
106
|
RSpec/StubbedMock: # (new in 1.44)
|
106
107
|
Enabled: true
|
107
108
|
|
109
|
+
RSpec/MultipleMemoizedHelpers:
|
110
|
+
Max: 6
|
111
|
+
|
108
112
|
# ----- Style ------
|
109
113
|
|
110
114
|
Style/Documentation:
|
@@ -276,3 +280,5 @@ RSpec/FactoryBot/SyntaxMethods: # new in 2.7
|
|
276
280
|
Enabled: true
|
277
281
|
RSpec/Rails/AvoidSetupHook: # new in 2.4
|
278
282
|
Enabled: true
|
283
|
+
Style/NestedFileDirname: # new in 1.26
|
284
|
+
Enabled: true
|
data/README.md
CHANGED
@@ -103,3 +103,17 @@ The following are the recommended naming conventions for code using Cocina model
|
|
103
103
|
* `cocina_admin_policy`: `Cocina::Models::AdminPolicy` instance
|
104
104
|
* `cocina_collection`: `Cocina::Models::Collection` instance
|
105
105
|
* `cocina_object`: `Cocina::Models::DRO` or `Cocina::Models::AdminPolicy` or `Cocina::Models::Collection` instance
|
106
|
+
|
107
|
+
## RSpec matchers
|
108
|
+
|
109
|
+
As of the 0.69.0 release, the `cocina-models` gem provides RSpec matchers for downstream apps to make it easier to compare Cocina data structures. The matchers provided include:
|
110
|
+
|
111
|
+
* `equal_cocina_model`: Compare a Cocina JSON string with a model instance. This matcher is especially valuable coupled with the `super_diff` gem (a dependency of `cocina-models` since the 0.69.0 release). Example usage:
|
112
|
+
* `expect(http_response_body_with_cocina_json).to equal_cocina_model(cocina_instance)`
|
113
|
+
* `cocina_object_with` (AKA `match_cocina_object_with`): Compare a Cocina model instance with a hash containining part of the structure of a Cocina object. Example usage:
|
114
|
+
* `expect(CocinaObjectStore).to have_received(:save).with(cocina_object_with(access: { view: 'world' }, structural: { contains: [...] }))`
|
115
|
+
* expect(updated_cocina_item).to match_cocina_object_with(structural: { hasMemberOrders: [] })
|
116
|
+
* `cocina_object_with_types`: Check a Cocina object's type information. Example usage:
|
117
|
+
* `expect(object_client).to have_received(:update).with(params: cocina_object_with_types(content_type: Cocina::Models::ObjectType.book, viewing_direction: 'left-to-right'))`
|
118
|
+
* `cocina_admin_policy_with_registration_collections`: Check a Cocina admin policy's collections. Example usage:
|
119
|
+
* `expect(object_client).to have_received(:update).with(params: cocina_admin_policy_with_registration_collections([collection_id]))`
|
data/cocina-models.gemspec
CHANGED
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
# Match these version requirements to what committee wants,
|
32
32
|
# so that our client (non-committee) users have the same dependencies.
|
33
33
|
spec.add_dependency 'openapi_parser', '>= 0.11.1', '< 1.0'
|
34
|
+
spec.add_dependency 'super_diff'
|
34
35
|
spec.add_dependency 'thor'
|
35
36
|
spec.add_dependency 'zeitwerk', '~> 2.1'
|
36
37
|
|
@@ -71,18 +71,26 @@ module Cocina
|
|
71
71
|
run("rubocop -a #{filepath} > /dev/null")
|
72
72
|
end
|
73
73
|
|
74
|
-
|
74
|
+
NO_CLEAN = [
|
75
|
+
'checkable.rb',
|
76
|
+
'dro_rights_description_builder.rb',
|
77
|
+
'license.rb',
|
78
|
+
'rights_description_builder.rb',
|
79
|
+
'title_builder.rb',
|
80
|
+
'validatable.rb',
|
81
|
+
'validator.rb',
|
82
|
+
'version.rb',
|
83
|
+
'vocabulary.rb'
|
84
|
+
].freeze
|
85
|
+
|
75
86
|
def clean_output
|
76
87
|
FileUtils.mkdir_p(options[:output])
|
77
88
|
files = Dir.glob("#{options[:output]}/*.rb")
|
78
89
|
# Leave alone
|
79
|
-
files.delete("#{options[:output]}
|
80
|
-
|
81
|
-
files.delete("#{options[:output]}/validator.rb")
|
82
|
-
files.delete("#{options[:output]}/validatable.rb")
|
90
|
+
NO_CLEAN.each { |filename| files.delete("#{options[:output]}/#{filename}") }
|
91
|
+
|
83
92
|
FileUtils.rm_f(files)
|
84
93
|
end
|
85
|
-
# rubocop:enable Metrics/AbcSize
|
86
94
|
end
|
87
95
|
end
|
88
96
|
end
|
@@ -32,7 +32,7 @@ module Cocina
|
|
32
32
|
module Cocina
|
33
33
|
module Models
|
34
34
|
# #{CLASS_COMMENT.fetch(namespace)}
|
35
|
-
class #{namespace}
|
35
|
+
class #{namespace} < Vocabulary('#{URIS.fetch(namespace)}')
|
36
36
|
#{draw_ruby_methods(methods, 6)}
|
37
37
|
end
|
38
38
|
end
|
@@ -45,6 +45,10 @@ module Cocina
|
|
45
45
|
attr_reader :schemas, :output_dir
|
46
46
|
|
47
47
|
BASE = 'https://cocina.sul.stanford.edu/models/'
|
48
|
+
URIS = {
|
49
|
+
'ObjectType' => BASE,
|
50
|
+
'FileSetType' => "#{BASE}resources/"
|
51
|
+
}.freeze
|
48
52
|
|
49
53
|
def vocabs
|
50
54
|
type_properties = schemas.values.map { |schema| schema.properties['type'] }.compact
|
@@ -56,21 +60,24 @@ module Cocina
|
|
56
60
|
|
57
61
|
def names
|
58
62
|
@names ||= vocabs.each_with_object({}) do |vocab, object|
|
59
|
-
# Note special handling of 3d
|
60
63
|
namespaced = vocab.delete_prefix(BASE)
|
61
|
-
.gsub('-', '_').gsub('3d', 'three_dimensional')
|
62
64
|
namespace, name = namespaced.include?('/') ? namespaced.split('/') : ['ObjectType', namespaced]
|
63
65
|
namespace = 'FileSetType' if namespace == 'resources'
|
64
|
-
object[namespace] ||=
|
65
|
-
object[namespace]
|
66
|
+
object[namespace] ||= []
|
67
|
+
object[namespace] << name
|
66
68
|
end
|
67
69
|
end
|
68
70
|
|
69
71
|
def draw_ruby_methods(methods, indent)
|
70
72
|
spaces = ' ' * indent
|
71
|
-
methods.map do |name
|
72
|
-
|
73
|
-
|
73
|
+
methods.map do |name|
|
74
|
+
if name == '3d'
|
75
|
+
"#{spaces}property :'3d', method_name: :three_dimensional"
|
76
|
+
else
|
77
|
+
name = "'#{name}'" if name.match?(/(^\d)|-/)
|
78
|
+
"#{spaces}property :#{name}"
|
79
|
+
end
|
80
|
+
end.join("\n")
|
74
81
|
end
|
75
82
|
end
|
76
83
|
end
|
@@ -0,0 +1,67 @@
|
|
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
|
@@ -3,70 +3,23 @@
|
|
3
3
|
module Cocina
|
4
4
|
module Models
|
5
5
|
# This vocabulary defines the types of file sets
|
6
|
-
class FileSetType
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
def self.file
|
24
|
-
'https://cocina.sul.stanford.edu/models/resources/file'
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.image
|
28
|
-
'https://cocina.sul.stanford.edu/models/resources/image'
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.main_augmented
|
32
|
-
'https://cocina.sul.stanford.edu/models/resources/main-augmented'
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.main_original
|
36
|
-
'https://cocina.sul.stanford.edu/models/resources/main-original'
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.media
|
40
|
-
'https://cocina.sul.stanford.edu/models/resources/media'
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.object
|
44
|
-
'https://cocina.sul.stanford.edu/models/resources/object'
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.page
|
48
|
-
'https://cocina.sul.stanford.edu/models/resources/page'
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.permissions
|
52
|
-
'https://cocina.sul.stanford.edu/models/resources/permissions'
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.preview
|
56
|
-
'https://cocina.sul.stanford.edu/models/resources/preview'
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.supplement
|
60
|
-
'https://cocina.sul.stanford.edu/models/resources/supplement'
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.thumb
|
64
|
-
'https://cocina.sul.stanford.edu/models/resources/thumb'
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.video
|
68
|
-
'https://cocina.sul.stanford.edu/models/resources/video'
|
69
|
-
end
|
6
|
+
class FileSetType < Vocabulary('https://cocina.sul.stanford.edu/models/resources/')
|
7
|
+
property :'3d', method_name: :three_dimensional
|
8
|
+
property :attachment
|
9
|
+
property :audio
|
10
|
+
property :document
|
11
|
+
property :file
|
12
|
+
property :image
|
13
|
+
property :'main-augmented'
|
14
|
+
property :'main-original'
|
15
|
+
property :media
|
16
|
+
property :object
|
17
|
+
property :page
|
18
|
+
property :permissions
|
19
|
+
property :preview
|
20
|
+
property :supplement
|
21
|
+
property :thumb
|
22
|
+
property :video
|
70
23
|
end
|
71
24
|
end
|
72
25
|
end
|
@@ -3,94 +3,29 @@
|
|
3
3
|
module Cocina
|
4
4
|
module Models
|
5
5
|
# This vocabulary defines the top level object type
|
6
|
-
class ObjectType
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.document
|
32
|
-
'https://cocina.sul.stanford.edu/models/document'
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.exhibit
|
36
|
-
'https://cocina.sul.stanford.edu/models/exhibit'
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.file
|
40
|
-
'https://cocina.sul.stanford.edu/models/file'
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.geo
|
44
|
-
'https://cocina.sul.stanford.edu/models/geo'
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.image
|
48
|
-
'https://cocina.sul.stanford.edu/models/image'
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.manuscript
|
52
|
-
'https://cocina.sul.stanford.edu/models/manuscript'
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.map
|
56
|
-
'https://cocina.sul.stanford.edu/models/map'
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.media
|
60
|
-
'https://cocina.sul.stanford.edu/models/media'
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.object
|
64
|
-
'https://cocina.sul.stanford.edu/models/object'
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.page
|
68
|
-
'https://cocina.sul.stanford.edu/models/page'
|
69
|
-
end
|
70
|
-
|
71
|
-
def self.photograph
|
72
|
-
'https://cocina.sul.stanford.edu/models/photograph'
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.series
|
76
|
-
'https://cocina.sul.stanford.edu/models/series'
|
77
|
-
end
|
78
|
-
|
79
|
-
def self.track
|
80
|
-
'https://cocina.sul.stanford.edu/models/track'
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.user_collection
|
84
|
-
'https://cocina.sul.stanford.edu/models/user-collection'
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.webarchive_binary
|
88
|
-
'https://cocina.sul.stanford.edu/models/webarchive-binary'
|
89
|
-
end
|
90
|
-
|
91
|
-
def self.webarchive_seed
|
92
|
-
'https://cocina.sul.stanford.edu/models/webarchive-seed'
|
93
|
-
end
|
6
|
+
class ObjectType < Vocabulary('https://cocina.sul.stanford.edu/models/')
|
7
|
+
property :'3d', method_name: :three_dimensional
|
8
|
+
property :admin_policy
|
9
|
+
property :agreement
|
10
|
+
property :book
|
11
|
+
property :collection
|
12
|
+
property :'curated-collection'
|
13
|
+
property :document
|
14
|
+
property :exhibit
|
15
|
+
property :file
|
16
|
+
property :geo
|
17
|
+
property :image
|
18
|
+
property :manuscript
|
19
|
+
property :map
|
20
|
+
property :media
|
21
|
+
property :object
|
22
|
+
property :page
|
23
|
+
property :photograph
|
24
|
+
property :series
|
25
|
+
property :track
|
26
|
+
property :'user-collection'
|
27
|
+
property :'webarchive-binary'
|
28
|
+
property :'webarchive-seed'
|
94
29
|
end
|
95
30
|
end
|
96
31
|
end
|
@@ -0,0 +1,81 @@
|
|
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
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
# TitleBuilder selects the prefered title from the cocina object for solr indexing
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class TitleBuilder
|
8
|
+
# @param [Cocina::Models::D*] cocina_object is the object to extract the title for
|
9
|
+
# @param [Symbol] strategy ":first" is the strategy for selection when primary or display title are missing
|
10
|
+
# @param [Boolean] add_punctuation determines if the title should be formmated with punctuation
|
11
|
+
# @return [String] the title value for Solr
|
12
|
+
def self.build(cocina_object, strategy: :first, add_punctuation: true)
|
13
|
+
new(cocina_object, strategy: strategy, add_punctuation: add_punctuation).build_title
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(cocina_object, strategy:, add_punctuation:)
|
17
|
+
@cocina_object = cocina_object
|
18
|
+
@strategy = strategy
|
19
|
+
@add_punctuation = add_punctuation
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String] the title value for Solr
|
23
|
+
def build_title
|
24
|
+
@titles = cocina_object.description.title
|
25
|
+
|
26
|
+
cocina_title = primary_title || untyped_title
|
27
|
+
cocina_title = other_title if cocina_title.blank?
|
28
|
+
|
29
|
+
if strategy == :first
|
30
|
+
extract_title(cocina_title)
|
31
|
+
else
|
32
|
+
cocina_title.map { |one| extract_title(one) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :cocina_object, :strategy, :titles
|
39
|
+
|
40
|
+
def extract_title(cocina_title)
|
41
|
+
result = if cocina_title.value
|
42
|
+
cocina_title.value
|
43
|
+
elsif cocina_title.structuredValue.present?
|
44
|
+
title_from_structured_values(cocina_title.structuredValue,
|
45
|
+
non_sorting_char_count(cocina_title))
|
46
|
+
elsif cocina_title.parallelValue.present?
|
47
|
+
return cocina_title.parallelValue.first.value
|
48
|
+
end
|
49
|
+
remove_trailing_punctuation(result.strip) if result.present?
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_punctuation?
|
53
|
+
@add_punctuation
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Cocina::Models::Title, nil] title that has status=primary
|
57
|
+
def primary_title
|
58
|
+
primary_title = titles.find do |title|
|
59
|
+
title.status == 'primary'
|
60
|
+
end
|
61
|
+
return primary_title if primary_title.present?
|
62
|
+
|
63
|
+
# NOTE: structuredValues would only have status primary assigned as a sibling, not as an attribute
|
64
|
+
|
65
|
+
titles.find do |title|
|
66
|
+
title.parallelValue&.find do |parallel_title|
|
67
|
+
parallel_title.status == 'primary'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def untyped_title
|
73
|
+
method = strategy == :first ? :find : :select
|
74
|
+
untyped_title_for(titles.public_send(method))
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Array[Cocina::Models::Title]] first title that has no type attribute
|
78
|
+
def untyped_title_for(titles)
|
79
|
+
titles.each do |title|
|
80
|
+
if title.parallelValue.present?
|
81
|
+
untyped_title_for(title.parallelValue)
|
82
|
+
else
|
83
|
+
title.type.nil? || title.type == 'title'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# This handles 'main title', 'uniform' or 'translated'
|
89
|
+
def other_title
|
90
|
+
if strategy == :first
|
91
|
+
titles.first
|
92
|
+
else
|
93
|
+
titles
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# rubocop:disable Metrics/BlockLength
|
98
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
99
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
100
|
+
# rubocop:disable Metrics/MethodLength
|
101
|
+
# rubocop:disable Metrics/AbcSize
|
102
|
+
# @param [Array<Cocina::Models::StructuredValue>] structured_values - the pieces of a structuredValue
|
103
|
+
# @param [Integer] the length of the non_sorting_characters
|
104
|
+
# @return [String] the title value from combining the pieces of the structured_values by type and order
|
105
|
+
# with desired punctuation per specs
|
106
|
+
def title_from_structured_values(structured_values, non_sorting_char_count)
|
107
|
+
structured_title = ''
|
108
|
+
part_name_number = ''
|
109
|
+
# combine pieces of the cocina structuredValue into a single title
|
110
|
+
structured_values.each do |structured_value|
|
111
|
+
# There can be a structuredValue inside a structuredValue. For example,
|
112
|
+
# a uniform title where both the name and the title have internal StructuredValue
|
113
|
+
if structured_value.structuredValue.present?
|
114
|
+
return title_from_structured_values(structured_value.structuredValue, non_sorting_char_count)
|
115
|
+
end
|
116
|
+
|
117
|
+
value = structured_value.value&.strip
|
118
|
+
next unless value
|
119
|
+
|
120
|
+
# additional types: name, uniform ...
|
121
|
+
case structured_value.type&.downcase
|
122
|
+
when 'nonsorting characters'
|
123
|
+
non_sorting_size = [non_sorting_char_count - (value&.size || 0), 0].max
|
124
|
+
non_sort_value = "#{value}#{' ' * non_sorting_size}"
|
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(structured_values)
|
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_char_count(title)
|
167
|
+
non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
|
168
|
+
return 0 unless non_sort_note
|
169
|
+
|
170
|
+
non_sort_note.value.to_i
|
171
|
+
end
|
172
|
+
|
173
|
+
# combine part name and part number:
|
174
|
+
# respect order of occurrence
|
175
|
+
# separated from each other by comma space
|
176
|
+
def part_name_number(structured_values)
|
177
|
+
title_from_part = ''
|
178
|
+
structured_values.each do |structured_value|
|
179
|
+
case structured_value.type&.downcase
|
180
|
+
when 'part name', 'part number'
|
181
|
+
value = structured_value.value&.strip
|
182
|
+
next unless value
|
183
|
+
|
184
|
+
title_from_part = append_part_to_title(title_from_part, value)
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|
188
|
+
title_from_part
|
189
|
+
end
|
190
|
+
|
191
|
+
def append_part_to_title(title_from_part, value)
|
192
|
+
if !add_punctuation?
|
193
|
+
[title_from_part, value].select(&:presence).join(' ')
|
194
|
+
elsif title_from_part.strip.present?
|
195
|
+
"#{title_from_part.sub(/[ .,]*$/, '')}, #{value}"
|
196
|
+
else
|
197
|
+
value
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
# rubocop:enable Metrics/ClassLength
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
class Vocabulary
|
6
|
+
# @private
|
7
|
+
# Disabled this cop because we want @@uri to be inheritable.
|
8
|
+
# rubocop:disable Style/ClassVars
|
9
|
+
def self.create(uri)
|
10
|
+
@@uri = uri
|
11
|
+
self
|
12
|
+
end
|
13
|
+
# rubocop:enable Style/ClassVars
|
14
|
+
|
15
|
+
def self.to_s
|
16
|
+
@@uri
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.property(name, method_name: name.to_s.underscore.to_sym)
|
20
|
+
uri = [to_s, name].join
|
21
|
+
properties[name] = uri
|
22
|
+
(class << self; self; end).send(:define_method, method_name) { uri }
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.properties
|
26
|
+
@properties ||= {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/cocina/models.rb
CHANGED
@@ -22,6 +22,7 @@ class CocinaModelsInflector < Zeitwerk::Inflector
|
|
22
22
|
'dro_access' => 'DROAccess',
|
23
23
|
'dro_structural' => 'DROStructural',
|
24
24
|
'request_dro_structural' => 'RequestDROStructural',
|
25
|
+
'rspec' => 'RSpec',
|
25
26
|
'version' => 'VERSION'
|
26
27
|
}.freeze
|
27
28
|
|
@@ -33,6 +34,8 @@ end
|
|
33
34
|
loader = Zeitwerk::Loader.new
|
34
35
|
loader.inflector = CocinaModelsInflector.new
|
35
36
|
loader.push_dir(File.absolute_path("#{__FILE__}/../.."))
|
37
|
+
loader.ignore("#{__dir__}/rspec.rb")
|
38
|
+
loader.ignore("#{__dir__}/rspec/**/*.rb")
|
36
39
|
loader.setup
|
37
40
|
|
38
41
|
module Cocina
|
@@ -56,6 +59,17 @@ module Cocina
|
|
56
59
|
include Dry.Types()
|
57
60
|
end
|
58
61
|
|
62
|
+
##
|
63
|
+
# Alias for `Cocina::Models::Vocabulary.create`.
|
64
|
+
#
|
65
|
+
# @param (see Cocina::Models::Vocabulary#initialize)
|
66
|
+
# @return [Class]
|
67
|
+
# rubocop:disable Naming/MethodName
|
68
|
+
def self.Vocabulary(uri)
|
69
|
+
Vocabulary.create(uri)
|
70
|
+
end
|
71
|
+
# rubocop:enable Naming/MethodName
|
72
|
+
|
59
73
|
# @param [Hash] dyn a ruby hash representation of the JSON serialization of a collection or DRO
|
60
74
|
# @param [boolean] validate
|
61
75
|
# @return [DRO,Collection,AdminPolicy]
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Provides RSpec matchers for Cocina models
|
4
|
+
module Cocina
|
5
|
+
module RSpec
|
6
|
+
# Cocina-related RSpec matchers
|
7
|
+
module Matchers
|
8
|
+
extend ::RSpec::Matchers::DSL
|
9
|
+
|
10
|
+
# NOTE: each k/v pair in the hash passed to this matcher will need to be present in actual
|
11
|
+
matcher :cocina_object_with do |**kwargs|
|
12
|
+
kwargs.each do |cocina_section, expected|
|
13
|
+
match do |actual|
|
14
|
+
expected.all? do |expected_key, expected_value|
|
15
|
+
# NOTE: there's no better method on Hash that I could find for this.
|
16
|
+
# #include? and #member? only check keys, not k/v pairs
|
17
|
+
actual.public_send(cocina_section).to_h.any? do |actual_key, actual_value|
|
18
|
+
if expected_value.is_a?(Hash) && actual_value.is_a?(Hash)
|
19
|
+
expected_value.all? { |pair| actual_value.to_a.include?(pair) }
|
20
|
+
else
|
21
|
+
actual_key == expected_key && actual_value == expected_value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
alias_matcher :match_cocina_object_with, :cocina_object_with
|
30
|
+
|
31
|
+
# The `equal_cocina_model` matcher compares a JSON string as actual value
|
32
|
+
# against a Cocina model coerced to JSON as expected value. We want to compare
|
33
|
+
# these as JSON rather than as hashes, else we'll have to start
|
34
|
+
# deep-converting some values within the hashes, thinking of date/time values
|
35
|
+
# in particular. This matching behavior continues what dor-services-app was
|
36
|
+
# already doing before this custom matcher was written.
|
37
|
+
#
|
38
|
+
# Note, though, that when actual and expected values do *not* match, we coerce
|
39
|
+
# both values to hashes to allow the `super_diff` gem to highlight the areas
|
40
|
+
# that differ. This is easier to scan than two giant JSON strings.
|
41
|
+
matcher :equal_cocina_model do |expected|
|
42
|
+
match do |actual|
|
43
|
+
Cocina::Models.build(JSON.parse(actual)).to_json == expected.to_json
|
44
|
+
rescue NoMethodError
|
45
|
+
warn "Could not match cocina models because expected is not a valid JSON string: #{expected}"
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
failure_message do |actual|
|
50
|
+
SuperDiff::EqualityMatchers::Hash.new(
|
51
|
+
expected: expected.to_h.deep_symbolize_keys,
|
52
|
+
actual: JSON.parse(actual, symbolize_names: true)
|
53
|
+
).fail
|
54
|
+
rescue StandardError => e
|
55
|
+
"ERROR in CocinaMatchers: #{e}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
matcher :cocina_object_with_types do |expected|
|
60
|
+
match do |actual|
|
61
|
+
return false if expected[:without_order] && !match_no_orders?
|
62
|
+
|
63
|
+
if expected[:content_type] && expected[:resource_types]
|
64
|
+
match_cocina_type?(actual, expected) && match_contained_cocina_types?(actual, expected)
|
65
|
+
elsif expected[:content_type] && expected[:viewing_direction]
|
66
|
+
match_cocina_type?(actual, expected) && match_cocina_viewing_direction?(actual, expected)
|
67
|
+
elsif expected[:content_type]
|
68
|
+
match_cocina_type?(actual, expected)
|
69
|
+
elsif expected[:resource_types]
|
70
|
+
match_contained_cocina_types?(actual, expected)
|
71
|
+
else
|
72
|
+
raise ArgumentError, 'must provide content_type and/or resource_types keyword args'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def match_no_orders?
|
77
|
+
actual.structural.hasMemberOrders.blank?
|
78
|
+
end
|
79
|
+
|
80
|
+
def match_cocina_type?(actual, expected)
|
81
|
+
actual.type == expected[:content_type]
|
82
|
+
end
|
83
|
+
|
84
|
+
def match_cocina_viewing_direction?(actual, expected)
|
85
|
+
actual.structural.hasMemberOrders.map(&:viewingDirection).all? do |viewing_direction|
|
86
|
+
viewing_direction == expected[:viewing_direction]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def match_contained_cocina_types?(actual, expected)
|
91
|
+
Array(actual.structural.contains).map(&:type).all? { |type| type.in?(expected[:resource_types]) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
matcher :cocina_admin_policy_with_registration_collections do |expected|
|
96
|
+
match do |actual|
|
97
|
+
actual.type == Cocina::Models::ObjectType.admin_policy &&
|
98
|
+
expected.all? { |collection_id| collection_id.in?(actual.administrative.collectionsForRegistration) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/cocina/rspec.rb
ADDED
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.69.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Coyne
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -86,6 +86,20 @@ dependencies:
|
|
86
86
|
- - "<"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '1.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: super_diff
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
89
103
|
- !ruby/object:Gem::Dependency
|
90
104
|
name: thor
|
91
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -308,6 +322,7 @@ files:
|
|
308
322
|
- lib/cocina/models/doi.rb
|
309
323
|
- lib/cocina/models/dro.rb
|
310
324
|
- lib/cocina/models/dro_access.rb
|
325
|
+
- lib/cocina/models/dro_rights_description_builder.rb
|
311
326
|
- lib/cocina/models/dro_structural.rb
|
312
327
|
- lib/cocina/models/druid.rb
|
313
328
|
- lib/cocina/models/embargo.rb
|
@@ -322,6 +337,7 @@ files:
|
|
322
337
|
- lib/cocina/models/identification.rb
|
323
338
|
- lib/cocina/models/lane_medical_barcode.rb
|
324
339
|
- lib/cocina/models/language.rb
|
340
|
+
- lib/cocina/models/license.rb
|
325
341
|
- lib/cocina/models/location_based_access.rb
|
326
342
|
- lib/cocina/models/location_based_download_access.rb
|
327
343
|
- lib/cocina/models/message_digest.rb
|
@@ -340,6 +356,7 @@ files:
|
|
340
356
|
- lib/cocina/models/request_file_set.rb
|
341
357
|
- lib/cocina/models/request_file_set_structural.rb
|
342
358
|
- lib/cocina/models/request_identification.rb
|
359
|
+
- lib/cocina/models/rights_description_builder.rb
|
343
360
|
- lib/cocina/models/sequence.rb
|
344
361
|
- lib/cocina/models/source.rb
|
345
362
|
- lib/cocina/models/source_id.rb
|
@@ -347,16 +364,20 @@ files:
|
|
347
364
|
- lib/cocina/models/standard_barcode.rb
|
348
365
|
- lib/cocina/models/stanford_access.rb
|
349
366
|
- lib/cocina/models/title.rb
|
367
|
+
- lib/cocina/models/title_builder.rb
|
350
368
|
- lib/cocina/models/validatable.rb
|
351
369
|
- lib/cocina/models/validator.rb
|
352
370
|
- lib/cocina/models/version.rb
|
371
|
+
- lib/cocina/models/vocabulary.rb
|
353
372
|
- lib/cocina/models/world_access.rb
|
373
|
+
- lib/cocina/rspec.rb
|
374
|
+
- lib/cocina/rspec/matchers.rb
|
354
375
|
- openapi.yml
|
355
376
|
homepage: https://github.com/sul-dlss/cocina-models
|
356
377
|
licenses: []
|
357
378
|
metadata:
|
358
379
|
rubygems_mfa_required: 'true'
|
359
|
-
post_install_message:
|
380
|
+
post_install_message:
|
360
381
|
rdoc_options: []
|
361
382
|
require_paths:
|
362
383
|
- lib
|
@@ -371,8 +392,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
371
392
|
- !ruby/object:Gem::Version
|
372
393
|
version: '0'
|
373
394
|
requirements: []
|
374
|
-
rubygems_version: 3.
|
375
|
-
signing_key:
|
395
|
+
rubygems_version: 3.2.32
|
396
|
+
signing_key:
|
376
397
|
specification_version: 4
|
377
398
|
summary: Data models for the SDR
|
378
399
|
test_files: []
|