cocina-models 0.124.0 → 0.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -1
  3. data/Gemfile.lock +38 -40
  4. data/bin/validate-data +1 -2
  5. data/iso15924_codes.yml +275 -0
  6. data/language_uri_iso639_2_codes.yml +509 -0
  7. data/language_uri_languages_codes.yml +487 -0
  8. data/lib/cocina/models/admin_policy.rb +1 -1
  9. data/lib/cocina/models/admin_policy_lite.rb +1 -1
  10. data/lib/cocina/models/admin_policy_with_metadata.rb +1 -1
  11. data/lib/cocina/models/collection.rb +1 -1
  12. data/lib/cocina/models/collection_lite.rb +1 -1
  13. data/lib/cocina/models/collection_with_metadata.rb +1 -1
  14. data/lib/cocina/models/contributor.rb +1 -1
  15. data/lib/cocina/models/description.rb +1 -1
  16. data/lib/cocina/models/descriptive_admin_metadata.rb +1 -1
  17. data/lib/cocina/models/descriptive_basic_value.rb +1 -1
  18. data/lib/cocina/models/descriptive_identifier.rb +45 -0
  19. data/lib/cocina/models/descriptive_parallel_event.rb +1 -1
  20. data/lib/cocina/models/descriptive_value.rb +1 -1
  21. data/lib/cocina/models/dro.rb +1 -1
  22. data/lib/cocina/models/dro_lite.rb +1 -1
  23. data/lib/cocina/models/dro_with_metadata.rb +1 -1
  24. data/lib/cocina/models/event.rb +1 -1
  25. data/lib/cocina/models/mapping/from_mods/access.rb +2 -2
  26. data/lib/cocina/models/related_resource.rb +1 -1
  27. data/lib/cocina/models/request_description.rb +1 -1
  28. data/lib/cocina/models/title.rb +1 -1
  29. data/lib/cocina/models/validators/composite_description_validator.rb +7 -1
  30. data/lib/cocina/models/validators/description_language_code_visitor_validator.rb +48 -0
  31. data/lib/cocina/models/validators/description_language_uri_visitor_validator.rb +86 -0
  32. data/lib/cocina/models/validators/description_name_source_code_visitor_validator.rb +45 -0
  33. data/lib/cocina/models/validators/description_script_visitor_validator.rb +74 -0
  34. data/lib/cocina/models/validators/description_subject_source_code_visitor_validator.rb +41 -0
  35. data/lib/cocina/models/validators/description_title_source_code_visitor_validator.rb +43 -0
  36. data/lib/cocina/models/version.rb +1 -1
  37. data/name_source_codes.yml +498 -0
  38. data/name_title_source_codes.yml +46 -0
  39. data/schema.json +109 -55
  40. data/subject_source_codes.yml +848 -0
  41. metadata +15 -2
@@ -28,7 +28,7 @@ module Cocina
28
28
  # Additional information relevant to a related resource.
29
29
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
30
30
  # Identifiers and URIs associated with the related resource.
31
- attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
31
+ attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
32
32
  # Property model for indicating the encoding, standard, or syntax to which a value
33
33
  # conforms (e.g. RDA).
34
34
  attribute? :standard, Standard.optional
@@ -23,7 +23,7 @@ module Cocina
23
23
  # Additional information relevant to a resource.
24
24
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
25
25
  # Identifiers and URIs associated with the resource.
26
- attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
26
+ attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
27
27
  # Terms associated with the intellectual content of the resource.
28
28
  attribute :subject, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
29
29
  # Information about how to access digital and physical versions of the object.
@@ -25,7 +25,7 @@ module Cocina
25
25
  # conforms (e.g. RDA).
26
26
  attribute? :encoding, Standard.optional
27
27
  # Identifiers and URIs associated with the descriptive element.
28
- attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
28
+ attribute :identifier, Types::Strict::Array.of(DescriptiveIdentifier).default([].freeze)
29
29
  # Property model for indicating the vocabulary, authority, or other origin for a term,
30
30
  # code, or identifier.
31
31
  attribute? :source, Source.optional
@@ -9,12 +9,18 @@ module Cocina
9
9
  DescriptionTypesVisitorValidator,
10
10
  DescriptionIdentifierSourceCodeVisitorValidator,
11
11
  DescriptionRoleSourceCodeVisitorValidator,
12
+ DescriptionNameSourceCodeVisitorValidator,
12
13
  DescriptionFormResourceTypeVisitorValidator,
14
+ DescriptionLanguageCodeVisitorValidator,
15
+ DescriptionSubjectSourceCodeVisitorValidator,
13
16
  DescriptionValuesVisitorValidator,
14
17
  DescriptionDateTimeVisitorValidator,
15
18
  DescriptionEventDateVisitorValidator,
16
19
  DescriptionSubjectTemporalEncodingVisitorValidator,
17
- DescriptionLocationSourceCodeVisitorValidator
20
+ DescriptionLocationSourceCodeVisitorValidator,
21
+ DescriptionTitleSourceCodeVisitorValidator,
22
+ DescriptionLanguageUriVisitorValidator,
23
+ DescriptionScriptVisitorValidator
18
24
  ].freeze
19
25
 
20
26
  def self.validate(clazz, attributes)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cocina_display'
4
+
5
+ module Cocina
6
+ module Models
7
+ module Validators
8
+ # Validates language.code values against searchworks_languages from cocina_display gem plus mul, und, zxx.
9
+ class DescriptionLanguageCodeVisitorValidator < BaseDescriptionVisitorValidator
10
+ EXTRA_VALID_CODES = %w[mul und zxx].freeze
11
+
12
+ def validate!
13
+ return if error_paths.empty?
14
+
15
+ raise ValidationError, "Unrecognized language codes in description: #{error_paths.join(', ')}"
16
+ end
17
+
18
+ def visit_hash(hash:, path:)
19
+ return unless language_path?(path)
20
+
21
+ code = hash[:code]
22
+ return unless code
23
+
24
+ error_paths << "#{path_to_s(path)}.code (#{code})" unless valid_codes.include?(code)
25
+ end
26
+
27
+ private
28
+
29
+ def error_paths
30
+ @error_paths ||= []
31
+ end
32
+
33
+ def language_path?(path)
34
+ path.length >= 2 && path[-1].is_a?(Integer) && path[-2].to_s == 'language'
35
+ end
36
+
37
+ # rubocop:disable Style/ClassVars
38
+ def valid_codes
39
+ @@valid_codes ||= begin
40
+ gem_codes = CocinaDisplay::Languages::Language.searchworks_languages.keys.to_set
41
+ gem_codes.merge(EXTRA_VALID_CODES)
42
+ end
43
+ end
44
+ # rubocop:enable Style/ClassVars
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates language.uri and valueLanguage.uri against LC ISO 639-2, LC MARC languages,
7
+ # and 5 approved ISO 639-3 URIs. Both http and https are acceptable.
8
+ class DescriptionLanguageUriVisitorValidator < BaseDescriptionVisitorValidator
9
+ ISO639_3_CODES = %w[ase dnt quc skr trw tta].freeze
10
+
11
+ def visit_hash(hash:, path:)
12
+ return unless language_or_value_language_path?(path)
13
+
14
+ uri = hash[:uri]
15
+ return unless uri
16
+
17
+ error_paths << "#{path_to_s(path)}.uri (#{uri})" unless valid_uri?(uri)
18
+ end
19
+
20
+ def validate!
21
+ return if error_paths.empty?
22
+
23
+ raise ValidationError, "Invalid language URIs in description: #{error_paths.join(', ')}"
24
+ end
25
+
26
+ private
27
+
28
+ def error_paths
29
+ @error_paths ||= []
30
+ end
31
+
32
+ def language_or_value_language_path?(path)
33
+ language_path?(path) || value_language_path?(path)
34
+ end
35
+
36
+ def language_path?(path)
37
+ path.length >= 2 && path[-1].is_a?(Integer) && path[-2].to_s == 'language'
38
+ end
39
+
40
+ def value_language_path?(path)
41
+ path.length >= 1 && path[-1].to_s == 'valueLanguage'
42
+ end
43
+
44
+ def valid_uri?(uri)
45
+ normalized = uri.sub(%r{\Ahttps?://}, '')
46
+ valid_iso639_2_uri?(normalized) || valid_iso639_3_uri?(normalized) || valid_languages_uri?(normalized)
47
+ end
48
+
49
+ def valid_iso639_2_uri?(normalized)
50
+ match = normalized.match(%r{\Aid\.loc\.gov/vocabulary/iso639-2/(.+)\z})
51
+ return false unless match
52
+
53
+ valid_iso639_2_codes.include?(match[1].downcase)
54
+ end
55
+
56
+ def valid_iso639_3_uri?(normalized)
57
+ match = normalized.match(%r{\Aid\.loc\.gov/vocabulary/iso639-3/(.+)\z})
58
+ return false unless match
59
+
60
+ ISO639_3_CODES.include?(match[1].downcase)
61
+ end
62
+
63
+ def valid_languages_uri?(normalized)
64
+ match = normalized.match(%r{\Aid\.loc\.gov/vocabulary/languages/(.+)\z})
65
+ return false unless match
66
+
67
+ valid_languages_codes.include?(match[1].downcase)
68
+ end
69
+
70
+ # rubocop:disable Style/ClassVars
71
+ def valid_iso639_2_codes
72
+ @@valid_iso639_2_codes ||= YAML.load_file(
73
+ ::File.expand_path('../../../../language_uri_iso639_2_codes.yml', __dir__)
74
+ ).to_set(&:downcase)
75
+ end
76
+
77
+ def valid_languages_codes
78
+ @@valid_languages_codes ||= YAML.load_file(
79
+ ::File.expand_path('../../../../language_uri_languages_codes.yml', __dir__)
80
+ ).to_set(&:downcase)
81
+ end
82
+ # rubocop:enable Style/ClassVars
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates contributor.name.source.code values against name_source_codes.yml.
7
+ class DescriptionNameSourceCodeVisitorValidator < BaseDescriptionVisitorValidator
8
+ def validate!
9
+ return if error_paths.empty?
10
+
11
+ raise ValidationError, "Unrecognized name source codes in description: #{error_paths.join(', ')}"
12
+ end
13
+
14
+ def visit_hash(hash:, path:)
15
+ return unless name_path?(path)
16
+
17
+ source_code = hash.dig(:source, :code)
18
+ return unless source_code
19
+ return if valid_codes.include?(source_code.downcase)
20
+
21
+ error_paths << "#{path_to_s(path)}.source.code (#{source_code})"
22
+ end
23
+
24
+ private
25
+
26
+ def error_paths
27
+ @error_paths ||= []
28
+ end
29
+
30
+ def name_path?(path)
31
+ path.length >= 2 &&
32
+ path[-1].is_a?(Integer) &&
33
+ path[-2].to_s == 'name' &&
34
+ path.any? { |part| part.to_s == 'contributor' }
35
+ end
36
+
37
+ # rubocop:disable Style/ClassVars
38
+ def valid_codes
39
+ @@valid_codes ||= YAML.load_file(::File.expand_path('../../../../name_source_codes.yml', __dir__)).to_set(&:downcase)
40
+ end
41
+ # rubocop:enable Style/ClassVars
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates language.script and valueLanguage.valueScript:
7
+ # - source.code, when present, must be 'iso15924' (case-insensitive)
8
+ # - code must be a recognized ISO 15924 script code (when source.code is 'iso15924')
9
+ class DescriptionScriptVisitorValidator < BaseDescriptionVisitorValidator
10
+ def validate!
11
+ raise ValidationError, "Unrecognized script source codes in description: #{source_error_paths.join(', ')}" if source_error_paths.any?
12
+ raise ValidationError, "Unrecognized script codes in description: #{error_paths.join(', ')}" if error_paths.any?
13
+ end
14
+
15
+ def visit_hash(hash:, path:)
16
+ if language_path?(path)
17
+ validate_script(hash, path, :script)
18
+ elsif value_language_path?(path)
19
+ validate_script(hash, path, :valueScript)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def error_paths
26
+ @error_paths ||= []
27
+ end
28
+
29
+ def source_error_paths
30
+ @source_error_paths ||= []
31
+ end
32
+
33
+ # Matches entries in a language array (e.g., [:language, 0] or [:adminMetadata, :language, 0]).
34
+ # Since language is an array of objects, the last element is the integer index
35
+ # and the second-to-last is the key 'language'.
36
+ def language_path?(path)
37
+ path.length >= 2 && path[-1].is_a?(Integer) && path[-2].to_s == 'language'
38
+ end
39
+
40
+ # Matches single valueLanguage objects (e.g., [:title, 0, :valueLanguage]).
41
+ # Since valueLanguage is a single object rather than an array, the path ends
42
+ # directly with 'valueLanguage'.
43
+ def value_language_path?(path)
44
+ path.present? && path[-1].to_s == 'valueLanguage'
45
+ end
46
+
47
+ def validate_script(hash, path, key)
48
+ script = hash[key]
49
+ return unless script
50
+
51
+ source_code = script.dig(:source, :code)
52
+ return unless source_code
53
+
54
+ unless source_code.downcase == 'iso15924'
55
+ source_error_paths << "#{path_to_s(path)}.#{key}.source.code (#{source_code})"
56
+ return
57
+ end
58
+
59
+ code = script[:code]
60
+ return unless code
61
+ return if valid_script_codes.include?(code.downcase)
62
+
63
+ error_paths << "#{path_to_s(path)}.#{key}.code (#{code})"
64
+ end
65
+
66
+ # rubocop:disable Style/ClassVars
67
+ def valid_script_codes
68
+ @@valid_script_codes ||= YAML.load_file(::File.expand_path('../../../../iso15924_codes.yml', __dir__)).to_set
69
+ end
70
+ # rubocop:enable Style/ClassVars
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates subject.source.code values against subject_source_codes.yml.
7
+ class DescriptionSubjectSourceCodeVisitorValidator < BaseDescriptionVisitorValidator
8
+ def validate!
9
+ return if error_paths.empty?
10
+
11
+ raise ValidationError, "Unrecognized subject source codes in description: #{error_paths.join(', ')}"
12
+ end
13
+
14
+ def visit_hash(hash:, path:)
15
+ return unless subject_path?(path)
16
+
17
+ source_code = hash.dig(:source, :code)
18
+ return unless source_code
19
+
20
+ error_paths << "#{path_to_s(path)}.source.code (#{source_code})" unless valid_codes.include?(source_code.downcase)
21
+ end
22
+
23
+ private
24
+
25
+ def error_paths
26
+ @error_paths ||= []
27
+ end
28
+
29
+ def subject_path?(path)
30
+ path.length >= 2 && path[-1].is_a?(Integer) && path[-2].to_s == 'subject'
31
+ end
32
+
33
+ # rubocop:disable Style/ClassVars
34
+ def valid_codes
35
+ @@valid_codes ||= YAML.load_file(::File.expand_path('../../../../subject_source_codes.yml', __dir__)).to_set(&:downcase)
36
+ end
37
+ # rubocop:enable Style/ClassVars
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates title.source.code values against name_title_source_codes.yml.
7
+ class DescriptionTitleSourceCodeVisitorValidator < BaseDescriptionVisitorValidator
8
+ def validate!
9
+ return if error_paths.empty?
10
+
11
+ raise ValidationError, "Unrecognized title source codes in description: #{error_paths.join(', ')}"
12
+ end
13
+
14
+ def visit_hash(hash:, path:)
15
+ return unless title_path?(path)
16
+
17
+ source_code = hash.dig(:source, :code)
18
+ return unless source_code
19
+ return if valid_codes.include?(source_code.downcase)
20
+
21
+ error_paths << "#{path_to_s(path)}.source.code (#{source_code})"
22
+ end
23
+
24
+ private
25
+
26
+ def error_paths
27
+ @error_paths ||= []
28
+ end
29
+
30
+ def title_path?(path)
31
+ # Match entries in a title array (e.g., [:title, 0] or [:relatedResource, 0, :title, 0]).
32
+ path.length >= 2 && path[-1].is_a?(Integer) && path[-2].to_s == 'title'
33
+ end
34
+
35
+ # rubocop:disable Style/ClassVars
36
+ def valid_codes
37
+ @@valid_codes ||= YAML.load_file(::File.expand_path('../../../../name_title_source_codes.yml', __dir__)).to_set(&:downcase)
38
+ end
39
+ # rubocop:enable Style/ClassVars
40
+ end
41
+ end
42
+ end
43
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cocina
4
4
  module Models
5
- VERSION = '0.124.0'
5
+ VERSION = '0.126.0'
6
6
  end
7
7
  end