valkyrie 2.0.0 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +60 -56
  3. data/.lando.yml +40 -0
  4. data/.rubocop.yml +4 -1
  5. data/.tool-versions +1 -1
  6. data/Appraisals +4 -4
  7. data/CHANGELOG.md +136 -0
  8. data/README.md +21 -49
  9. data/Rakefile +21 -20
  10. data/db/config.yml +3 -10
  11. data/db/schema.rb +0 -40
  12. data/gemfiles/activerecord_5_2.gemfile +2 -0
  13. data/gemfiles/{activerecord_5_1.gemfile → activerecord_6_0.gemfile} +3 -1
  14. data/lib/generators/valkyrie/resource_generator.rb +3 -3
  15. data/lib/valkyrie.rb +33 -15
  16. data/lib/valkyrie/change_set.rb +3 -3
  17. data/lib/valkyrie/id.rb +26 -3
  18. data/lib/valkyrie/indexers/access_controls_indexer.rb +17 -17
  19. data/lib/valkyrie/logging.rb +72 -0
  20. data/lib/valkyrie/persistence/composite_persister.rb +1 -1
  21. data/lib/valkyrie/persistence/fedora.rb +2 -0
  22. data/lib/valkyrie/persistence/fedora/list_node.rb +46 -49
  23. data/lib/valkyrie/persistence/fedora/metadata_adapter.rb +2 -2
  24. data/lib/valkyrie/persistence/fedora/ordered_list.rb +90 -90
  25. data/lib/valkyrie/persistence/fedora/ordered_reader.rb +5 -5
  26. data/lib/valkyrie/persistence/fedora/permissive_schema.rb +3 -3
  27. data/lib/valkyrie/persistence/fedora/persister.rb +82 -83
  28. data/lib/valkyrie/persistence/fedora/persister/model_converter.rb +16 -17
  29. data/lib/valkyrie/persistence/fedora/persister/orm_converter.rb +38 -18
  30. data/lib/valkyrie/persistence/fedora/query_service.rb +54 -53
  31. data/lib/valkyrie/persistence/memory/persister.rb +33 -33
  32. data/lib/valkyrie/persistence/memory/query_service.rb +52 -34
  33. data/lib/valkyrie/persistence/postgres/orm_converter.rb +52 -52
  34. data/lib/valkyrie/persistence/postgres/query_service.rb +86 -33
  35. data/lib/valkyrie/persistence/postgres/resource_converter.rb +1 -1
  36. data/lib/valkyrie/persistence/shared/json_value_mapper.rb +4 -2
  37. data/lib/valkyrie/persistence/solr/model_converter.rb +337 -337
  38. data/lib/valkyrie/persistence/solr/orm_converter.rb +3 -3
  39. data/lib/valkyrie/persistence/solr/persister.rb +4 -17
  40. data/lib/valkyrie/persistence/solr/queries/find_all_query.rb +6 -0
  41. data/lib/valkyrie/persistence/solr/queries/find_members_query.rb +1 -1
  42. data/lib/valkyrie/persistence/solr/query_service.rb +42 -53
  43. data/lib/valkyrie/persistence/solr/repository.rb +2 -1
  44. data/lib/valkyrie/rdf_patches.rb +2 -2
  45. data/lib/valkyrie/resource.rb +36 -5
  46. data/lib/valkyrie/specs/shared_specs/change_set.rb +1 -1
  47. data/lib/valkyrie/specs/shared_specs/persister.rb +17 -6
  48. data/lib/valkyrie/specs/shared_specs/queries.rb +112 -9
  49. data/lib/valkyrie/storage/fedora.rb +17 -17
  50. data/lib/valkyrie/storage_adapter.rb +16 -13
  51. data/lib/valkyrie/types.rb +3 -1
  52. data/lib/valkyrie/version.rb +1 -1
  53. data/solr/config/solrconfig.xml +0 -10
  54. data/tasks/dev.rake +14 -51
  55. data/valkyrie.gemspec +4 -4
  56. metadata +40 -37
  57. data/.docker-stack/valkyrie-development/docker-compose.yml +0 -53
  58. data/.docker-stack/valkyrie-test/docker-compose.yml +0 -53
  59. data/db/seeds.rb +0 -8
  60. data/tasks/docker.rake +0 -31
@@ -21,65 +21,65 @@ module Valkyrie::Persistence::Postgres
21
21
 
22
22
  private
23
23
 
24
- # Construct a new Valkyrie Resource using the attributes retrieved from the database
25
- # @return [Valkyrie::Resource]
26
- def resource
27
- resource_klass.new(
28
- attributes.merge(
29
- new_record: false,
30
- Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK => lock_token
31
- )
24
+ # Construct a new Valkyrie Resource using the attributes retrieved from the database
25
+ # @return [Valkyrie::Resource]
26
+ def resource
27
+ resource_klass.new(
28
+ attributes.merge(
29
+ new_record: false,
30
+ Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK => lock_token
32
31
  )
33
- end
32
+ )
33
+ end
34
34
 
35
- # Construct the optimistic lock token using the adapter and lock version for the Resource
36
- # @return [Valkyrie::Persistence::OptimisticLockToken]
37
- def lock_token
38
- return lock_token_warning unless orm_object.class.column_names.include?("lock_version")
39
- @lock_token ||=
40
- Valkyrie::Persistence::OptimisticLockToken.new(
41
- adapter_id: resource_factory.adapter_id,
42
- token: orm_object.lock_version
43
- )
44
- end
35
+ # Construct the optimistic lock token using the adapter and lock version for the Resource
36
+ # @return [Valkyrie::Persistence::OptimisticLockToken]
37
+ def lock_token
38
+ return lock_token_warning unless orm_object.class.column_names.include?("lock_version")
39
+ @lock_token ||=
40
+ Valkyrie::Persistence::OptimisticLockToken.new(
41
+ adapter_id: resource_factory.adapter_id,
42
+ token: orm_object.lock_version
43
+ )
44
+ end
45
45
 
46
- # Issue a migration warning for previous releases of Valkyrie which did not support optimistic locking
47
- def lock_token_warning
48
- return nil unless resource_klass.optimistic_locking_enabled?
49
- warn "[MIGRATION REQUIRED] You have loaded a resource from the Postgres adapter with " \
50
- "optimistic locking enabled, but the necessary migrations have not been run. \n" \
51
- "Please run `bin/rails valkyrie_engine:install:migrations && bin/rails db:migrate` " \
52
- "to enable this feature for Postgres."
53
- nil
54
- end
46
+ # Issue a migration warning for previous releases of Valkyrie which did not support optimistic locking
47
+ def lock_token_warning
48
+ return nil unless resource_klass.optimistic_locking_enabled?
49
+ warn "[MIGRATION REQUIRED] You have loaded a resource from the Postgres adapter with " \
50
+ "optimistic locking enabled, but the necessary migrations have not been run. \n" \
51
+ "Please run `bin/rails valkyrie_engine:install:migrations && bin/rails db:migrate` " \
52
+ "to enable this feature for Postgres."
53
+ nil
54
+ end
55
55
 
56
- # Retrieve the Class used to construct the Valkyrie Resource
57
- # @return [Class]
58
- def resource_klass
59
- Valkyrie.config.resource_class_resolver.call(internal_resource)
60
- end
56
+ # Retrieve the Class used to construct the Valkyrie Resource
57
+ # @return [Class]
58
+ def resource_klass
59
+ Valkyrie.config.resource_class_resolver.call(internal_resource)
60
+ end
61
61
 
62
- # Access the String for the Valkyrie Resource type within the attributes
63
- # @return [String]
64
- def internal_resource
65
- attributes[:internal_resource]
66
- end
62
+ # Access the String for the Valkyrie Resource type within the attributes
63
+ # @return [String]
64
+ def internal_resource
65
+ attributes[:internal_resource]
66
+ end
67
67
 
68
- # @return [Hash] Valkyrie-style hash of attributes.
69
- def attributes
70
- @attributes ||= orm_object.attributes.merge(rdf_metadata).symbolize_keys
71
- end
68
+ # @return [Hash] Valkyrie-style hash of attributes.
69
+ def attributes
70
+ @attributes ||= orm_object.attributes.merge(rdf_metadata).symbolize_keys
71
+ end
72
72
 
73
- # Generate a Hash derived from Valkyrie Resource metadata encoded in the RDF
74
- # @return [Hash]
75
- def rdf_metadata
76
- @rdf_metadata ||= RDFMetadata.new(orm_object.metadata).result
77
- end
73
+ # Generate a Hash derived from Valkyrie Resource metadata encoded in the RDF
74
+ # @return [Hash]
75
+ def rdf_metadata
76
+ @rdf_metadata ||= RDFMetadata.new(orm_object.metadata).result
77
+ end
78
78
 
79
- # Responsible for converting `metadata` JSON-B field in
80
- # {Valkyrie::Persistence::Postgres::ORM::Resource} into an acceptable hash
81
- # for {Valkyrie::Resource}
82
- class RDFMetadata < ::Valkyrie::Persistence::Shared::JSONValueMapper
83
- end
79
+ # Responsible for converting `metadata` JSON-B field in
80
+ # {Valkyrie::Persistence::Postgres::ORM::Resource} into an acceptable hash
81
+ # for {Valkyrie::Resource}
82
+ class RDFMetadata < ::Valkyrie::Persistence::Shared::JSONValueMapper
83
+ end
84
84
  end
85
85
  end
@@ -35,6 +35,13 @@ module Valkyrie::Persistence::Postgres
35
35
  end
36
36
  end
37
37
 
38
+ # Count all records for a specific resource type
39
+ # @param [Class] model
40
+ # @return integer
41
+ def count_all_of_model(model:)
42
+ orm_class.where(internal_resource: model.to_s).count
43
+ end
44
+
38
45
  # Find a record using a Valkyrie ID, and map it to a Valkyrie Resource
39
46
  # @param [Valkyrie::ID, String] id
40
47
  # @return [Valkyrie::Resource]
@@ -94,30 +101,28 @@ module Valkyrie::Persistence::Postgres
94
101
  find_inverse_references_by(resource: resource, property: :member_ids)
95
102
  end
96
103
 
97
- # Find all resources related to a given Valkyrie Resource by a property
98
- # @param [Valkyrie::Resource] resource
99
- # @param [String] property
100
- # @return [Array<Valkyrie::Resource>]
101
- def find_references_by(resource:, property:)
104
+ # (see Valkyrie::Persistence::Memory::QueryService#find_references_by)
105
+ def find_references_by(resource:, property:, model: nil)
102
106
  return [] if resource.id.blank? || resource[property].blank?
103
107
  # only return ordered if needed to avoid performance penalties
104
108
  if ordered_property?(resource: resource, property: property)
105
- run_query(find_ordered_references_query, property, resource.id.to_s)
109
+ find_ordered_references_by(resource: resource, property: property, model: model)
106
110
  else
107
- run_query(find_references_query, property, resource.id.to_s)
111
+ find_unordered_references_by(resource: resource, property: property, model: model)
108
112
  end
109
113
  end
110
114
 
111
- # Find all resources referencing a given Valkyrie Resource by a property
112
- # @param [Valkyrie::Resource] resource
113
- # @param [String] property
114
- # @return [Array<Valkyrie::Resource>]
115
- def find_inverse_references_by(resource: nil, id: nil, property:)
115
+ # (see Valkyrie::Persistence::Memory::QueryService#find_inverse_references_by)
116
+ def find_inverse_references_by(resource: nil, id: nil, property:, model: nil)
116
117
  raise ArgumentError, "Provide resource or id" unless resource || id
117
118
  ensure_persisted(resource) if resource
118
119
  id ||= resource.id
119
120
  internal_array = "{\"#{property}\": [{\"id\": \"#{id}\"}]}"
120
- run_query(find_inverse_references_query, internal_array)
121
+ if model
122
+ run_query(find_inverse_references_with_type_query, internal_array, model)
123
+ else
124
+ run_query(find_inverse_references_query, internal_array)
125
+ end
121
126
  end
122
127
 
123
128
  # Execute a query in SQL for resource records and map them to Valkyrie
@@ -180,6 +185,21 @@ module Valkyrie::Persistence::Postgres
180
185
  SQL
181
186
  end
182
187
 
188
+ # Generate the SQL query for retrieving member resources in PostgreSQL using a
189
+ # JSON object literal (e. g. { "alternate_ids": [{"id": "d6e88f80-41b3-4dbf-a2a0-cd79e20f6d10"}] }).
190
+ # and resource type as arguments
191
+ # @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
192
+ # This uses JSON functions in order to retrieve JSON property values
193
+ # @see https://www.postgresql.org/docs/current/static/functions-json.html
194
+ # @return [String]
195
+ def find_inverse_references_with_type_query
196
+ <<-SQL
197
+ SELECT * FROM orm_resources WHERE
198
+ metadata @> ?
199
+ AND internal_resource = ?
200
+ SQL
201
+ end
202
+
183
203
  # Generate the SQL query for retrieving member resources in PostgreSQL using a
184
204
  # JSON object literal and resource ID as arguments.
185
205
  # @see https://guides.rubyonrails.org/active_record_querying.html#array-conditions
@@ -197,6 +217,14 @@ module Valkyrie::Persistence::Postgres
197
217
  SQL
198
218
  end
199
219
 
220
+ def find_references_with_type_query
221
+ <<-SQL
222
+ SELECT DISTINCT member.* FROM orm_resources a,
223
+ jsonb_array_elements(a.metadata->?) AS b(member)
224
+ JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ? AND member.internal_resource = ?
225
+ SQL
226
+ end
227
+
200
228
  def find_ordered_references_query
201
229
  <<-SQL
202
230
  SELECT member.* FROM orm_resources a,
@@ -206,6 +234,15 @@ module Valkyrie::Persistence::Postgres
206
234
  SQL
207
235
  end
208
236
 
237
+ def find_ordered_references_with_type_query
238
+ <<-SQL
239
+ SELECT member.* FROM orm_resources a,
240
+ jsonb_array_elements(a.metadata->?) WITH ORDINALITY AS b(member, member_pos)
241
+ JOIN orm_resources member ON (b.member->>'id')::#{id_type} = member.id WHERE a.id = ? AND member.internal_resource = ?
242
+ ORDER BY b.member_pos
243
+ SQL
244
+ end
245
+
209
246
  # Constructs a Valkyrie::Persistence::CustomQueryContainer using this query service
210
247
  # @return [Valkyrie::Persistence::CustomQueryContainer]
211
248
  def custom_queries
@@ -214,30 +251,46 @@ module Valkyrie::Persistence::Postgres
214
251
 
215
252
  private
216
253
 
217
- # Determines whether or not an Object is a Valkyrie ID
218
- # @param [Object] id
219
- # @raise [ArgumentError]
220
- def validate_id(id)
221
- raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
254
+ def find_ordered_references_by(resource:, property:, model: nil)
255
+ if model
256
+ run_query(find_ordered_references_with_type_query, property, resource.id.to_s, model)
257
+ else
258
+ run_query(find_ordered_references_query, property, resource.id.to_s)
222
259
  end
260
+ end
223
261
 
224
- # Determines whether or not a resource has been persisted
225
- # @param [Object] resource
226
- # @raise [ArgumentError]
227
- def ensure_persisted(resource)
228
- raise ArgumentError, 'resource is not saved' unless resource.persisted?
262
+ def find_unordered_references_by(resource:, property:, model: nil)
263
+ if model
264
+ run_query(find_references_with_type_query, property, resource.id.to_s, model)
265
+ else
266
+ run_query(find_references_query, property, resource.id.to_s)
229
267
  end
268
+ end
230
269
 
231
- # Accesses the data type in PostgreSQL used for the primary key
232
- # (For example, a UUID)
233
- # @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaCache.html#method-i-columns_hash
234
- # @return [Symbol]
235
- def id_type
236
- @id_type ||= orm_class.columns_hash["id"].type
237
- end
270
+ # Determines whether or not an Object is a Valkyrie ID
271
+ # @param [Object] id
272
+ # @raise [ArgumentError]
273
+ def validate_id(id)
274
+ raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
275
+ end
238
276
 
239
- def ordered_property?(resource:, property:)
240
- resource.ordered_attribute?(property)
241
- end
277
+ # Determines whether or not a resource has been persisted
278
+ # @param [Object] resource
279
+ # @raise [ArgumentError]
280
+ def ensure_persisted(resource)
281
+ raise ArgumentError, 'resource is not saved' unless resource.persisted?
282
+ end
283
+
284
+ # Accesses the data type in PostgreSQL used for the primary key
285
+ # (For example, a UUID)
286
+ # @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaCache.html#method-i-columns_hash
287
+ # @return [Symbol]
288
+ def id_type
289
+ @id_type ||= orm_class.columns_hash["id"].type
290
+ end
291
+
292
+ def ordered_property?(resource:, property:)
293
+ resource.ordered_attribute?(property)
294
+ end
242
295
  end
243
296
  end
@@ -20,7 +20,7 @@ module Valkyrie::Persistence::Postgres
20
20
  orm_object.internal_resource = resource.internal_resource
21
21
  process_lock_token(orm_object)
22
22
  orm_object.disable_optimistic_locking! unless resource.optimistic_locking_enabled?
23
- orm_object.metadata.merge!(attributes)
23
+ orm_object.metadata = attributes
24
24
  end
25
25
  end
26
26
 
@@ -137,20 +137,22 @@ module Valkyrie::Persistence::Shared
137
137
  # e. g. 1970-01-01
138
138
  # @param [Object] value
139
139
  # @return [Boolean]
140
+ # rubocop:disable Metrics/CyclomaticComplexity
140
141
  def self.handles?(value)
141
142
  return false unless value.is_a?(String)
142
- return false unless value[4] == "-"
143
+ return false unless value[4] == "-" && value[10] == "T"
143
144
  year = value.to_s[0..3]
144
145
  return false unless year.length == 4 && year.to_i.to_s == year
145
146
  DateTime.iso8601(value)
146
147
  rescue
147
148
  false
148
149
  end
150
+ # rubocop:enable Metrics/CyclomaticComplexity
149
151
 
150
152
  # Generates a Time object in the UTC from the datestamp string value
151
153
  # @return [Time]
152
154
  def result
153
- DateTime.iso8601(value).utc
155
+ DateTime.iso8601(value).new_offset(0)
154
156
  end
155
157
  end
156
158
  end
@@ -65,412 +65,412 @@ module Valkyrie::Persistence::Solr
65
65
 
66
66
  private
67
67
 
68
- # Maps Solr Document fields to attributes with single values
69
- # Filters for fields which store the Valkyrie resource type
70
- # @param [Hash] attribute_hash
71
- # @return [Hash]
72
- def add_single_values(attribute_hash)
73
- attribute_hash.select do |k, v|
74
- field = k.to_s.split("_").last
75
- property = k.to_s.gsub("_#{field}", "")
76
- next true if multivalued?(field)
77
- next false if property == "internal_resource"
78
- next false if v.length > 1
79
- true
80
- end
68
+ # Maps Solr Document fields to attributes with single values
69
+ # Filters for fields which store the Valkyrie resource type
70
+ # @param [Hash] attribute_hash
71
+ # @return [Hash]
72
+ def add_single_values(attribute_hash)
73
+ attribute_hash.select do |k, v|
74
+ field = k.to_s.split("_").last
75
+ property = k.to_s.gsub("_#{field}", "")
76
+ next true if multivalued?(field)
77
+ next false if property == "internal_resource"
78
+ next false if v.length > 1
79
+ true
81
80
  end
81
+ end
82
82
 
83
- # Determines whether or not a field is multivalued
84
- # @note this is tied to conventions in the Solr Schema
85
- # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
86
- # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
87
- # @param [String] field
88
- # @return [Boolean]
89
- def multivalued?(field)
90
- field.end_with?('m', 'mv')
91
- end
83
+ # Determines whether or not a field is multivalued
84
+ # @note this is tied to conventions in the Solr Schema
85
+ # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
86
+ # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
87
+ # @param [String] field
88
+ # @return [Boolean]
89
+ def multivalued?(field)
90
+ field.end_with?('m', 'mv')
91
+ end
92
92
 
93
- # If optimistic locking is enabled for this Valkyrie Resource, generates a Hash containing the locking token
94
- # @return [Hash]
95
- def lock_hash
96
- return {} unless resource.optimistic_locking_enabled? && lock_token.present?
97
- { _version_: lock_token }
98
- end
93
+ # If optimistic locking is enabled for this Valkyrie Resource, generates a Hash containing the locking token
94
+ # @return [Hash]
95
+ def lock_hash
96
+ return {} unless resource.optimistic_locking_enabled? && lock_token.present?
97
+ { _version_: lock_token }
98
+ end
99
99
 
100
- # Retrieves the lock token from the resource attributes
101
- # @return [String]
102
- def lock_token
103
- @lock_token ||= begin
104
- found_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
105
- .find { |token| token.adapter_id == resource_factory.adapter_id }
106
- return if found_token.nil?
107
- found_token.token
108
- end
100
+ # Retrieves the lock token from the resource attributes
101
+ # @return [String]
102
+ def lock_token
103
+ @lock_token ||= begin
104
+ found_token = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
105
+ .find { |token| token.adapter_id == resource_factory.adapter_id }
106
+ return if found_token.nil?
107
+ found_token.token
109
108
  end
109
+ end
110
110
 
111
- # Generates the Valkyrie Resource attribute Hash
112
- # @return [Hash]
113
- def attribute_hash
114
- properties.each_with_object({}) do |property, hsh|
115
- next if property == Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK
116
- attr = resource_attributes[property]
117
- mapper_val = SolrMapperValue.for(Property.new(property, attr)).result
118
- unless mapper_val.respond_to?(:apply_to)
119
- raise "Unable to cast #{resource_attributes[:internal_resource]}#" \
120
- "#{property} which has been set to an instance of '#{attr.class}'"
121
- end
122
- mapper_val.apply_to(hsh)
123
- end
111
+ # Generates the Valkyrie Resource attribute Hash
112
+ # @return [Hash]
113
+ def attribute_hash
114
+ properties.each_with_object({}) do |property, hsh|
115
+ next if property == Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK
116
+ attr = resource_attributes[property]
117
+ mapper_val = SolrMapperValue.for(Property.new(property, attr)).result
118
+ unless mapper_val.respond_to?(:apply_to)
119
+ raise "Unable to cast #{resource_attributes[:internal_resource]}#" \
120
+ "#{property} which has been set to an instance of '#{attr.class}'"
121
+ end
122
+ mapper_val.apply_to(hsh)
124
123
  end
124
+ end
125
125
 
126
- # Accesses the keys for the attributes on the Valkyrie Resource
127
- # @return [Array<Symbol>]
128
- def properties
129
- resource_attributes.keys - [:id, :created_at, :updated_at, :new_record]
130
- end
131
-
132
- # Access the attributes for the Valkyrie resources
133
- # @return [Hash]
134
- def resource_attributes
135
- @resource_attributes ||= resource.attributes
136
- end
137
-
138
- ##
139
- # A container resource for holding a `key`, `value, and `scope` of a value
140
- # in a resource together for casting.
141
- class Property
142
- attr_reader :key, :value, :scope
143
- # @param key [Symbol] Property identifier.
144
- # @param value [Object] Value or list of values which are underneath the
145
- # key.
146
- # @param scope [Object] The resource or point where the key and values
147
- # came from.
148
- def initialize(key, value, scope = [])
149
- @key = key
150
- @value = value
151
- @scope = scope
152
- end
126
+ # Accesses the keys for the attributes on the Valkyrie Resource
127
+ # @return [Array<Symbol>]
128
+ def properties
129
+ resource_attributes.keys - [:id, :created_at, :updated_at, :new_record]
130
+ end
131
+
132
+ # Access the attributes for the Valkyrie resources
133
+ # @return [Hash]
134
+ def resource_attributes
135
+ @resource_attributes ||= resource.attributes
136
+ end
137
+
138
+ ##
139
+ # A container resource for holding a `key`, `value, and `scope` of a value
140
+ # in a resource together for casting.
141
+ class Property
142
+ attr_reader :key, :value, :scope
143
+ # @param key [Symbol] Property identifier.
144
+ # @param value [Object] Value or list of values which are underneath the
145
+ # key.
146
+ # @param scope [Object] The resource or point where the key and values
147
+ # came from.
148
+ def initialize(key, value, scope = [])
149
+ @key = key
150
+ @value = value
151
+ @scope = scope
153
152
  end
153
+ end
154
154
 
155
- ##
156
- # Represents a key/value combination in the solr document, used for isolating logic around
157
- # how to apply a value to a hash.
158
- class SolrRow
159
- attr_reader :key, :fields, :values
160
- # @param key [Symbol] Solr key.
161
- # @param fields [Array<Symbol>] Field suffixes to index into.
162
- # @param values [Array] Values to index into the given fields.
163
- def initialize(key:, fields:, values:)
164
- @key = key
165
- @fields = Array.wrap(fields)
166
- @values = Array.wrap(values)
167
- end
155
+ ##
156
+ # Represents a key/value combination in the solr document, used for isolating logic around
157
+ # how to apply a value to a hash.
158
+ class SolrRow
159
+ attr_reader :key, :fields, :values
160
+ # @param key [Symbol] Solr key.
161
+ # @param fields [Array<Symbol>] Field suffixes to index into.
162
+ # @param values [Array] Values to index into the given fields.
163
+ def initialize(key:, fields:, values:)
164
+ @key = key
165
+ @fields = Array.wrap(fields)
166
+ @values = Array.wrap(values)
167
+ end
168
168
 
169
- # @param hsh [Hash] The solr hash to apply to.
170
- # @return [Hash] The updated solr hash.
171
- def apply_to(hsh)
172
- return hsh if values.blank?
173
- fields.each do |field|
174
- hsh["#{key}_#{field}".to_sym] ||= []
175
- hsh["#{key}_#{field}".to_sym] += values
176
- end
177
- hsh
169
+ # @param hsh [Hash] The solr hash to apply to.
170
+ # @return [Hash] The updated solr hash.
171
+ def apply_to(hsh)
172
+ return hsh if values.blank?
173
+ fields.each do |field|
174
+ hsh["#{key}_#{field}".to_sym] ||= []
175
+ hsh["#{key}_#{field}".to_sym] += values
178
176
  end
177
+ hsh
179
178
  end
179
+ end
180
180
 
181
- ##
182
- # Wraps up multiple SolrRows to apply them all at once, while looking like
183
- # just one.
184
- class CompositeSolrRow
185
- attr_reader :solr_rows
181
+ ##
182
+ # Wraps up multiple SolrRows to apply them all at once, while looking like
183
+ # just one.
184
+ class CompositeSolrRow
185
+ attr_reader :solr_rows
186
186
 
187
- # @param [Array<Valkyrie::Persistence::Solr::Mapper::SolrRow>] solr_rows
188
- def initialize(solr_rows)
189
- @solr_rows = solr_rows
190
- end
187
+ # @param [Array<Valkyrie::Persistence::Solr::Mapper::SolrRow>] solr_rows
188
+ def initialize(solr_rows)
189
+ @solr_rows = solr_rows
190
+ end
191
191
 
192
- # Merge a Hash of attribute values into a logical row of Solr fields
193
- # @param [Hash] hsh
194
- # @see Valkyrie::Persistence::Solr::Mapper::SolrRow#apply_to
195
- def apply_to(hsh)
196
- solr_rows.each do |solr_row|
197
- solr_row.apply_to(hsh)
198
- end
199
- hsh
192
+ # Merge a Hash of attribute values into a logical row of Solr fields
193
+ # @param [Hash] hsh
194
+ # @see Valkyrie::Persistence::Solr::Mapper::SolrRow#apply_to
195
+ def apply_to(hsh)
196
+ solr_rows.each do |solr_row|
197
+ solr_row.apply_to(hsh)
200
198
  end
199
+ hsh
201
200
  end
201
+ end
202
202
 
203
- # Container for casting mappers.
204
- class SolrMapperValue < ::Valkyrie::ValueMapper
205
- end
203
+ # Container for casting mappers.
204
+ class SolrMapperValue < ::Valkyrie::ValueMapper
205
+ end
206
206
 
207
- # Casts {Boolean} values into a recognizable string in Solr.
208
- class BooleanPropertyValue < ::Valkyrie::ValueMapper
209
- SolrMapperValue.register(self)
207
+ # Casts {Boolean} values into a recognizable string in Solr.
208
+ class BooleanPropertyValue < ::Valkyrie::ValueMapper
209
+ SolrMapperValue.register(self)
210
210
 
211
- # Determines whether or not a Property value behaves like a boolean value
212
- # @param [Object] value
213
- # @return [Boolean]
214
- def self.handles?(value)
215
- value.is_a?(Property) && ([true, false].include? value.value)
216
- end
211
+ # Determines whether or not a Property value behaves like a boolean value
212
+ # @param [Object] value
213
+ # @return [Boolean]
214
+ def self.handles?(value)
215
+ value.is_a?(Property) && ([true, false].include? value.value)
216
+ end
217
217
 
218
- # Constructs a SolrRow object for a Property with a Boolean value
219
- # @note this prepends the string "boolean-" to the value indexed in Solr
220
- # @return [SolrRow]
221
- def result
222
- calling_mapper.for(Property.new(value.key, "boolean-#{value.value}")).result
223
- end
218
+ # Constructs a SolrRow object for a Property with a Boolean value
219
+ # @note this prepends the string "boolean-" to the value indexed in Solr
220
+ # @return [SolrRow]
221
+ def result
222
+ calling_mapper.for(Property.new(value.key, "boolean-#{value.value}")).result
224
223
  end
224
+ end
225
225
 
226
- # Casts nested resources into a JSON string in solr.
227
- class NestedObjectValue < ::Valkyrie::ValueMapper
228
- SolrMapperValue.register(self)
226
+ # Casts nested resources into a JSON string in solr.
227
+ class NestedObjectValue < ::Valkyrie::ValueMapper
228
+ SolrMapperValue.register(self)
229
229
 
230
- # Determines whether or not a Property value is a Hash
231
- # @param [Object] value
232
- # @return [Boolean]
233
- def self.handles?(value)
234
- value.value.is_a?(Hash) || value.value.is_a?(Valkyrie::Resource)
235
- end
230
+ # Determines whether or not a Property value is a Hash
231
+ # @param [Object] value
232
+ # @return [Boolean]
233
+ def self.handles?(value)
234
+ value.value.is_a?(Hash) || value.value.is_a?(Valkyrie::Resource)
235
+ end
236
236
 
237
- # Constructs a SolrRow object for a Property with a Hash value
238
- # @note this prepends the string "serialized-" to the value indexed in Solr
239
- # This is indexed as a stored multivalued text
240
- # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
241
- # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
242
- # @return [SolrRow]
243
- def result
244
- SolrRow.new(key: value.key, fields: ["tsim"], values: "serialized-#{value.value.to_json}")
245
- end
237
+ # Constructs a SolrRow object for a Property with a Hash value
238
+ # @note this prepends the string "serialized-" to the value indexed in Solr
239
+ # This is indexed as a stored multivalued text
240
+ # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
241
+ # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
242
+ # @return [SolrRow]
243
+ def result
244
+ SolrRow.new(key: value.key, fields: ["tsim"], values: "serialized-#{value.value.to_json}")
246
245
  end
246
+ end
247
247
 
248
- # Casts enumerable values one by one.
249
- class EnumerableValue < ::Valkyrie::ValueMapper
250
- SolrMapperValue.register(self)
248
+ # Casts enumerable values one by one.
249
+ class EnumerableValue < ::Valkyrie::ValueMapper
250
+ SolrMapperValue.register(self)
251
251
 
252
- # Determines whether or not a Property value is an Array
253
- # @param [Object] value
254
- # @return [Boolean]
255
- def self.handles?(value)
256
- value.is_a?(Property) && value.value.is_a?(Array)
257
- end
252
+ # Determines whether or not a Property value is an Array
253
+ # @param [Object] value
254
+ # @return [Boolean]
255
+ def self.handles?(value)
256
+ value.is_a?(Property) && value.value.is_a?(Array)
257
+ end
258
258
 
259
- # Constructs a CompositeSolrRow object for a set of Property values
260
- # @return [CompositeSolrRow]
261
- def result
262
- CompositeSolrRow.new(
263
- value.value.map do |val|
264
- calling_mapper.for(Property.new(value.key, val, value.value)).result
265
- end
266
- )
267
- end
259
+ # Constructs a CompositeSolrRow object for a set of Property values
260
+ # @return [CompositeSolrRow]
261
+ def result
262
+ CompositeSolrRow.new(
263
+ value.value.map do |val|
264
+ calling_mapper.for(Property.new(value.key, val, value.value)).result
265
+ end
266
+ )
268
267
  end
268
+ end
269
269
 
270
- # Skips nil values.
271
- class NilPropertyValue < ::Valkyrie::ValueMapper
272
- SolrMapperValue.register(self)
270
+ # Skips nil values.
271
+ class NilPropertyValue < ::Valkyrie::ValueMapper
272
+ SolrMapperValue.register(self)
273
273
 
274
- # Determines whether or not a Property value responds as nil
275
- # @param [Object] value
276
- # @return [Boolean]
277
- def self.handles?(value)
278
- value.is_a?(Property) && value.value.nil?
279
- end
274
+ # Determines whether or not a Property value responds as nil
275
+ # @param [Object] value
276
+ # @return [Boolean]
277
+ def self.handles?(value)
278
+ value.is_a?(Property) && value.value.nil?
279
+ end
280
280
 
281
- # Constructs a SolrRow object for a Property with a nil value
282
- # @return [SolrRow]
283
- def result
284
- SolrRow.new(key: value.key, fields: [], values: nil)
285
- end
281
+ # Constructs a SolrRow object for a Property with a nil value
282
+ # @return [SolrRow]
283
+ def result
284
+ SolrRow.new(key: value.key, fields: [], values: nil)
286
285
  end
286
+ end
287
287
 
288
- # Casts {Valkyrie::ID} values into a recognizable string in solr.
289
- class IDPropertyValue < ::Valkyrie::ValueMapper
290
- SolrMapperValue.register(self)
288
+ # Casts {Valkyrie::ID} values into a recognizable string in solr.
289
+ class IDPropertyValue < ::Valkyrie::ValueMapper
290
+ SolrMapperValue.register(self)
291
291
 
292
- # Determines whether or not a Property value is a Valkyrie ID
293
- # @param [Object] value
294
- # @return [Boolean]
295
- def self.handles?(value)
296
- value.is_a?(Property) && value.value.is_a?(::Valkyrie::ID)
297
- end
292
+ # Determines whether or not a Property value is a Valkyrie ID
293
+ # @param [Object] value
294
+ # @return [Boolean]
295
+ def self.handles?(value)
296
+ value.is_a?(Property) && value.value.is_a?(::Valkyrie::ID)
297
+ end
298
298
 
299
- # Constructs a SolrRow object for the Property Valkyrie ID value
300
- # @note this prepends the string "id-" to the value indexed in Solr
301
- # @return [SolrRow]
302
- def result
303
- calling_mapper.for(Property.new(value.key, "id-#{value.value.id}")).result
304
- end
299
+ # Constructs a SolrRow object for the Property Valkyrie ID value
300
+ # @note this prepends the string "id-" to the value indexed in Solr
301
+ # @return [SolrRow]
302
+ def result
303
+ calling_mapper.for(Property.new(value.key, "id-#{value.value.id}")).result
305
304
  end
305
+ end
306
306
 
307
- # Casts {RDF::URI} values into a recognizable string in solr.
308
- class URIPropertyValue < ::Valkyrie::ValueMapper
309
- SolrMapperValue.register(self)
307
+ # Casts {RDF::URI} values into a recognizable string in solr.
308
+ class URIPropertyValue < ::Valkyrie::ValueMapper
309
+ SolrMapperValue.register(self)
310
310
 
311
- # Determines whether or not a Property value is a URI
312
- # @param [Object] value
313
- # @return [Boolean]
314
- def self.handles?(value)
315
- value.is_a?(Property) && value.value.is_a?(::RDF::URI)
316
- end
311
+ # Determines whether or not a Property value is a URI
312
+ # @param [Object] value
313
+ # @return [Boolean]
314
+ def self.handles?(value)
315
+ value.is_a?(Property) && value.value.is_a?(::RDF::URI)
316
+ end
317
317
 
318
- # Constructs a SolrRow object for the Property URI value
319
- # @note this prepends the string "uri-" to the value indexed in Solr
320
- # @return [SolrRow]
321
- def result
322
- calling_mapper.for(Property.new(value.key, "uri-#{value.value}")).result
323
- end
318
+ # Constructs a SolrRow object for the Property URI value
319
+ # @note this prepends the string "uri-" to the value indexed in Solr
320
+ # @return [SolrRow]
321
+ def result
322
+ calling_mapper.for(Property.new(value.key, "uri-#{value.value}")).result
324
323
  end
324
+ end
325
325
 
326
- # Casts {Integer} values into a recognizable string in Solr.
327
- class IntegerPropertyValue < ::Valkyrie::ValueMapper
328
- SolrMapperValue.register(self)
326
+ # Casts {Integer} values into a recognizable string in Solr.
327
+ class IntegerPropertyValue < ::Valkyrie::ValueMapper
328
+ SolrMapperValue.register(self)
329
329
 
330
- # Determines whether or not a Property value is an Integer
331
- # @param [Object] value
332
- # @return [Boolean]
333
- def self.handles?(value)
334
- value.is_a?(Property) && value.value.is_a?(Integer)
335
- end
330
+ # Determines whether or not a Property value is an Integer
331
+ # @param [Object] value
332
+ # @return [Boolean]
333
+ def self.handles?(value)
334
+ value.is_a?(Property) && value.value.is_a?(Integer)
335
+ end
336
336
 
337
- # Constructs a SolrRow object for the Property Integer value
338
- # @note this prepends the string "integer-" to the value indexed in Solr
339
- # @return [SolrRow]
340
- def result
341
- calling_mapper.for(Property.new(value.key, "integer-#{value.value}")).result
342
- end
337
+ # Constructs a SolrRow object for the Property Integer value
338
+ # @note this prepends the string "integer-" to the value indexed in Solr
339
+ # @return [SolrRow]
340
+ def result
341
+ calling_mapper.for(Property.new(value.key, "integer-#{value.value}")).result
343
342
  end
343
+ end
344
344
 
345
- # Casts {Float} values into a recognizable string in Solr.
346
- class FloatPropertyValue < ::Valkyrie::ValueMapper
347
- SolrMapperValue.register(self)
348
- def self.handles?(value)
349
- value.is_a?(Property) && value.value.is_a?(Float)
350
- end
345
+ # Casts {Float} values into a recognizable string in Solr.
346
+ class FloatPropertyValue < ::Valkyrie::ValueMapper
347
+ SolrMapperValue.register(self)
348
+ def self.handles?(value)
349
+ value.is_a?(Property) && value.value.is_a?(Float)
350
+ end
351
351
 
352
- def result
353
- calling_mapper.for(Property.new(value.key, "float-#{value.value}")).result
354
- end
352
+ def result
353
+ calling_mapper.for(Property.new(value.key, "float-#{value.value}")).result
355
354
  end
355
+ end
356
356
 
357
- # Casts {DateTime} values into a recognizable string in Solr.
358
- class DateTimePropertyValue < ::Valkyrie::ValueMapper
359
- SolrMapperValue.register(self)
357
+ # Casts {DateTime} values into a recognizable string in Solr.
358
+ class DateTimePropertyValue < ::Valkyrie::ValueMapper
359
+ SolrMapperValue.register(self)
360
360
 
361
- # Determines whether or not a Property value is a DateTime or Time
362
- # @param [Object] value
363
- # @return [Boolean]
364
- def self.handles?(value)
365
- value.is_a?(Property) && (value.value.is_a?(Time) || value.value.is_a?(DateTime))
366
- end
361
+ # Determines whether or not a Property value is a DateTime or Time
362
+ # @param [Object] value
363
+ # @return [Boolean]
364
+ def self.handles?(value)
365
+ value.is_a?(Property) && (value.value.is_a?(Time) || value.value.is_a?(DateTime))
366
+ end
367
367
 
368
- # Constructs a SolrRow object for a datestamp derived from the value
369
- # @note this prepends the string "datetime-" to the value indexed in Solr
370
- # @return [SolrRow]
371
- def result
372
- calling_mapper.for(Property.new(value.key, "datetime-#{JSON.parse(to_datetime(value.value).to_json)}")).result
373
- end
368
+ # Constructs a SolrRow object for a datestamp derived from the value
369
+ # @note this prepends the string "datetime-" to the value indexed in Solr
370
+ # @return [SolrRow]
371
+ def result
372
+ calling_mapper.for(Property.new(value.key, "datetime-#{JSON.parse(to_datetime(value.value).to_json)}")).result
373
+ end
374
374
 
375
- private
375
+ private
376
376
 
377
- # Converts a value to a UTC timestamp if it is a DateTime or behaves like a Time value
378
- # @param [Object] value
379
- # @return [Time]
380
- def to_datetime(value)
381
- return value.utc if value.is_a?(DateTime)
382
- return value.to_datetime.utc if value.respond_to?(:to_datetime)
383
- end
377
+ # Converts a value to a UTC timestamp if it is a DateTime or behaves like a Time value
378
+ # @param [Object] value
379
+ # @return [Time]
380
+ def to_datetime(value)
381
+ return value.new_offset(0) if value.is_a?(DateTime)
382
+ return value.to_datetime.new_offset(0) if value.respond_to?(:to_datetime)
384
383
  end
384
+ end
385
385
 
386
- # Handles casting language-tagged strings when there are both
387
- # language-tagged and non-language-tagged strings in Solr. Assumes English
388
- # for non-language-tagged strings.
389
- class SharedStringPropertyValue < ::Valkyrie::ValueMapper
390
- SolrMapperValue.register(self)
386
+ # Handles casting language-tagged strings when there are both
387
+ # language-tagged and non-language-tagged strings in Solr. Assumes English
388
+ # for non-language-tagged strings.
389
+ class SharedStringPropertyValue < ::Valkyrie::ValueMapper
390
+ SolrMapperValue.register(self)
391
391
 
392
- # Determines whether or not a Property value is a String whether or not the Property has an RDF literal specifying the language tag
393
- # @param [Object] value
394
- # @return [Boolean]
395
- def self.handles?(value)
396
- value.is_a?(Property) && value.value.is_a?(String) && value.scope.find { |x| x.is_a?(::RDF::Literal) }.present?
397
- end
392
+ # Determines whether or not a Property value is a String whether or not the Property has an RDF literal specifying the language tag
393
+ # @param [Object] value
394
+ # @return [Boolean]
395
+ def self.handles?(value)
396
+ value.is_a?(Property) && value.value.is_a?(String) && value.scope.find { |x| x.is_a?(::RDF::Literal) }.present?
397
+ end
398
398
 
399
- # Constructs a CompositeSolrRow object with the language-tagged literal value
400
- # @return [CompositeSolrRow]
401
- def result
402
- CompositeSolrRow.new(
403
- [
404
- calling_mapper.for(Property.new(value.key, value.value)).result,
405
- calling_mapper.for(Property.new("#{value.key}_lang", "eng")).result,
406
- calling_mapper.for(Property.new("#{value.key}_type", "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")).result
407
- ]
408
- )
409
- end
399
+ # Constructs a CompositeSolrRow object with the language-tagged literal value
400
+ # @return [CompositeSolrRow]
401
+ def result
402
+ CompositeSolrRow.new(
403
+ [
404
+ calling_mapper.for(Property.new(value.key, value.value)).result,
405
+ calling_mapper.for(Property.new("#{value.key}_lang", "eng")).result,
406
+ calling_mapper.for(Property.new("#{value.key}_type", "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")).result
407
+ ]
408
+ )
410
409
  end
410
+ end
411
411
 
412
- # Handles casting strings.
413
- class StringPropertyValue < ::Valkyrie::ValueMapper
414
- SolrMapperValue.register(self)
412
+ # Handles casting strings.
413
+ class StringPropertyValue < ::Valkyrie::ValueMapper
414
+ SolrMapperValue.register(self)
415
415
 
416
- # Determines whether or not a Property value is a String
417
- # @param [Object] value
418
- # @return [Boolean]
419
- def self.handles?(value)
420
- value.is_a?(Property) && value.value.is_a?(String)
421
- end
416
+ # Determines whether or not a Property value is a String
417
+ # @param [Object] value
418
+ # @return [Boolean]
419
+ def self.handles?(value)
420
+ value.is_a?(Property) && value.value.is_a?(String)
421
+ end
422
422
 
423
- # Constructs a SolrRow object with the String values and Solr field settings
424
- # @return [SolrRow]
425
- def result
426
- SolrRow.new(key: value.key, fields: fields, values: value.value)
427
- end
423
+ # Constructs a SolrRow object with the String values and Solr field settings
424
+ # @return [SolrRow]
425
+ def result
426
+ SolrRow.new(key: value.key, fields: fields, values: value.value)
427
+ end
428
428
 
429
- # Generates the Solr fields used during the indexing
430
- # String are normally indexed using the following:
431
- # - stored text
432
- # - stored english text
433
- # - stored single string
434
- # - multivalued string
435
- # - stored multivalued text
436
- # - stored multivalued english text
437
- # If the string is greater than 1000 characters in length, it is only indexed as a stored multivalued text
438
- # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
439
- # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
440
- # @return [Array<Symbol>]
441
- def fields
442
- if value.value.length > 1000
443
- [:tsim]
444
- else
445
- [:tsim, :ssim, :tesim, :tsi, :ssi, :tesi]
446
- end
429
+ # Generates the Solr fields used during the indexing
430
+ # String are normally indexed using the following:
431
+ # - stored text
432
+ # - stored english text
433
+ # - stored single string
434
+ # - multivalued string
435
+ # - stored multivalued text
436
+ # - stored multivalued english text
437
+ # If the string is greater than 1000 characters in length, it is only indexed as a stored multivalued text
438
+ # @see https://lucene.apache.org/solr/guide/defining-fields.html#defining-fields
439
+ # @see https://github.com/samvera-labs/valkyrie/blob/master/solr/config/schema.xml
440
+ # @return [Array<Symbol>]
441
+ def fields
442
+ if value.value.length > 1000
443
+ [:tsim]
444
+ else
445
+ [:tsim, :ssim, :tesim, :tsi, :ssi, :tesi]
447
446
  end
448
447
  end
448
+ end
449
449
 
450
- # Handles casting language-typed {RDF::Literal}s
451
- class LiteralPropertyValue < ::Valkyrie::ValueMapper
452
- SolrMapperValue.register(self)
450
+ # Handles casting language-typed {RDF::Literal}s
451
+ class LiteralPropertyValue < ::Valkyrie::ValueMapper
452
+ SolrMapperValue.register(self)
453
453
 
454
- # Determines whether or not a Property value is an RDF literal
455
- # @param [Object] value
456
- # @return [Boolean]
457
- def self.handles?(value)
458
- value.is_a?(Property) && value.value.is_a?(::RDF::Literal)
459
- end
454
+ # Determines whether or not a Property value is an RDF literal
455
+ # @param [Object] value
456
+ # @return [Boolean]
457
+ def self.handles?(value)
458
+ value.is_a?(Property) && value.value.is_a?(::RDF::Literal)
459
+ end
460
460
 
461
- # Constructs a CompositeSolrRow object with the language-tagged literal value
462
- # @return [CompositeSolrRow]
463
- def result
464
- key = value.key
465
- val = value.value
466
- CompositeSolrRow.new(
467
- [
468
- calling_mapper.for(Property.new(key, val.to_s)).result,
469
- calling_mapper.for(Property.new("#{key}_lang", val.language.to_s)).result,
470
- calling_mapper.for(Property.new("#{key}_type", val.datatype.to_s)).result
471
- ]
472
- )
473
- end
461
+ # Constructs a CompositeSolrRow object with the language-tagged literal value
462
+ # @return [CompositeSolrRow]
463
+ def result
464
+ key = value.key
465
+ val = value.value
466
+ CompositeSolrRow.new(
467
+ [
468
+ calling_mapper.for(Property.new(key, val.to_s)).result,
469
+ calling_mapper.for(Property.new("#{key}_lang", val.language.to_s)).result,
470
+ calling_mapper.for(Property.new("#{key}_type", val.datatype.to_s)).result
471
+ ]
472
+ )
474
473
  end
474
+ end
475
475
  end
476
476
  end