solis 0.98.0 → 0.99.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: 9c57f5cfabe93ada0f5aa3c008ecafa1b130147dfc7ca5b77930362a330a3457
4
+ data.tar.gz: 7b9854033bb6e4677bde52c4557705f4f48adffe2e0988af33600890c4615984
5
5
  SHA512:
6
- metadata.gz: ec93a8f1b15c02a1b2b5b52e4f85d0d3058c2528ad73d33d15944e5a3c5609bbfe9e2e83e8ab15efc41c1e98350745903b089cf3902cd2157a3a2aaaeb0f5657
7
- data.tar.gz: fd75bb60d80282d6c285b0bb004cdb466a0fa3d6ce2de9960732bb201e5d068c6c4256e7cf560dbf1b67b86371706659f562bc72bb149826285f22cefac6ba84
6
+ metadata.gz: 6f527bc987723ee900f250c8361a7570df65e3f6238c8ccc560c65097f94e0941a43540222e35e26196f4a70ed94d193040d55f0b2455f38f8ea2b2c39466e2f
7
+ data.tar.gz: 404047145bf332a80ee0bee495640071137682f009a1a17de7e21b8d531a2b3cf57aec7c704a423366f778c2cc2259267ed1e67f638fc092c38858c611f72a81
@@ -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
data/lib/solis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Solis
2
- VERSION = "0.98.0"
2
+ VERSION = "0.99.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.99.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