cocina-models 0.67.0 → 0.69.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/README.md +19 -0
  4. data/cocina-models.gemspec +4 -1
  5. data/lib/cocina/generator/generator.rb +15 -12
  6. data/lib/cocina/generator/vocab.rb +46 -42
  7. data/lib/cocina/models/access.rb +3 -3
  8. data/lib/cocina/models/admin_policy.rb +1 -1
  9. data/lib/cocina/models/{admin_policy_default_access.rb → admin_policy_access_template.rb} +4 -4
  10. data/lib/cocina/models/admin_policy_administrative.rb +1 -1
  11. data/lib/cocina/models/administrative.rb +0 -3
  12. data/lib/cocina/models/citation_only_access.rb +3 -3
  13. data/lib/cocina/models/collection.rb +5 -5
  14. data/lib/cocina/models/collection_access.rb +1 -1
  15. data/lib/cocina/models/controlled_digital_lending_access.rb +3 -3
  16. data/lib/cocina/models/dark_access.rb +3 -3
  17. data/lib/cocina/models/dro.rb +15 -15
  18. data/lib/cocina/models/dro_access.rb +4 -4
  19. data/lib/cocina/models/dro_rights_description_builder.rb +67 -0
  20. data/lib/cocina/models/embargo.rb +3 -3
  21. data/lib/cocina/models/file.rb +1 -1
  22. data/lib/cocina/models/file_access.rb +3 -3
  23. data/lib/cocina/models/file_set.rb +16 -16
  24. data/lib/cocina/models/file_set_type.rb +25 -0
  25. data/lib/cocina/models/license.rb +10 -0
  26. data/lib/cocina/models/location_based_access.rb +3 -3
  27. data/lib/cocina/models/location_based_download_access.rb +3 -3
  28. data/lib/cocina/models/object_type.rb +31 -0
  29. data/lib/cocina/models/request_admin_policy.rb +1 -1
  30. data/lib/cocina/models/request_administrative.rb +14 -0
  31. data/lib/cocina/models/request_collection.rb +6 -6
  32. data/lib/cocina/models/request_dro.rb +16 -16
  33. data/lib/cocina/models/request_file.rb +1 -1
  34. data/lib/cocina/models/request_file_set.rb +16 -16
  35. data/lib/cocina/models/rights_description_builder.rb +81 -0
  36. data/lib/cocina/models/stanford_access.rb +3 -3
  37. data/lib/cocina/models/title_builder.rb +203 -0
  38. data/lib/cocina/models/version.rb +1 -1
  39. data/lib/cocina/models/vocabulary.rb +30 -0
  40. data/lib/cocina/models/world_access.rb +3 -3
  41. data/lib/cocina/models.rb +26 -7
  42. data/lib/cocina/rspec/matchers.rb +103 -0
  43. data/lib/cocina/rspec.rb +14 -0
  44. data/openapi.yml +132 -124
  45. metadata +39 -10
  46. data/lib/cocina/models/vocab.rb +0 -162
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
 
@@ -42,7 +43,7 @@ module Cocina
42
43
  # Raised when the type attribute is not valid.
43
44
  class UnknownTypeError < Error; end
44
45
 
45
- # Raised when an error occurs validating against openapi.
46
+ # Raised when the type attribute is missing or an error occurs validating against openapi.
46
47
  class ValidationError < Error; end
47
48
 
48
49
  # Base class for Cocina Structs
@@ -56,14 +57,25 @@ module Cocina
56
57
  include Dry.Types()
57
58
  end
58
59
 
60
+ ##
61
+ # Alias for `Cocina::Models::Vocabulary.create`.
62
+ #
63
+ # @param (see Cocina::Models::Vocabulary#initialize)
64
+ # @return [Class]
65
+ # rubocop:disable Naming/MethodName
66
+ def self.Vocabulary(uri)
67
+ Vocabulary.create(uri)
68
+ end
69
+ # rubocop:enable Naming/MethodName
70
+
59
71
  # @param [Hash] dyn a ruby hash representation of the JSON serialization of a collection or DRO
60
72
  # @param [boolean] validate
61
73
  # @return [DRO,Collection,AdminPolicy]
62
74
  # @raises [UnknownTypeError] if a valid type is not found in the data
63
- # @raises [KeyError] if a type field cannot be found in the data
75
+ # @raises [ValidationError] if a type field cannot be found in the data
64
76
  # @raises [ValidationError] if hash representation fails openapi validation
65
77
  def self.build(dyn, validate: true)
66
- clazz = case dyn.fetch('type')
78
+ clazz = case type_for(dyn)
67
79
  when *DRO::TYPES
68
80
  DRO
69
81
  when *Collection::TYPES
@@ -71,7 +83,7 @@ module Cocina
71
83
  when *AdminPolicy::TYPES
72
84
  AdminPolicy
73
85
  else
74
- raise UnknownTypeError, "Unknown type: '#{dyn.fetch('type')}'"
86
+ raise UnknownTypeError, "Unknown type: '#{dyn.with_indifferent_access.fetch('type')}'"
75
87
  end
76
88
  clazz.new(dyn, false, validate)
77
89
  end
@@ -80,10 +92,10 @@ module Cocina
80
92
  # @param [boolean] validate
81
93
  # @return [RequestDRO,RequestCollection,RequestAdminPolicy]
82
94
  # @raises [UnknownTypeError] if a valid type is not found in the data
83
- # @raises [KeyError] if a type field cannot be found in the data
95
+ # @raises [ValidationError] if a type field cannot be found in the data
84
96
  # @raises [ValidationError] if hash representation fails openapi validation
85
97
  def self.build_request(dyn, validate: true)
86
- clazz = case dyn.fetch('type')
98
+ clazz = case type_for(dyn)
87
99
  when *DRO::TYPES
88
100
  RequestDRO
89
101
  when *Collection::TYPES
@@ -91,9 +103,16 @@ module Cocina
91
103
  when *AdminPolicy::TYPES
92
104
  RequestAdminPolicy
93
105
  else
94
- raise UnknownTypeError, "Unknown type: '#{dyn.fetch('type')}'"
106
+ raise UnknownTypeError, "Unknown type: '#{dyn.with_indifferent_access.fetch('type')}'"
95
107
  end
96
108
  clazz.new(dyn, false, validate)
97
109
  end
110
+
111
+ def self.type_for(dyn)
112
+ dyn.with_indifferent_access.fetch('type')
113
+ rescue KeyError
114
+ raise ValidationError, 'Type field not found'
115
+ end
116
+ private_class_method :type_for
98
117
  end
99
118
  end
@@ -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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/matchers'
5
+ if defined?(Rails)
6
+ require 'super_diff/rspec-rails'
7
+ else
8
+ require 'super_diff/rspec'
9
+ end
10
+ require 'cocina/rspec/matchers'
11
+
12
+ RSpec.configure do |config|
13
+ config.include Cocina::RSpec::Matchers
14
+ end