solis 0.121.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 +186 -233
- 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,7 +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
|
-
|
|
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)
|
|
254
241
|
raise Solis::Error::NotFoundError if original_klass.nil?
|
|
255
242
|
updated_klass = original_klass.deep_dup
|
|
256
243
|
|
|
@@ -274,15 +261,12 @@ values ?s {<#{self.graph_id}>}
|
|
|
274
261
|
all_embedded = embedded_by_key.values.flatten
|
|
275
262
|
existing_ids = self.class.batch_exists?(sparql, all_embedded)
|
|
276
263
|
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
next if orig.nil?
|
|
282
|
-
Array(orig).each do |e|
|
|
283
|
-
original_embedded_lookup[e.id] = e if solis_model?(e) && e.id
|
|
284
|
-
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)
|
|
285
268
|
end
|
|
269
|
+
embedded_originals = batch_load_originals(existing_embedded)
|
|
286
270
|
|
|
287
271
|
# Second pass: process embedded entities using batched results
|
|
288
272
|
embedded_by_key.each do |key, embedded_list|
|
|
@@ -314,9 +298,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
314
298
|
else
|
|
315
299
|
if entity_exists
|
|
316
300
|
embedded_data = properties_to_hash(embedded)
|
|
317
|
-
|
|
318
|
-
prefetched = original_embedded_lookup[embedded.id]
|
|
319
|
-
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])
|
|
320
302
|
new_embedded_values << embedded
|
|
321
303
|
else
|
|
322
304
|
embedded_value = embedded.save(validate_dependencies, false)
|
|
@@ -364,10 +346,11 @@ values ?s {<#{self.graph_id}>}
|
|
|
364
346
|
Solis::LOGGER.info("#{original_klass.class.name} unchanged, skipping")
|
|
365
347
|
data = original_klass
|
|
366
348
|
else
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
|
|
370
|
-
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)
|
|
371
354
|
where_graph = RDF::Graph.new(graph_name: RDF::URI("#{self.class.graph_name}#{tableized_class_name(self)}/#{id}"), data: RDF::Repository.new)
|
|
372
355
|
|
|
373
356
|
if id.is_a?(Array)
|
|
@@ -378,8 +361,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
378
361
|
where_graph << [RDF::URI("#{self.class.graph_name}#{tableized_class_name(self)}/#{id}"), :p, :o]
|
|
379
362
|
end
|
|
380
363
|
|
|
381
|
-
|
|
382
|
-
insert_graph = as_graph(updated_klass, true, insert_known)
|
|
364
|
+
validate_graph(insert_graph) if validate_dependencies
|
|
383
365
|
|
|
384
366
|
delete_insert_query = SPARQL::Client::Update::DeleteInsert.new(delete_graph, insert_graph, where_graph, graph: insert_graph.name).to_s
|
|
385
367
|
delete_insert_query.gsub!('_:p', '?p')
|
|
@@ -407,7 +389,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
407
389
|
|
|
408
390
|
data
|
|
409
391
|
rescue StandardError => e
|
|
410
|
-
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
|
|
411
393
|
Solis::LOGGER.error(e.message)
|
|
412
394
|
Solis::LOGGER.error original_graph.dump(:ttl) if defined?(original_graph) && original_graph
|
|
413
395
|
Solis::LOGGER.error delete_insert_query if defined?(delete_insert_query)
|
|
@@ -459,14 +441,11 @@ values ?s {<#{self.graph_id}>}
|
|
|
459
441
|
to_create.each_slice(batch_size) do |batch|
|
|
460
442
|
combined_graph = RDF::Graph.new
|
|
461
443
|
combined_graph.name = RDF::URI(graph_name)
|
|
462
|
-
|
|
463
|
-
# Pre-collect known entities from all entities being created
|
|
464
|
-
known = {}
|
|
465
|
-
batch.each { |e| e.send(:collect_known_entities, e, known) }
|
|
444
|
+
visited = Set.new
|
|
466
445
|
|
|
467
446
|
batch.each do |entity|
|
|
468
447
|
entity.before_create_proc&.call(entity)
|
|
469
|
-
entity.send(:
|
|
448
|
+
entity.send(:serialize_entity, combined_graph, entity, true, visited, [])
|
|
470
449
|
end
|
|
471
450
|
|
|
472
451
|
sparql.insert_data(combined_graph, graph: combined_graph.name)
|
|
@@ -649,8 +628,8 @@ values ?s {<#{self.graph_id}>}
|
|
|
649
628
|
|
|
650
629
|
private
|
|
651
630
|
|
|
652
|
-
# Walk the entity tree and collect
|
|
653
|
-
#
|
|
631
|
+
# Walk the in-memory entity tree and collect every entity by UUID
|
|
632
|
+
# ({ uuid => entity }), following embedded (node_kind) attributes.
|
|
654
633
|
def collect_known_entities(entity, collected = {})
|
|
655
634
|
uuid = entity.instance_variable_get("@id")
|
|
656
635
|
return collected if uuid.nil? || collected.key?(uuid)
|
|
@@ -678,6 +657,25 @@ values ?s {<#{self.graph_id}>}
|
|
|
678
657
|
end
|
|
679
658
|
end
|
|
680
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
|
+
|
|
681
679
|
# Helper method to check if an entity is readonly (code table)
|
|
682
680
|
def readonly_entity?(entity, readonly_list = nil)
|
|
683
681
|
readonly_list ||= (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
|
|
@@ -778,195 +776,150 @@ values ?s {<#{self.graph_id}>}
|
|
|
778
776
|
Set.new(results.map { |r| r[:o].to_s })
|
|
779
777
|
end
|
|
780
778
|
|
|
781
|
-
|
|
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)
|
|
782
784
|
graph = RDF::Graph.new
|
|
783
785
|
graph.name = RDF::URI(self.class.graph_name)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
+
serialize_entity(graph, entity, deep, Set.new, [])
|
|
786
787
|
graph
|
|
787
788
|
end
|
|
788
789
|
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
791
798
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
uuid = klass.instance_variable_get("@id") || SecureRandom.uuid
|
|
796
|
-
id = RDF::URI("#{graph_name}#{klass_name.tableize}/#{uuid}")
|
|
799
|
+
metadata = entity.class.metadata
|
|
800
|
+
hierarchy.push("#{entity.class.name}(#{uuid})")
|
|
801
|
+
graph << [id, RDF::RDFV.type, metadata[:target_class]]
|
|
797
802
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
# Use cached entity if available, otherwise query the store (unless skip_store_fetch)
|
|
801
|
-
original_klass = known_entities[uuid]
|
|
802
|
-
if original_klass.nil? && !skip_store_fetch
|
|
803
|
-
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
803
|
+
metadata[:attributes].each do |attribute, attr_metadata|
|
|
804
|
+
serialize_attribute(graph, id, entity, attribute, attr_metadata, deep, visited, hierarchy)
|
|
804
805
|
end
|
|
805
806
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
resolve_all = false unless skip_store_fetch
|
|
813
|
-
klass.instance_variables.map { |m| m.to_s.gsub(/^@/, '') }
|
|
814
|
-
.select { |s| !["model_name", "model_plural_name"].include?(s) }.each do |attribute|
|
|
815
|
-
data = klass.instance_variable_get("@#{attribute}")
|
|
816
|
-
original_data = original_klass.instance_variable_get("@#{attribute}")
|
|
817
|
-
original_klass.instance_variable_set("@#{attribute}", data) unless original_data.eql?(data)
|
|
818
|
-
end
|
|
819
|
-
end
|
|
807
|
+
hierarchy.pop
|
|
808
|
+
id
|
|
809
|
+
rescue StandardError => e
|
|
810
|
+
Solis::LOGGER.error(e.message)
|
|
811
|
+
raise e
|
|
812
|
+
end
|
|
820
813
|
|
|
821
|
-
|
|
822
|
-
|
|
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}")
|
|
823
819
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
Solis::
|
|
828
|
-
|
|
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]}"
|
|
829
825
|
end
|
|
830
826
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
end
|
|
827
|
+
# skip if nil or an empty container
|
|
828
|
+
return if data.nil? || ([Hash, Array, String].include?(data.class) && data.empty?)
|
|
834
829
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
834
|
+
|
|
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 }
|
|
845
842
|
end
|
|
846
843
|
end
|
|
847
844
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if
|
|
852
|
-
if
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if original_klass.nil? && !skip_store_fetch
|
|
857
|
-
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
858
|
-
known_entities[uuid] = original_klass if original_klass
|
|
859
|
-
end
|
|
860
|
-
unless original_klass.nil?
|
|
861
|
-
klass = original_klass
|
|
862
|
-
data = klass.instance_variable_get("@#{attribute}")
|
|
863
|
-
end
|
|
845
|
+
data = [data] unless data.is_a?(Array)
|
|
846
|
+
|
|
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}"
|
|
864
853
|
end
|
|
865
|
-
# if data is still nil
|
|
866
|
-
raise Solis::Error::InvalidAttributeError, "#{hierarchy.join('.')}~#{klass.class.name}.#{attribute} min=#{metadata[:mincount]} and max=#{metadata[:maxcount]}" if data.nil?
|
|
867
854
|
end
|
|
868
855
|
|
|
869
|
-
|
|
870
|
-
raise Solis::Error::InvalidAttributeError, "#{hierarchy.join('.')}~#{klass.class.name}.#{attribute} min=#{metadata[:mincount]} and max=#{metadata[:maxcount]}" if data.nil?
|
|
871
|
-
end
|
|
856
|
+
d = d.first if d.is_a?(Array) && d.length == 1
|
|
872
857
|
|
|
873
|
-
|
|
874
|
-
next if data.nil? || ([Hash, Array, String].include?(data.class) && data&.empty?)
|
|
858
|
+
d = coerce_literal(d, metadata, attribute, hierarchy)
|
|
875
859
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
when 'http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'
|
|
880
|
-
data = data.to_json
|
|
881
|
-
end
|
|
860
|
+
Array(d).each { |v| graph << [id, RDF::URI("#{metadata[:path]}"), v] }
|
|
861
|
+
end
|
|
862
|
+
end
|
|
882
863
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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']) }
|
|
870
|
+
else
|
|
871
|
+
RDF::Literal.new(d['@value'], language: d['@language'])
|
|
890
872
|
end
|
|
873
|
+
else
|
|
874
|
+
RDF::Literal.new(d, language: @language)
|
|
891
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
|
|
892
899
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
internal_resolve = false
|
|
903
|
-
d = build_ttl_objekt(graph, d, hierarchy, internal_resolve, known_entities)
|
|
904
|
-
else
|
|
905
|
-
d = "#{klass.class.graph_name}#{d.class.name.tableize}/#{d.id}"
|
|
906
|
-
end
|
|
907
|
-
elsif solis_model?(d) && self.class.graph.shape?(d.class.name)
|
|
908
|
-
d = "#{klass.class.graph_name}#{d.class.name.tableize}/#{d.id}"
|
|
909
|
-
end
|
|
910
|
-
|
|
911
|
-
if d.is_a?(Array) && d.length == 1
|
|
912
|
-
d = d.first
|
|
913
|
-
end
|
|
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
|
|
914
909
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
d_r = []
|
|
919
|
-
d['@value'].each do |v|
|
|
920
|
-
d_r << RDF::Literal.new(v, language: d['@language'])
|
|
921
|
-
end
|
|
922
|
-
d_r
|
|
923
|
-
else
|
|
924
|
-
RDF::Literal.new(d['@value'], language: d['@language'])
|
|
925
|
-
end
|
|
926
|
-
else
|
|
927
|
-
RDF::Literal.new(d, language: @language)
|
|
928
|
-
end
|
|
929
|
-
elsif metadata[:datatype_rdf].eql?('http://www.w3.org/2001/XMLSchema#anyURI')
|
|
930
|
-
RDF::Literal.new(d.to_s, datatype: RDF::XSD.anyURI)
|
|
931
|
-
elsif metadata[:node].is_a?(RDF::URI)
|
|
932
|
-
RDF::URI(d)
|
|
933
|
-
elsif metadata[:datatype_rdf] =~ /datatypes\/edtf/ || metadata[:datatype_rdf] =~ /edtf$/i
|
|
934
|
-
# Handle EDTF dates
|
|
935
|
-
begin
|
|
936
|
-
RDF::Literal::EDTF.new(d)
|
|
937
|
-
rescue StandardError => e
|
|
938
|
-
raise Solis::Error::InvalidDatatypeError, "#{hierarchy.join('.')}.#{attribute}: #{e.message}"
|
|
939
|
-
end
|
|
940
|
-
elsif metadata[:datatype_rdf].eql?('http://www.w3.org/2006/time#DateTimeInterval')
|
|
941
|
-
begin
|
|
942
|
-
datatype = metadata[:datatype_rdf]
|
|
943
|
-
RDF::Literal.new(ISO8601::TimeInterval.parse(d).to_s, datatype: datatype)
|
|
944
|
-
rescue StandardError => e
|
|
945
|
-
raise Solis::Error::InvalidDatatypeError, "#{hierarchy.join('.')}.#{attribute}: #{e.message}"
|
|
946
|
-
end
|
|
947
|
-
else
|
|
948
|
-
datatype = RDF::Vocabulary.find_term(metadata[:datatype_rdf])
|
|
949
|
-
datatype = metadata[:node] if datatype.nil?
|
|
950
|
-
datatype = metadata[:datatype_rdf] if datatype.nil?
|
|
951
|
-
RDF::Literal.new(d, datatype: datatype)
|
|
952
|
-
end
|
|
910
|
+
shapes = SHACL.get_shapes(self.class.graph.instance_variable_get(:@graph))
|
|
911
|
+
report = shapes.execute(graph)
|
|
912
|
+
return if report.conform?
|
|
953
913
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
914
|
+
messages = Array(report.results).map do |r|
|
|
915
|
+
r.respond_to?(:message) ? Array(r.message).join(', ') : r.to_s
|
|
916
|
+
end
|
|
957
917
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
else
|
|
963
|
-
graph << [id, RDF::URI("#{metadata[:path]}"), d]
|
|
964
|
-
end
|
|
965
|
-
end
|
|
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('; ')}"
|
|
966
922
|
end
|
|
967
|
-
rescue StandardError => e
|
|
968
|
-
Solis::LOGGER.error(e.message)
|
|
969
|
-
raise e
|
|
970
923
|
end
|
|
971
924
|
|
|
972
925
|
def properties_to_hash(model)
|
data/lib/solis/version.rb
CHANGED