solis 0.122.0 → 0.123.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 +187 -227
- data/lib/solis/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d9aacb8e97e1c584368f1c792a26ca48b8f885e64faa6c9d39f09f59a2b1260
|
|
4
|
+
data.tar.gz: 1caf4c17d138b900041f758f493a1fccdb302eb27b86651911dd22c529545219
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49e3387254454c6bbea21ab51f3bbe2440022dd4f23eaffb77351aeb5e6cf967e79d2f9e6e25fc971c3614aa128afe82c6b63c6116e3f195e5bc535657e7dd8a
|
|
7
|
+
data.tar.gz: 9e8478ee6bfe98cf1d3a7dae252383120ff93bb1684a0e8f2bb1aae1296ad6e0472b8b7a34042be1b8b9a90a02d99c8c413c487e1ed298622b22c2c83100ee8e
|
data/lib/solis/model.rb
CHANGED
|
@@ -87,22 +87,22 @@ module Solis
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def to_ttl(resolve_all = true)
|
|
90
|
-
graph = as_graph(self, resolve_all)
|
|
90
|
+
graph = as_graph(self, deep: resolve_all)
|
|
91
91
|
graph.dump(:ttl)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def dump(format = :ttl, resolve_all = true)
|
|
95
|
-
graph = as_graph(self, resolve_all)
|
|
95
|
+
graph = as_graph(self, deep: resolve_all)
|
|
96
96
|
graph.dump(format)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
def to_graph(resolve_all = true)
|
|
100
|
-
as_graph(self, resolve_all)
|
|
100
|
+
as_graph(self, deep: resolve_all)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def valid?
|
|
104
104
|
begin
|
|
105
|
-
graph = as_graph(self
|
|
105
|
+
graph = as_graph(self)
|
|
106
106
|
rescue Solis::Error::InvalidAttributeError => e
|
|
107
107
|
Solis::LOGGER.error(e.message)
|
|
108
108
|
end
|
|
@@ -156,60 +156,44 @@ values ?s {<#{self.graph_id}>}
|
|
|
156
156
|
data = properties_to_hash(self)
|
|
157
157
|
result = update(data, validate_dependencies, top_level, sparql)
|
|
158
158
|
else
|
|
159
|
-
data = properties_to_hash(self)
|
|
160
|
-
attributes = data.include?('attributes') ? data['attributes'] : data
|
|
161
159
|
readonly_list = (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
|
|
162
160
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
existing_ids = self.class.batch_exists?(sparql, all_embedded)
|
|
177
|
-
|
|
178
|
-
# Separate embedded entities into creates and updates
|
|
179
|
-
to_create_embedded = []
|
|
180
|
-
to_update_embedded = []
|
|
181
|
-
|
|
182
|
-
all_embedded.each do |embedded|
|
|
183
|
-
entity_exists = existing_ids.include?(embedded.graph_id)
|
|
184
|
-
|
|
185
|
-
if readonly_entity?(embedded, readonly_list)
|
|
186
|
-
unless entity_exists
|
|
187
|
-
Solis::LOGGER.warn("#{embedded.class.name} (id: #{embedded.id}) is readonly but does not exist in database. Skipping.")
|
|
188
|
-
end
|
|
161
|
+
# Enumerate the whole in-memory tree: self plus every embedded descendant.
|
|
162
|
+
all_entities = collect_known_entities(self).values
|
|
163
|
+
existing_ids = self.class.batch_exists?(sparql, all_entities)
|
|
164
|
+
|
|
165
|
+
# Classify each entity: new (insert), existing embedded (update), or readonly.
|
|
166
|
+
# readonly only protects EMBEDDED entities; the entity being saved (self) is
|
|
167
|
+
# always created even when its class is a code table.
|
|
168
|
+
new_entities = []
|
|
169
|
+
existing_embedded = []
|
|
170
|
+
all_entities.each do |entity|
|
|
171
|
+
entity_exists = existing_ids.include?(entity.graph_id)
|
|
172
|
+
if !entity.equal?(self) && readonly_entity?(entity, readonly_list)
|
|
173
|
+
Solis::LOGGER.warn("#{entity.class.name} (id: #{entity.id}) is readonly but does not exist in database. Skipping.") unless entity_exists
|
|
189
174
|
elsif entity_exists
|
|
190
|
-
|
|
175
|
+
existing_embedded << entity
|
|
191
176
|
else
|
|
192
|
-
|
|
177
|
+
new_entities << entity
|
|
193
178
|
end
|
|
194
179
|
end
|
|
195
180
|
|
|
196
|
-
#
|
|
197
|
-
unless
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
sparql.insert_data(embedded_graph, graph: embedded_graph.name)
|
|
181
|
+
# Existing embedded entities are updated individually (each needs DELETE/INSERT).
|
|
182
|
+
unless existing_embedded.empty?
|
|
183
|
+
embedded_originals = batch_load_originals(existing_embedded)
|
|
184
|
+
existing_embedded.each do |embedded|
|
|
185
|
+
embedded.update(properties_to_hash(embedded), validate_dependencies, false, nil,
|
|
186
|
+
prefetched_original: embedded_originals[embedded.id])
|
|
187
|
+
end
|
|
204
188
|
end
|
|
205
189
|
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
190
|
+
# Serialize self and every new embedded entity into one INSERT DATA operation.
|
|
191
|
+
graph = RDF::Graph.new
|
|
192
|
+
graph.name = RDF::URI(self.class.graph_name)
|
|
193
|
+
visited = Set.new
|
|
194
|
+
new_entities.each { |entity| serialize_entity(graph, entity, false, visited, []) }
|
|
211
195
|
|
|
212
|
-
graph
|
|
196
|
+
validate_graph(graph) if validate_dependencies
|
|
213
197
|
|
|
214
198
|
Solis::LOGGER.info SPARQL::Client::Update::InsertData.new(graph, graph: graph.name).to_s if ConfigFile[:debug]
|
|
215
199
|
|
|
@@ -250,12 +234,10 @@ values ?s {<#{self.graph_id}>}
|
|
|
250
234
|
id = attributes.delete('id')
|
|
251
235
|
sparql = sparql_client || SPARQL::Client.new(self.class.sparql_endpoint)
|
|
252
236
|
|
|
253
|
-
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
original_klass = nil if original_klass && shallow_stub?(original_klass)
|
|
258
|
-
original_klass ||= self.query.filter({ language: self.class.language, filters: { id: [id] } }).find_all.map { |m| m }&.first
|
|
237
|
+
# prefetched_original is used only when it is a complete entity; an id-only stub
|
|
238
|
+
# cannot seed updated_klass (omitted mandatory attributes would be lost).
|
|
239
|
+
original_klass = prefetched_original unless prefetched_original && shallow_stub?(prefetched_original)
|
|
240
|
+
original_klass ||= load_original(id)
|
|
259
241
|
raise Solis::Error::NotFoundError if original_klass.nil?
|
|
260
242
|
updated_klass = original_klass.deep_dup
|
|
261
243
|
|
|
@@ -279,15 +261,12 @@ values ?s {<#{self.graph_id}>}
|
|
|
279
261
|
all_embedded = embedded_by_key.values.flatten
|
|
280
262
|
existing_ids = self.class.batch_exists?(sparql, all_embedded)
|
|
281
263
|
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
next if orig.nil?
|
|
287
|
-
Array(orig).each do |e|
|
|
288
|
-
original_embedded_lookup[e.id] = e if solis_model?(e) && e.id
|
|
289
|
-
end
|
|
264
|
+
# Batch-load full stored originals for embedded entities that already exist, so
|
|
265
|
+
# each recursive embedded update receives a complete original (one query per class).
|
|
266
|
+
existing_embedded = all_embedded.select do |e|
|
|
267
|
+
existing_ids.include?(e.graph_id) && !readonly_entity?(e, readonly_list)
|
|
290
268
|
end
|
|
269
|
+
embedded_originals = batch_load_originals(existing_embedded)
|
|
291
270
|
|
|
292
271
|
# Second pass: process embedded entities using batched results
|
|
293
272
|
embedded_by_key.each do |key, embedded_list|
|
|
@@ -319,9 +298,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
319
298
|
else
|
|
320
299
|
if entity_exists
|
|
321
300
|
embedded_data = properties_to_hash(embedded)
|
|
322
|
-
|
|
323
|
-
prefetched = original_embedded_lookup[embedded.id]
|
|
324
|
-
embedded.update(embedded_data, validate_dependencies, false, nil, prefetched_original: prefetched)
|
|
301
|
+
embedded.update(embedded_data, validate_dependencies, false, nil, prefetched_original: embedded_originals[embedded.id])
|
|
325
302
|
new_embedded_values << embedded
|
|
326
303
|
else
|
|
327
304
|
embedded_value = embedded.save(validate_dependencies, false)
|
|
@@ -369,10 +346,11 @@ values ?s {<#{self.graph_id}>}
|
|
|
369
346
|
Solis::LOGGER.info("#{original_klass.class.name} unchanged, skipping")
|
|
370
347
|
data = original_klass
|
|
371
348
|
else
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
delete_graph = as_graph(original_klass, false
|
|
349
|
+
# The delete graph carries the stored original's triples; the insert graph the
|
|
350
|
+
# updated entity's. Embedded children are emitted as URI references in both —
|
|
351
|
+
# they are persisted by their own recursive update/save above.
|
|
352
|
+
delete_graph = as_graph(original_klass, deep: false)
|
|
353
|
+
insert_graph = as_graph(updated_klass, deep: false)
|
|
376
354
|
where_graph = RDF::Graph.new(graph_name: RDF::URI("#{self.class.graph_name}#{tableized_class_name(self)}/#{id}"), data: RDF::Repository.new)
|
|
377
355
|
|
|
378
356
|
if id.is_a?(Array)
|
|
@@ -383,8 +361,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
383
361
|
where_graph << [RDF::URI("#{self.class.graph_name}#{tableized_class_name(self)}/#{id}"), :p, :o]
|
|
384
362
|
end
|
|
385
363
|
|
|
386
|
-
|
|
387
|
-
insert_graph = as_graph(updated_klass, true, insert_known)
|
|
364
|
+
validate_graph(insert_graph) if validate_dependencies
|
|
388
365
|
|
|
389
366
|
delete_insert_query = SPARQL::Client::Update::DeleteInsert.new(delete_graph, insert_graph, where_graph, graph: insert_graph.name).to_s
|
|
390
367
|
delete_insert_query.gsub!('_:p', '?p')
|
|
@@ -412,7 +389,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
412
389
|
|
|
413
390
|
data
|
|
414
391
|
rescue StandardError => e
|
|
415
|
-
original_graph = as_graph(original_klass, false) if defined?(original_klass) && original_klass
|
|
392
|
+
original_graph = as_graph(original_klass, deep: false) if defined?(original_klass) && original_klass
|
|
416
393
|
Solis::LOGGER.error(e.message)
|
|
417
394
|
Solis::LOGGER.error original_graph.dump(:ttl) if defined?(original_graph) && original_graph
|
|
418
395
|
Solis::LOGGER.error delete_insert_query if defined?(delete_insert_query)
|
|
@@ -464,14 +441,11 @@ values ?s {<#{self.graph_id}>}
|
|
|
464
441
|
to_create.each_slice(batch_size) do |batch|
|
|
465
442
|
combined_graph = RDF::Graph.new
|
|
466
443
|
combined_graph.name = RDF::URI(graph_name)
|
|
467
|
-
|
|
468
|
-
# Pre-collect known entities from all entities being created
|
|
469
|
-
known = {}
|
|
470
|
-
batch.each { |e| e.send(:collect_known_entities, e, known) }
|
|
444
|
+
visited = Set.new
|
|
471
445
|
|
|
472
446
|
batch.each do |entity|
|
|
473
447
|
entity.before_create_proc&.call(entity)
|
|
474
|
-
entity.send(:
|
|
448
|
+
entity.send(:serialize_entity, combined_graph, entity, true, visited, [])
|
|
475
449
|
end
|
|
476
450
|
|
|
477
451
|
sparql.insert_data(combined_graph, graph: combined_graph.name)
|
|
@@ -654,8 +628,8 @@ values ?s {<#{self.graph_id}>}
|
|
|
654
628
|
|
|
655
629
|
private
|
|
656
630
|
|
|
657
|
-
# Walk the entity tree and collect
|
|
658
|
-
#
|
|
631
|
+
# Walk the in-memory entity tree and collect every entity by UUID
|
|
632
|
+
# ({ uuid => entity }), following embedded (node_kind) attributes.
|
|
659
633
|
def collect_known_entities(entity, collected = {})
|
|
660
634
|
uuid = entity.instance_variable_get("@id")
|
|
661
635
|
return collected if uuid.nil? || collected.key?(uuid)
|
|
@@ -683,6 +657,25 @@ values ?s {<#{self.graph_id}>}
|
|
|
683
657
|
end
|
|
684
658
|
end
|
|
685
659
|
|
|
660
|
+
# Load the full stored entity for this model's class by id. Returns nil when absent.
|
|
661
|
+
def load_original(id)
|
|
662
|
+
self.query.filter({ language: self.class.language, filters: { id: [id] } })
|
|
663
|
+
.find_all.map { |m| m }&.first
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Load full stored originals for the given embedded models, one query per class.
|
|
667
|
+
# Returns { id => full_entity }.
|
|
668
|
+
def batch_load_originals(models)
|
|
669
|
+
originals = {}
|
|
670
|
+
models.select { |m| solis_model?(m) && m.id }.group_by(&:class).each do |_klass, group|
|
|
671
|
+
ids = group.map(&:id).uniq
|
|
672
|
+
group.first.query
|
|
673
|
+
.filter({ language: group.first.class.language, filters: { id: ids } })
|
|
674
|
+
.find_all.each { |entity| originals[entity.id] = entity }
|
|
675
|
+
end
|
|
676
|
+
originals
|
|
677
|
+
end
|
|
678
|
+
|
|
686
679
|
# Helper method to check if an entity is readonly (code table)
|
|
687
680
|
def readonly_entity?(entity, readonly_list = nil)
|
|
688
681
|
readonly_list ||= (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
|
|
@@ -783,183 +776,150 @@ values ?s {<#{self.graph_id}>}
|
|
|
783
776
|
Set.new(results.map { |r| r[:o].to_s })
|
|
784
777
|
end
|
|
785
778
|
|
|
786
|
-
|
|
779
|
+
# Build an RDF::Graph for `entity`. Pure: reads only the in-memory entity tree,
|
|
780
|
+
# never the store, and performs no validation. Embedded children are emitted as
|
|
781
|
+
# URI references; when `deep` is true, embedded children that carry their own
|
|
782
|
+
# data (i.e. are not id-only references) are also serialized into the graph.
|
|
783
|
+
def as_graph(entity = self, deep: false)
|
|
787
784
|
graph = RDF::Graph.new
|
|
788
785
|
graph.name = RDF::URI(self.class.graph_name)
|
|
789
|
-
|
|
790
|
-
|
|
786
|
+
serialize_entity(graph, entity, deep, Set.new, [])
|
|
791
787
|
graph
|
|
792
788
|
end
|
|
793
789
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
uuid
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
graph << [id, RDF::RDFV.type, klass_metadata[:target_class]]
|
|
790
|
+
# Emit `entity`'s own triples (rdf:type + attribute statements) into `graph` and
|
|
791
|
+
# return the entity URI. `visited` guards against emitting the same entity twice;
|
|
792
|
+
# `hierarchy` guards against same-class recursion cycles.
|
|
793
|
+
def serialize_entity(graph, entity, deep, visited, hierarchy)
|
|
794
|
+
uuid = entity.id
|
|
795
|
+
id = build_entity_uri(entity)
|
|
796
|
+
return id if uuid && visited.include?(uuid)
|
|
797
|
+
visited << uuid
|
|
804
798
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
809
|
-
end
|
|
799
|
+
metadata = entity.class.metadata
|
|
800
|
+
hierarchy.push("#{entity.class.name}(#{uuid})")
|
|
801
|
+
graph << [id, RDF::RDFV.type, metadata[:target_class]]
|
|
810
802
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
else
|
|
814
|
-
# A store-fetched original carries id-only stub children, so its subgraph is
|
|
815
|
-
# not re-resolved. In the create path (skip_store_fetch) known_entities is
|
|
816
|
-
# pre-populated with full in-memory entities whose children must still be built.
|
|
817
|
-
resolve_all = false unless skip_store_fetch
|
|
818
|
-
klass.instance_variables.map { |m| m.to_s.gsub(/^@/, '') }
|
|
819
|
-
.select { |s| !["model_name", "model_plural_name"].include?(s) }.each do |attribute|
|
|
820
|
-
data = klass.instance_variable_get("@#{attribute}")
|
|
821
|
-
original_data = original_klass.instance_variable_get("@#{attribute}")
|
|
822
|
-
original_klass.instance_variable_set("@#{attribute}", data) unless original_data.eql?(data)
|
|
823
|
-
end
|
|
824
|
-
end
|
|
825
|
-
|
|
826
|
-
# Cache entity for potential reuse in recursive calls
|
|
827
|
-
known_entities[uuid] = original_klass
|
|
828
|
-
|
|
829
|
-
begin
|
|
830
|
-
make_graph(graph, hierarchy, id, original_klass, klass_metadata, resolve_all, known_entities, skip_store_fetch: skip_store_fetch)
|
|
831
|
-
rescue => e
|
|
832
|
-
Solis::LOGGER.error(e.message)
|
|
833
|
-
raise e
|
|
803
|
+
metadata[:attributes].each do |attribute, attr_metadata|
|
|
804
|
+
serialize_attribute(graph, id, entity, attribute, attr_metadata, deep, visited, hierarchy)
|
|
834
805
|
end
|
|
835
806
|
|
|
836
807
|
hierarchy.pop
|
|
837
808
|
id
|
|
809
|
+
rescue StandardError => e
|
|
810
|
+
Solis::LOGGER.error(e.message)
|
|
811
|
+
raise e
|
|
838
812
|
end
|
|
839
813
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
814
|
+
# Emit the statements for a single attribute of `entity` into `graph`.
|
|
815
|
+
# For embedded attributes the child is emitted as a URI reference; when `deep`
|
|
816
|
+
# is true a child carrying its own data is also serialized into `graph`.
|
|
817
|
+
def serialize_attribute(graph, id, entity, attribute, metadata, deep, visited, hierarchy)
|
|
818
|
+
data = entity.instance_variable_get("@#{attribute}")
|
|
843
819
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
851
|
-
known_entities[uuid] = original_klass if original_klass
|
|
852
|
-
end
|
|
853
|
-
unless original_klass.nil?
|
|
854
|
-
klass = original_klass
|
|
855
|
-
data = klass.instance_variable_get("@#{attribute}")
|
|
856
|
-
end
|
|
857
|
-
end
|
|
858
|
-
# if data is still nil
|
|
859
|
-
raise Solis::Error::InvalidAttributeError, "#{hierarchy.join('.')}~#{klass.class.name}.#{attribute} min=#{metadata[:mincount]} and max=#{metadata[:maxcount]}" if data.nil?
|
|
860
|
-
end
|
|
820
|
+
# cardinality (min) check — mandatory attribute must be present
|
|
821
|
+
if data.nil? && metadata.key?(:mincount) && (metadata[:mincount].nil? || metadata[:mincount] > 0) &&
|
|
822
|
+
graph.query(RDF::Query.new({ attribute.to_sym => { RDF.type => metadata[:node] } })).size == 0
|
|
823
|
+
raise Solis::Error::InvalidAttributeError,
|
|
824
|
+
"#{hierarchy.join('.')}~#{entity.class.name}.#{attribute} min=#{metadata[:mincount]} and max=#{metadata[:maxcount]}"
|
|
825
|
+
end
|
|
861
826
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
end
|
|
827
|
+
# skip if nil or an empty container
|
|
828
|
+
return if data.nil? || ([Hash, Array, String].include?(data.class) && data.empty?)
|
|
865
829
|
|
|
866
|
-
|
|
867
|
-
|
|
830
|
+
case metadata[:datatype_rdf]
|
|
831
|
+
when 'http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'
|
|
832
|
+
data = data.to_json
|
|
833
|
+
end
|
|
868
834
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
data = data
|
|
835
|
+
# coerce embedded hashes to model instances
|
|
836
|
+
unless metadata[:node_kind].nil?
|
|
837
|
+
model = self.class.graph.shape_as_model(metadata[:datatype].to_s)
|
|
838
|
+
if data.is_a?(Hash)
|
|
839
|
+
data = model.new(data)
|
|
840
|
+
elsif data.is_a?(Array)
|
|
841
|
+
data = data.map { |m| m.is_a?(Hash) ? model.new(m) : m }
|
|
874
842
|
end
|
|
843
|
+
end
|
|
875
844
|
|
|
876
|
-
|
|
877
|
-
unless metadata[:node_kind].nil?
|
|
878
|
-
model = self.class.graph.shape_as_model(metadata[:datatype].to_s)
|
|
879
|
-
if data.is_a?(Hash)
|
|
880
|
-
data = model.new(data)
|
|
881
|
-
elsif data.is_a?(Array)
|
|
882
|
-
data = data.map { |m| m.is_a?(Hash) ? model.new(m) : m }
|
|
883
|
-
end
|
|
884
|
-
end
|
|
845
|
+
data = [data] unless data.is_a?(Array)
|
|
885
846
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
internal_resolve = false
|
|
893
|
-
d = build_ttl_objekt(graph, d, hierarchy, internal_resolve, known_entities)
|
|
894
|
-
elsif self.class.graph.shape_as_model(d.class.name) && hierarchy.select { |s| s =~ /^#{d.class.name}/ }.size == 0
|
|
895
|
-
internal_resolve = false
|
|
896
|
-
d = build_ttl_objekt(graph, d, hierarchy, internal_resolve, known_entities)
|
|
897
|
-
else
|
|
898
|
-
d = "#{klass.class.graph_name}#{d.class.name.tableize}/#{d.id}"
|
|
899
|
-
end
|
|
900
|
-
elsif solis_model?(d) && self.class.graph.shape?(d.class.name)
|
|
901
|
-
d = "#{klass.class.graph_name}#{d.class.name.tableize}/#{d.id}"
|
|
847
|
+
data.each do |d|
|
|
848
|
+
if solis_model?(d) && self.class.graph.shape?(d.class.name)
|
|
849
|
+
if deep && !shallow_stub?(d) && hierarchy.none? { |s| s.start_with?("#{d.class.name}(") }
|
|
850
|
+
d = serialize_entity(graph, d, deep, visited, hierarchy)
|
|
851
|
+
else
|
|
852
|
+
d = "#{self.class.graph_name}#{d.class.name.tableize}/#{d.id}"
|
|
902
853
|
end
|
|
854
|
+
end
|
|
903
855
|
|
|
904
|
-
|
|
905
|
-
d = d.first
|
|
906
|
-
end
|
|
856
|
+
d = d.first if d.is_a?(Array) && d.length == 1
|
|
907
857
|
|
|
908
|
-
|
|
909
|
-
if d.is_a?(Hash) && (d.keys - ["@language", "@value"]).size == 0
|
|
910
|
-
if d['@value'].is_a?(Array)
|
|
911
|
-
d_r = []
|
|
912
|
-
d['@value'].each do |v|
|
|
913
|
-
d_r << RDF::Literal.new(v, language: d['@language'])
|
|
914
|
-
end
|
|
915
|
-
d_r
|
|
916
|
-
else
|
|
917
|
-
RDF::Literal.new(d['@value'], language: d['@language'])
|
|
918
|
-
end
|
|
919
|
-
else
|
|
920
|
-
RDF::Literal.new(d, language: @language)
|
|
921
|
-
end
|
|
922
|
-
elsif metadata[:datatype_rdf].eql?('http://www.w3.org/2001/XMLSchema#anyURI')
|
|
923
|
-
RDF::Literal.new(d.to_s, datatype: RDF::XSD.anyURI)
|
|
924
|
-
elsif metadata[:node].is_a?(RDF::URI)
|
|
925
|
-
RDF::URI(d)
|
|
926
|
-
elsif metadata[:datatype_rdf] =~ /datatypes\/edtf/ || metadata[:datatype_rdf] =~ /edtf$/i
|
|
927
|
-
# Handle EDTF dates
|
|
928
|
-
begin
|
|
929
|
-
RDF::Literal::EDTF.new(d)
|
|
930
|
-
rescue StandardError => e
|
|
931
|
-
raise Solis::Error::InvalidDatatypeError, "#{hierarchy.join('.')}.#{attribute}: #{e.message}"
|
|
932
|
-
end
|
|
933
|
-
elsif metadata[:datatype_rdf].eql?('http://www.w3.org/2006/time#DateTimeInterval')
|
|
934
|
-
begin
|
|
935
|
-
datatype = metadata[:datatype_rdf]
|
|
936
|
-
RDF::Literal.new(ISO8601::TimeInterval.parse(d).to_s, datatype: datatype)
|
|
937
|
-
rescue StandardError => e
|
|
938
|
-
raise Solis::Error::InvalidDatatypeError, "#{hierarchy.join('.')}.#{attribute}: #{e.message}"
|
|
939
|
-
end
|
|
940
|
-
else
|
|
941
|
-
datatype = RDF::Vocabulary.find_term(metadata[:datatype_rdf])
|
|
942
|
-
datatype = metadata[:node] if datatype.nil?
|
|
943
|
-
datatype = metadata[:datatype_rdf] if datatype.nil?
|
|
944
|
-
RDF::Literal.new(d, datatype: datatype)
|
|
945
|
-
end
|
|
858
|
+
d = coerce_literal(d, metadata, attribute, hierarchy)
|
|
946
859
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
860
|
+
Array(d).each { |v| graph << [id, RDF::URI("#{metadata[:path]}"), v] }
|
|
861
|
+
end
|
|
862
|
+
end
|
|
950
863
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
864
|
+
# Coerce a serialized value to its RDF term according to the attribute datatype.
|
|
865
|
+
def coerce_literal(d, metadata, attribute, hierarchy)
|
|
866
|
+
if metadata[:datatype_rdf].eql?('http://www.w3.org/1999/02/22-rdf-syntax-ns#langString')
|
|
867
|
+
if d.is_a?(Hash) && (d.keys - ["@language", "@value"]).size == 0
|
|
868
|
+
if d['@value'].is_a?(Array)
|
|
869
|
+
d['@value'].map { |v| RDF::Literal.new(v, language: d['@language']) }
|
|
955
870
|
else
|
|
956
|
-
|
|
871
|
+
RDF::Literal.new(d['@value'], language: d['@language'])
|
|
957
872
|
end
|
|
873
|
+
else
|
|
874
|
+
RDF::Literal.new(d, language: @language)
|
|
958
875
|
end
|
|
876
|
+
elsif metadata[:datatype_rdf].eql?('http://www.w3.org/2001/XMLSchema#anyURI')
|
|
877
|
+
RDF::Literal.new(d.to_s, datatype: RDF::XSD.anyURI)
|
|
878
|
+
elsif metadata[:node].is_a?(RDF::URI)
|
|
879
|
+
RDF::URI(d)
|
|
880
|
+
elsif metadata[:datatype_rdf] =~ /datatypes\/edtf/ || metadata[:datatype_rdf] =~ /edtf$/i
|
|
881
|
+
begin
|
|
882
|
+
RDF::Literal::EDTF.new(d)
|
|
883
|
+
rescue StandardError => e
|
|
884
|
+
raise Solis::Error::InvalidDatatypeError, "#{hierarchy.join('.')}.#{attribute}: #{e.message}"
|
|
885
|
+
end
|
|
886
|
+
elsif metadata[:datatype_rdf].eql?('http://www.w3.org/2006/time#DateTimeInterval')
|
|
887
|
+
begin
|
|
888
|
+
RDF::Literal.new(ISO8601::TimeInterval.parse(d).to_s, datatype: metadata[:datatype_rdf])
|
|
889
|
+
rescue StandardError => e
|
|
890
|
+
raise Solis::Error::InvalidDatatypeError, "#{hierarchy.join('.')}.#{attribute}: #{e.message}"
|
|
891
|
+
end
|
|
892
|
+
else
|
|
893
|
+
datatype = RDF::Vocabulary.find_term(metadata[:datatype_rdf])
|
|
894
|
+
datatype = metadata[:node] if datatype.nil?
|
|
895
|
+
datatype = metadata[:datatype_rdf] if datatype.nil?
|
|
896
|
+
RDF::Literal.new(d, datatype: datatype)
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# Validate a serialized insert graph according to the configured :validation mode
|
|
901
|
+
# (Solis::Options key :validation):
|
|
902
|
+
# :cardinality (default) — no extra check; minCount is already enforced inline
|
|
903
|
+
# during serialization (raises InvalidAttributeError).
|
|
904
|
+
# :warn — run full SHACL, log every non-conformance as a warning.
|
|
905
|
+
# :full — run full SHACL, raise InvalidAttributeError on any.
|
|
906
|
+
def validate_graph(graph)
|
|
907
|
+
mode = (Solis::Options.instance.get[:validation] || :cardinality).to_sym
|
|
908
|
+
return if mode == :cardinality
|
|
909
|
+
|
|
910
|
+
shapes = SHACL.get_shapes(self.class.graph.instance_variable_get(:@graph))
|
|
911
|
+
report = shapes.execute(graph)
|
|
912
|
+
return if report.conform?
|
|
913
|
+
|
|
914
|
+
messages = Array(report.results).map do |r|
|
|
915
|
+
r.respond_to?(:message) ? Array(r.message).join(', ') : r.to_s
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
if mode == :warn
|
|
919
|
+
messages.each { |m| Solis::LOGGER.warn("SHACL: #{m}") }
|
|
920
|
+
else
|
|
921
|
+
raise Solis::Error::InvalidAttributeError, "SHACL validation failed: #{messages.join('; ')}"
|
|
959
922
|
end
|
|
960
|
-
rescue StandardError => e
|
|
961
|
-
Solis::LOGGER.error(e.message)
|
|
962
|
-
raise e
|
|
963
923
|
end
|
|
964
924
|
|
|
965
925
|
def properties_to_hash(model)
|
data/lib/solis/version.rb
CHANGED