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 +4 -4
- data/lib/solis/model.rb +39 -15
- data/lib/solis/query/run.rb +249 -42
- data/lib/solis/shape/reader/sheet.rb +14 -4
- data/lib/solis/version.rb +1 -1
- metadata +1 -2
- data/lib/solis/query/result_transformer.rb +0 -130
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 848141bb6e9f81fea57188c0a10cc3cf86b656a137e5f5a2a665a835af2ef6f5
|
|
4
|
+
data.tar.gz: 49505d5ce710f03daa6b14f2488df1c3d0ff8e2e24301b17940aa5d60aed67d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/solis/query/run.rb
CHANGED
|
@@ -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
|
-
|
|
8
|
-
Solis::Options.instance.get[:sparql_endpoint],
|
|
9
|
-
graph_name: graph_name
|
|
10
|
-
)
|
|
6
|
+
result = {}
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
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[:
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
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.
|
|
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
|