solis 0.98.0 → 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b5f53e6ee613f19d1de7527908da768b55623f51f45bdc4ae31ab3ba338a5cc
4
- data.tar.gz: d1dfade779a59ad0c36422a20bcc1b8b21fc7c74b75774e1b69be9488050c9ef
3
+ metadata.gz: 848141bb6e9f81fea57188c0a10cc3cf86b656a137e5f5a2a665a835af2ef6f5
4
+ data.tar.gz: 49505d5ce710f03daa6b14f2488df1c3d0ff8e2e24301b17940aa5d60aed67d1
5
5
  SHA512:
6
- metadata.gz: ec93a8f1b15c02a1b2b5b52e4f85d0d3058c2528ad73d33d15944e5a3c5609bbfe9e2e83e8ab15efc41c1e98350745903b089cf3902cd2157a3a2aaaeb0f5657
7
- data.tar.gz: fd75bb60d80282d6c285b0bb004cdb466a0fa3d6ce2de9960732bb201e5d068c6c4256e7cf560dbf1b67b86371706659f562bc72bb149826285f22cefac6ba84
6
+ metadata.gz: 16c1286a2b9046f5c267b2e59d9d2d293fd3919e5792a867d174d6385fbfe522bdc4b306e96af244ff4493f21727ce09f71c6c98015cac3ab2f1206cf1543771
7
+ data.tar.gz: 6e02c4c9c3511271295f010579f76c758c9c590fae999c01ac262615a6fb21384aa2b73bc7e719be61563faec7a080b5f2a95be3a857e156cb2be9400c96c5ff
data/lib/solis/model.rb CHANGED
@@ -145,22 +145,28 @@ values ?s {<#{self.graph_id}>}
145
145
  else
146
146
  data = properties_to_hash(self)
147
147
  attributes = data.include?('attributes') ? data['attributes'] : data
148
+ readonly_list = (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
149
+
148
150
  attributes.each_pair do |key, value|
149
151
  unless self.class.metadata[:attributes][key][:node].nil?
150
152
  value = [value] unless value.is_a?(Array)
151
153
  value.each do |sub_value|
152
154
  embedded = self.class.graph.shape_as_model(self.class.metadata[:attributes][key][:datatype].to_s).new(sub_value)
153
- embedded_readonly_entities = Solis::Options.instance.get[:embedded_readonly].map{|s| s.to_s} || []
154
155
 
155
- if (embedded.class.ancestors.map{|s| s.to_s} & embedded_readonly_entities).empty? || top_level
156
+ if readonly_entity?(embedded, readonly_list)
157
+ # Readonly entities (code tables) should never be modified
158
+ # Only verify they exist, do not create or update them
159
+ unless embedded.exists?(sparql)
160
+ Solis::LOGGER.warn("#{embedded.class.name} (id: #{embedded.id}) is readonly but does not exist in database. Skipping.")
161
+ end
162
+ else
163
+ # Non-readonly entities can be created or updated
156
164
  if embedded.exists?(sparql)
157
165
  embedded_data = properties_to_hash(embedded)
158
166
  embedded.update(embedded_data, validate_dependencies, false)
159
167
  else
160
168
  embedded.save(validate_dependencies, false)
161
169
  end
162
- else
163
- Solis::LOGGER.info("#{embedded.class.name} is embedded not allowed to change. Skipping")
164
170
  end
165
171
  end
166
172
  end
@@ -193,6 +199,9 @@ values ?s {<#{self.graph_id}>}
193
199
  raise Solis::Error::NotFoundError if original_klass.nil?
194
200
  updated_klass = original_klass.deep_dup
195
201
 
202
+ # Cache readonly entities list once
203
+ readonly_list = (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
204
+
196
205
  # Track entities to potentially delete
197
206
  entities_to_check_for_deletion = {}
198
207
 
@@ -216,9 +225,16 @@ values ?s {<#{self.graph_id}>}
216
225
  embedded = self.class.graph.shape_as_model(original_klass.class.metadata[:attributes][key][:datatype].to_s).new(sub_value)
217
226
  new_ids << embedded.id if embedded.id
218
227
 
219
- embedded_readonly_entities = Solis::Options.instance.get[:embedded_readonly].map{|s| s.to_s} || []
220
-
221
- if (embedded.class.ancestors.map{|s| s.to_s} & embedded_readonly_entities).empty? || top_level
228
+ if readonly_entity?(embedded, readonly_list)
229
+ # Readonly entities (code tables) should never be modified
230
+ # Only verify they exist, do not create or update them
231
+ if embedded.exists?(sparql)
232
+ new_embedded_values << embedded
233
+ else
234
+ Solis::LOGGER.warn("#{embedded.class.name} (id: #{embedded.id}) is readonly but does not exist in database. Skipping.")
235
+ end
236
+ else
237
+ # Non-readonly entities can be created or updated
222
238
  if embedded.exists?(sparql)
223
239
  embedded_data = properties_to_hash(embedded)
224
240
  embedded.update(embedded_data, validate_dependencies, false)
@@ -227,13 +243,11 @@ values ?s {<#{self.graph_id}>}
227
243
  embedded_value = embedded.save(validate_dependencies, false)
228
244
  new_embedded_values << embedded_value
229
245
  end
230
- else
231
- Solis::LOGGER.info("#{embedded.class.name} is embedded not allowed to change. Skipping")
232
- new_embedded_values << embedded
233
246
  end
234
247
  end
235
248
 
236
249
  # Identify orphaned entities (in original but not in new)
250
+ # Note: Readonly entities will be filtered out in delete_orphaned_entities
237
251
  orphaned_ids = original_ids - new_ids
238
252
  unless orphaned_ids.empty?
239
253
  orphaned_entities = original_embedded.select { |e| solis_model?(e) && orphaned_ids.include?(e.id) }
@@ -289,11 +303,11 @@ values ?s {<#{self.graph_id}>}
289
303
 
290
304
  data
291
305
  rescue StandardError => e
292
- original_graph = as_graph(original_klass, false)
306
+ original_graph = as_graph(original_klass, false) if defined?(original_klass) && original_klass
293
307
  Solis::LOGGER.error(e.message)
294
- Solis::LOGGER.error original_graph.dump(:ttl)
308
+ Solis::LOGGER.error original_graph.dump(:ttl) if defined?(original_graph) && original_graph
295
309
  Solis::LOGGER.error delete_insert_query if defined?(delete_insert_query)
296
- sparql.insert_data(original_graph, graph: original_graph.name)
310
+ sparql.insert_data(original_graph, graph: original_graph.name) if defined?(original_graph) && original_graph && defined?(sparql) && sparql
297
311
 
298
312
  raise e
299
313
  end
@@ -467,6 +481,12 @@ values ?s {<#{self.graph_id}>}
467
481
  obj.class.ancestors.include?(Solis::Model)
468
482
  end
469
483
 
484
+ # Helper method to check if an entity is readonly (code table)
485
+ def readonly_entity?(entity, readonly_list = nil)
486
+ readonly_list ||= (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
487
+ (entity.class.ancestors.map(&:to_s) & readonly_list).any?
488
+ end
489
+
470
490
  # Helper method to get tableized class name
471
491
  def tableized_class_name(obj)
472
492
  obj.class.name.tableize
@@ -486,14 +506,18 @@ values ?s {<#{self.graph_id}>}
486
506
 
487
507
  # Delete orphaned entities that are no longer referenced
488
508
  def delete_orphaned_entities(entities_to_check, sparql)
489
- embedded_readonly_entities = Solis::Options.instance.get[:embedded_readonly].map{|s| s.to_s} || []
509
+ return if entities_to_check.nil? || entities_to_check.empty?
510
+
511
+ readonly_list = (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
490
512
 
491
513
  entities_to_check.each do |key, orphaned_entities|
514
+ next if orphaned_entities.nil?
515
+
492
516
  orphaned_entities.each do |orphaned_entity|
493
517
  next unless solis_model?(orphaned_entity)
494
518
 
495
519
  # Skip if it's a readonly entity (like code tables)
496
- if (orphaned_entity.class.ancestors.map{|s| s.to_s} & embedded_readonly_entities).any?
520
+ if readonly_entity?(orphaned_entity, readonly_list)
497
521
  Solis::LOGGER.info("#{orphaned_entity.class.name} (id: #{orphaned_entity.id}) is in embedded_readonly list. Skipping deletion.")
498
522
  next
499
523
  end
@@ -1,75 +1,282 @@
1
1
  require 'solis/store/sparql/client'
2
- require 'solis/query/result_transformer'
3
2
  require 'solis/config_file'
4
3
 
5
4
  class Solis::Query::Runner
6
5
  def self.run(entity, query, options = {})
7
- sparql_client = Solis::Store::Sparql::Client.new(
8
- Solis::Options.instance.get[:sparql_endpoint],
9
- graph_name: graph_name
10
- )
6
+ result = {}
11
7
 
12
- raw_result = sparql_client.query(query, options)
13
- model = options[:model] || nil
8
+ c = Solis::Store::Sparql::Client.new(Solis::Options.instance.get[:sparql_endpoint], graph_name: graph_name)
9
+ r = c.query(query, options)
14
10
 
15
- transform_result(raw_result, entity, model)
11
+ if r.is_a?(SPARQL::Client)
12
+ result = direct_transform_with_embedding(r, entity, options)
13
+ else
14
+ t = r.map(&:to_h)
15
+ result = sanitize_result({'@graph' => t})
16
+ end
17
+ result
16
18
  rescue StandardError => e
17
19
  puts e.message
18
20
  raise e
19
21
  end
20
22
 
23
+ def self.direct_transform_with_embedding(client, entity, options = {})
24
+ results = client.query('select * where{?s ?p ?o}')
25
+
26
+ # Step 1: Group all triples by subject
27
+ grouped = group_by_subject(results)
28
+
29
+ # Step 2: Build objects index (without embedding yet)
30
+ objects_index = build_objects_index(grouped)
31
+
32
+ # Step 3: Embed references recursively
33
+ max_depth = options[:max_embed_depth] || 10
34
+ root_subjects = find_root_subjects(grouped, entity)
35
+
36
+ root_subjects.map do |subject|
37
+ embed_references(objects_index[subject], objects_index, max_depth, Set.new)
38
+ end.compact
39
+ end
40
+
21
41
  private
22
42
 
23
- def self.transform_result(raw_result, entity, model)
24
- if raw_result.is_a?(SPARQL::Client)
25
- frame_and_transform(raw_result, entity, model)
26
- else
27
- transform_select_results(raw_result, model)
43
+ def self.group_by_subject(results)
44
+ results.each_with_object({}) do |solution, acc|
45
+ subject = solution.s.to_s
46
+ acc[subject] ||= []
47
+ acc[subject] << { predicate: solution.p, object: solution.o }
28
48
  end
29
49
  end
30
50
 
31
- def self.frame_and_transform(sparql_result, entity, model)
32
- graph = build_graph_from_result(sparql_result)
33
- context = build_context(entity)
51
+ def self.build_objects_index(grouped)
52
+ grouped.each_with_object({}) do |(subject, triples), index|
53
+ obj = {
54
+ '_id' => subject, # Full URI for resolution
55
+ 'id' => nil, # Will be set from predicate if exists
56
+ '@subject' => subject, # Internal marker for reference resolution
57
+ '@type' => nil
58
+ }
59
+
60
+ triples.each do |triple|
61
+ predicate = triple[:predicate]
62
+ object = triple[:object]
63
+
64
+ # Handle rdf:type
65
+ if predicate.to_s =~ /type$/i || predicate == RDF::RDFV.type
66
+ obj['@type'] = object.to_s.split('/').last
67
+ next
68
+ end
34
69
 
35
- framed = nil
36
- JSON::LD::API.fromRDF(graph) do |expanded|
37
- framed = JSON::LD::API.frame(expanded, context)
70
+ # Get predicate name (last part of URI)
71
+ pred_name = predicate.to_s.split('/').last.underscore
72
+
73
+ # Extract value
74
+ value = if object.is_a?(RDF::URI)
75
+ { '@ref' => object.to_s } # Mark as reference for later resolution
76
+ else
77
+ extract_value(object)
78
+ end
79
+
80
+ # Capture the 'id' predicate value specifically
81
+ if pred_name == 'id'
82
+ obj['id'] = value
83
+ next
84
+ end
85
+
86
+ # Handle multiple values for same predicate
87
+ if obj.key?(pred_name)
88
+ obj[pred_name] = [obj[pred_name]] unless obj[pred_name].is_a?(Array)
89
+ obj[pred_name] << value
90
+ else
91
+ obj[pred_name] = value
92
+ end
93
+ end
94
+
95
+ # Fallback: if no 'id' predicate was found, extract from URI
96
+ if obj['id'].nil?
97
+ obj['id'] = subject.split('/').last
98
+ end
99
+
100
+ if obj['@type'].nil?
101
+ obj['@type'] = subject.split('/')[-2].classify
102
+ end
103
+
104
+ index[subject] = obj
38
105
  end
106
+ end
107
+
108
+ def self.find_root_subjects(grouped, entity)
109
+ # Find subjects that match the requested entity type
110
+ grouped.select do |subject, triples|
111
+ type_triple = triples.find { |t| t[:predicate].to_s =~ /type$/i || t[:predicate] == RDF::RDFV.type }
112
+ next false unless type_triple
39
113
 
40
- Solis::Query::ResultTransformer.new(model).transform(framed)
114
+ type_name = type_triple[:object].to_s.split('/').last
115
+ type_name.downcase == entity.downcase ||
116
+ type_name.tableize == entity.tableize ||
117
+ type_name == entity
118
+ end.keys
41
119
  end
42
120
 
43
- def self.transform_select_results(raw_result, model)
44
- results = raw_result.map(&:to_h)
45
- Solis::Query::ResultTransformer.new(model).transform({'@graph' => results})
121
+ def self.embed_references(obj, objects_index, max_depth, visited, current_depth = 0)
122
+ return nil if obj.nil?
123
+
124
+ subject = obj['@subject']
125
+
126
+ # At max depth, return minimal reference with both IDs
127
+ if current_depth >= max_depth
128
+ #return { '_id' => obj['_id'], 'id' => obj['id'], '@type' => obj['@type'] }
129
+ return { '_id' => obj['_id'], 'id' => obj['id'] }
130
+ end
131
+
132
+ # Circular reference detection
133
+ if visited.include?(subject)
134
+ # Return a reference object instead of embedding
135
+ #return { '_id' => obj['_id'], 'id' => obj['id'], '@type' => obj['@type'] }
136
+ return { '_id' => obj['_id'], 'id' => obj['id'] }
137
+ end
138
+
139
+ visited = visited.dup
140
+ visited.add(subject)
141
+
142
+ # Create clean copy without internal markers (except _id)
143
+ result = {
144
+ '_id' => obj['_id'],
145
+ 'id' => obj['id']
146
+ }
147
+
148
+ obj.each do |key, value|
149
+ next if key.start_with?('@') # Skip internal markers
150
+ next if key == '_id' || key == 'id' # Already added
151
+
152
+ result[key] = resolve_value(value, objects_index, max_depth, visited, current_depth)
153
+ end
154
+
155
+ result
46
156
  end
47
157
 
48
- def self.build_graph_from_result(sparql_result)
49
- graph = RDF::Graph.new
50
- sparql_result.query('select * where{?s ?p ?o}').each do |statement|
51
- graph << [statement.s, statement.p, statement.o]
158
+ def self.resolve_value(value, objects_index, max_depth, visited, current_depth)
159
+ case value
160
+ when Array
161
+ value.map { |v| resolve_value(v, objects_index, max_depth, visited, current_depth) }
162
+ when Hash
163
+ if value.key?('@ref')
164
+ # This is a reference - try to embed it
165
+ ref_uri = value['@ref']
166
+ referenced_obj = objects_index[ref_uri]
167
+
168
+ if referenced_obj
169
+ embed_references(referenced_obj, objects_index, max_depth, visited, current_depth + 1)
170
+ else
171
+ # External reference - return both IDs
172
+ { '_id' => ref_uri, 'id' => ref_uri.split('/').last }
173
+ end
174
+ else
175
+ # Regular hash - recurse
176
+ value.transform_values { |v| resolve_value(v, objects_index, max_depth, visited, current_depth) }
177
+ end
178
+ else
179
+ value
52
180
  end
53
- graph
54
181
  end
55
182
 
56
- def self.build_context(entity)
57
- JSON.parse(%(
58
- {
59
- "@context": {
60
- "@vocab": "#{graph_name}",
61
- "id": "@id"
62
- },
63
- "@type": "#{entity}",
64
- "@embed": "@always"
65
- }
66
- ))
183
+ def self.extract_value(literal)
184
+ return literal.to_s if literal.is_a?(RDF::URI)
185
+
186
+ datatype = literal.datatype&.to_s
187
+
188
+ case datatype
189
+ when "http://www.w3.org/2001/XMLSchema#dateTime"
190
+ DateTime.parse(literal.value)
191
+ when "http://www.w3.org/2001/XMLSchema#date"
192
+ Date.parse(literal.value)
193
+ when "http://www.w3.org/2001/XMLSchema#boolean"
194
+ literal.value == "true"
195
+ when "http://www.w3.org/2001/XMLSchema#integer", "http://www.w3.org/2001/XMLSchema#int"
196
+ literal.value.to_i
197
+ when "http://www.w3.org/2001/XMLSchema#float", "http://www.w3.org/2001/XMLSchema#double", "http://www.w3.org/2001/XMLSchema#decimal"
198
+ literal.value.to_f
199
+ when "http://www.w3.org/2006/time#DateTimeInterval"
200
+ ISO8601::TimeInterval.parse(literal.value).to_s
201
+ when "http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON"
202
+ JSON.parse(literal.value) rescue literal.value
203
+ else
204
+ # Handle language-tagged strings
205
+ if literal.respond_to?(:language) && literal.language
206
+ { '@value' => literal.value, '@language' => literal.language.to_s }
207
+ else
208
+ literal.value
209
+ end
210
+ end
211
+ rescue StandardError => e
212
+ Solis::LOGGER.warn("Error extracting value: #{e.message}")
213
+ literal.to_s
67
214
  end
68
215
 
69
216
  def self.graph_name
70
- graphs = Solis::Options.instance.get[:graphs]
71
- raise Solis::Error::NotFoundError, 'No graph name found' if graphs.nil?
217
+ Solis::Options.instance.get.key?(:graphs) ? Solis::Options.instance.get[:graphs].select { |s| s['type'].eql?(:main) }&.first['name'] : ''
218
+ end
219
+
220
+ # Keep original methods for backward compatibility
221
+ def self.sanitize_result(framed)
222
+ data = framed&.key?('@graph') ? framed['@graph'] : [framed]
223
+ sanitatize_data_in_result(data)
224
+ end
72
225
 
73
- graphs.find { |g| g['type'].eql?(:main) }&.fetch('name') || ''
226
+ def self.sanitatize_data_in_result(data)
227
+ data.map do |d|
228
+ d.delete_if { |e| e =~ /^@/ }
229
+ if d.is_a?(Hash)
230
+ new_d = {}
231
+ d.each do |k, v|
232
+ if v.is_a?(Hash)
233
+ if v.key?('@type')
234
+ type = v['@type']
235
+ if v.key?('@value')
236
+ value = v['@value']
237
+ case type
238
+ when "http://www.w3.org/2001/XMLSchema#dateTime"
239
+ value = DateTime.parse(value)
240
+ when "http://www.w3.org/2001/XMLSchema#date"
241
+ value = Date.parse(value)
242
+ when "http://www.w3.org/2006/time#DateTimeInterval"
243
+ value = ISO8601::TimeInterval.parse(value)
244
+ when "http://www.w3.org/2001/XMLSchema#boolean"
245
+ value = value == "true"
246
+ end
247
+ v = value
248
+ end
249
+ v = sanitize_result(v) if v.is_a?(Hash)
250
+ end
251
+ if v.is_a?(Hash)
252
+ new_d[k] = v.class.method_defined?(:value) ? v.value : sanitize_result(v)
253
+ else
254
+ new_d[k] = v.class.method_defined?(:value) ? v.value : v
255
+ end
256
+ elsif v.is_a?(Array)
257
+ new_d[k] = []
258
+ v.each do |vt|
259
+ if vt.is_a?(Hash)
260
+ if vt.key?('@value')
261
+ new_d[k] << vt['@value']
262
+ else
263
+ new_d[k] << (vt.is_a?(String) ? vt : sanitize_result(vt))
264
+ end
265
+ else
266
+ new_d[k] << (vt.is_a?(String) ? vt : sanitize_result(vt))
267
+ end
268
+ end
269
+ new_d[k].flatten!
270
+ else
271
+ new_d[k] = v.class.method_defined?(:value) ? v.value : v
272
+ end
273
+ end
274
+ d = new_d
275
+ end
276
+ d
277
+ end
278
+ rescue StandardError => e
279
+ Solis::LOGGER.error(e.message)
280
+ data
74
281
  end
75
282
  end
@@ -289,7 +289,7 @@ hide empty members
289
289
  graph_prefix = data[:ontologies][:base][:prefix]
290
290
  graph_name = data[:ontologies][:base][:uri]
291
291
 
292
- description = metadata[:comment]
292
+ description = metadata[:description]
293
293
  label = metadata[:label]
294
294
  target_class = "#{graph_prefix}:#{entity_name}"
295
295
  node = metadata[:sub_class_of]
@@ -454,9 +454,19 @@ hide empty members
454
454
 
455
455
  datas.each do |data|
456
456
  data[:entities].select { |_k, v| !v[:same_as].empty? }.each do |k, v|
457
- prefix, verb = v[:same_as].split(':')
458
- rdf_vocabulary = RDF::Vocabulary.from_sym(prefix.upcase)
459
- rdf_verb = rdf_vocabulary[verb.to_sym]
457
+ same_as_value = v[:same_as].strip
458
+
459
+ if same_as_value.start_with?('<') && same_as_value.end_with?('>')
460
+ # Full URI format: <http://xmlns.com/foaf/0.1/Person>
461
+ uri_string = same_as_value[1...-1]
462
+ rdf_verb = RDF::URI(uri_string)
463
+ else
464
+ # Prefixed name format: foaf:Person
465
+ prefix, verb = same_as_value.split(':')
466
+ rdf_vocabulary = RDF::Vocabulary.from_sym(prefix.upcase)
467
+ rdf_verb = rdf_vocabulary[verb.to_sym]
468
+ end
469
+
460
470
  graph << RDF::Statement.new(rdf_verb, RDF::RDFV.type, RDF::OWL.Class)
461
471
  graph << RDF::Statement.new(rdf_verb, RDF::Vocab::OWL.sameAs, o[k.to_sym])
462
472
  rescue StandardError => e
data/lib/solis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Solis
2
- VERSION = "0.98.0"
2
+ VERSION = "0.100.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.98.0
4
+ version: 0.100.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik
@@ -299,7 +299,6 @@ files:
299
299
  - lib/solis/query.rb
300
300
  - lib/solis/query/construct.rb
301
301
  - lib/solis/query/filter.rb
302
- - lib/solis/query/result_transformer.rb
303
302
  - lib/solis/query/run.rb
304
303
  - lib/solis/resource.rb
305
304
  - lib/solis/shape.rb
@@ -1,130 +0,0 @@
1
- class Solis::Query::ResultTransformer
2
- def initialize(model)
3
- @model = model
4
- @type_mappings = load_type_mappings
5
- @cardinality_map = build_cardinality_map
6
- end
7
-
8
- def transform(data)
9
- items = data.key?('@graph') ? data['@graph'] : [data]
10
- items.map { |item| transform_item(item) }
11
- end
12
-
13
- private
14
-
15
- def transform_item(item)
16
- clean_item = remove_json_ld_metadata(item)
17
- cast_and_shape(clean_item)
18
- end
19
-
20
- def cast_and_shape(item)
21
- item.each_with_object({}) do |(key, value), result|
22
- value = cast_value(value)
23
- result[key] = enforce_cardinality(key, value)
24
- end
25
- end
26
-
27
- # def cast_and_shape(item)
28
- # item.each_with_object({}) do |(key, value), result|
29
- # value = cast_value(value)
30
- # value = enforce_cardinality(key, value)
31
- # value = transform_nested_entity(key, value)
32
- # result[key] = value
33
- # end
34
- # end
35
-
36
- def transform_nested_entity(property_key, value)
37
- return value if @model.nil?
38
-
39
- metadata = @cardinality_map[property_key]
40
- return value if metadata.nil?
41
-
42
- datatype = metadata[:datatype]
43
- node = metadata[:node]
44
-
45
- # Check if datatype points to another entity (has a node)
46
- if node.is_a?(RDF::URI) && value.is_a?(Hash)
47
- # Recursively transform nested entity if we have its model
48
- # You'd need a way to resolve the node URI to the model class
49
- nested_model = resolve_model_from_node(datatype)
50
- Solis::Query::ResultTransformer.new(nested_model).transform(value) if nested_model
51
- elsif value.is_a?(Array) && value.all?(Hash)
52
- # Transform array of nested entities
53
- value.map { |v| transform_nested_entity(property_key, v) }
54
- else
55
- value
56
- end
57
- end
58
-
59
- def resolve_model_from_node(datatype_node)
60
- @model.graph.shape_as_model(datatype_node.to_s)
61
- end
62
-
63
-
64
- def remove_json_ld_metadata(item)
65
- item.reject { |key| key.start_with?('@') }
66
- end
67
-
68
- def cast_value(value)
69
- case value
70
- when Hash
71
- handle_typed_value(value)
72
- when Array
73
- value.map { |v| cast_value(v) }
74
- else
75
- value
76
- end
77
- end
78
-
79
- def handle_typed_value(value)
80
- return value unless value.key?('@type') && value.key?('@value')
81
-
82
- type = value['@type']
83
- raw_value = value['@value']
84
-
85
- cast_by_type(type, raw_value)
86
- end
87
-
88
- def cast_by_type(type, value)
89
- caster = @type_mappings[type]
90
- caster ? caster.call(value) : value
91
- end
92
-
93
- def enforce_cardinality(property_key, value)
94
- return value if @model.nil?
95
-
96
- metadata = @cardinality_map[property_key]
97
- return value if metadata.nil?
98
-
99
- maxcount = metadata[:maxcount]
100
-
101
- # If maxcount is nil or > 1, ensure it's an array
102
- if maxcount.nil? || maxcount > 1
103
- value.is_a?(Array) ? value : [value]
104
- # If maxcount is 0 or 1, ensure it's a single value
105
- else
106
- value.is_a?(Array) ? value.first : value
107
- end
108
- end
109
-
110
- def build_cardinality_map
111
- return {} if @model.nil? || @model.metadata.nil?
112
-
113
- attributes = @model.metadata[:attributes] || {}
114
-
115
- attributes.each_with_object({}) do |(property_name, property_metadata), map|
116
- map[property_name.to_s] = property_metadata
117
- end
118
- end
119
-
120
- def load_type_mappings
121
- {
122
- "http://www.w3.org/2001/XMLSchema#dateTime" => ->(v) { DateTime.parse(v) },
123
- "http://www.w3.org/2001/XMLSchema#date" => ->(v) { Date.parse(v) },
124
- "http://www.w3.org/2006/time#DateTimeInterval" => ->(v) { ISO8601::TimeInterval.parse(v) },
125
- "http://www.w3.org/2001/XMLSchema#boolean" => ->(v) { v == "true" },
126
- "http://www.w3.org/2001/XMLSchema#integer" => ->(v) { v.to_i },
127
- "http://www.w3.org/2001/XMLSchema#decimal" => ->(v) { BigDecimal(v) }
128
- }
129
- end
130
- end