solis 0.123.0 → 0.124.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 +124 -8
- 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: 6d5d2032cb4615c4ab9d41f6ff2b89f6000fcc5bfc29ff55972305166b2f7904
|
|
4
|
+
data.tar.gz: 572b1eacf27a9e63579e4403c9f00c003c8dde292b28492960669aa933b8755f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d501867d8e19209212b168abf92b0d91b3b95b183c40732943d8c373afa0d72acbe1c0d6fd23ba6e0ee5c7e8c8aa6de3cbc65ca01975ae1d26f95c6e0afa6bb3
|
|
7
|
+
data.tar.gz: 8642feba84874ac698cbc8d9b3cfacd775f8fc90793fec6092834bd79108ae21862c8a5dbe803bf3f17032983199838619ab10a301953934af1dfd15ce31d171
|
data/lib/solis/model.rb
CHANGED
|
@@ -26,10 +26,18 @@ module Solis
|
|
|
26
26
|
inner_class = self.class.metadata[:attributes][attribute.to_s][:datatype].to_s
|
|
27
27
|
inner_model = self.class.graph.shape_as_model(inner_class)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
# Resolve a polymorphic reference to its concrete subclass, preferring an
|
|
30
|
+
# explicit `type` key, then a full URI whose path segment names the class.
|
|
31
|
+
# Keys may be strings (JSON) or symbols (internal callers).
|
|
32
|
+
explicit_type = (value['type'] || value[:type] || value['@type'] || value[:'@type']).to_s
|
|
33
|
+
value = value.reject { |k, _| %w[type @type].include?(k.to_s) }
|
|
34
|
+
id_value = (value['id'] || value[:id]).to_s
|
|
35
|
+
if !explicit_type.empty? && descendant_shape_names(inner_model.name).include?(explicit_type)
|
|
36
|
+
inner_model = self.class.graph.shape_as_model(explicit_type)
|
|
37
|
+
elsif !id_value.empty? && id_value.match?(self.class.graph_name)
|
|
38
|
+
concrete = id_value.gsub(self.class.graph_name, '').split('/').first.classify.to_s
|
|
39
|
+
if descendant_shape_names(inner_model.name).include?(concrete)
|
|
40
|
+
inner_model = self.class.graph.shape_as_model(concrete)
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
|
|
@@ -158,19 +166,28 @@ values ?s {<#{self.graph_id}>}
|
|
|
158
166
|
else
|
|
159
167
|
readonly_list = (Solis::Options.instance.get[:embedded_readonly] || []).map(&:to_s)
|
|
160
168
|
|
|
169
|
+
# Re-type polymorphic base-class id-only references (e.g. an `agent` stub
|
|
170
|
+
# that is really an `Organisatie`) to their concrete subclass, so URIs and
|
|
171
|
+
# existence checks target the subclass's storage path.
|
|
172
|
+
resolve_polymorphic_references!(self, sparql)
|
|
173
|
+
|
|
161
174
|
# Enumerate the whole in-memory tree: self plus every embedded descendant.
|
|
162
175
|
all_entities = collect_known_entities(self).values
|
|
163
176
|
existing_ids = self.class.batch_exists?(sparql, all_entities)
|
|
164
177
|
|
|
165
|
-
# Classify each entity: new (insert), existing embedded (update),
|
|
166
|
-
# readonly only protects EMBEDDED entities; the entity
|
|
167
|
-
# always created even when its class is a code table.
|
|
178
|
+
# Classify each entity: new (insert), existing embedded (update), readonly,
|
|
179
|
+
# or a pure reference. readonly only protects EMBEDDED entities; the entity
|
|
180
|
+
# being saved (self) is always created even when its class is a code table.
|
|
168
181
|
new_entities = []
|
|
169
182
|
existing_embedded = []
|
|
170
183
|
all_entities.each do |entity|
|
|
171
184
|
entity_exists = existing_ids.include?(entity.graph_id)
|
|
172
185
|
if !entity.equal?(self) && readonly_entity?(entity, readonly_list)
|
|
173
186
|
Solis::LOGGER.warn("#{entity.class.name} (id: #{entity.id}) is readonly but does not exist in database. Skipping.") unless entity_exists
|
|
187
|
+
elsif !entity.equal?(self) && shallow_stub?(entity) && top_level_entity?(entity)
|
|
188
|
+
# An id-only reference to an independently-addressable entity: link only.
|
|
189
|
+
# It is emitted as a URI by serialize_entity; never create or rewrite it.
|
|
190
|
+
raise Solis::Error::NotFoundError, "#{entity.class.name} (id: #{entity.id}) is referenced but does not exist" unless entity_exists
|
|
174
191
|
elsif entity_exists
|
|
175
192
|
existing_embedded << entity
|
|
176
193
|
else
|
|
@@ -249,11 +266,17 @@ values ?s {<#{self.graph_id}>}
|
|
|
249
266
|
|
|
250
267
|
# First pass: collect all embedded entities for batched existence check
|
|
251
268
|
embedded_by_key = {}
|
|
269
|
+
poly_cache = {}
|
|
252
270
|
attributes.each_pair do |key, value|
|
|
253
271
|
unless original_klass.class.metadata[:attributes][key][:node].nil?
|
|
254
272
|
value = [value] unless value.is_a?(Array)
|
|
255
273
|
embedded_by_key[key] = value.map do |sub_value|
|
|
256
|
-
self.class.graph.shape_as_model(original_klass.class.metadata[:attributes][key][:datatype].to_s).new(sub_value)
|
|
274
|
+
model = self.class.graph.shape_as_model(original_klass.class.metadata[:attributes][key][:datatype].to_s).new(sub_value)
|
|
275
|
+
# Re-type a polymorphic base-class id-only reference to its concrete
|
|
276
|
+
# subclass so existence checks and emitted URIs target the right path.
|
|
277
|
+
concrete = resolve_polymorphic_class(model, sparql, poly_cache)
|
|
278
|
+
model = concrete.new({ id: model.id }) if concrete && concrete != model.class
|
|
279
|
+
model
|
|
257
280
|
end
|
|
258
281
|
end
|
|
259
282
|
end
|
|
@@ -657,6 +680,99 @@ values ?s {<#{self.graph_id}>}
|
|
|
657
680
|
end
|
|
658
681
|
end
|
|
659
682
|
|
|
683
|
+
# Map of shape_name => parent_shape_name, derived from each shape's sh:node
|
|
684
|
+
# (target_node) pointing at "<graph_name><Parent>Shape". Pure metadata.
|
|
685
|
+
def polymorphic_parent_map
|
|
686
|
+
graph_name = self.class.graph_name
|
|
687
|
+
map = {}
|
|
688
|
+
self.class.shapes.each do |name, meta|
|
|
689
|
+
tn = meta[:target_node]
|
|
690
|
+
next if tn.nil?
|
|
691
|
+
if tn.to_s =~ /^#{Regexp.escape(graph_name)}(.+)Shape$/
|
|
692
|
+
parent = $1
|
|
693
|
+
map[name] = parent unless parent == name
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
map
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# Names of shapes that inherit (directly or transitively, via sh:node) from
|
|
700
|
+
# base_shape_name — i.e. the concrete subclasses of a polymorphic base.
|
|
701
|
+
def descendant_shape_names(base_shape_name)
|
|
702
|
+
parent_of = polymorphic_parent_map
|
|
703
|
+
parent_of.keys.select do |name|
|
|
704
|
+
ancestor = parent_of[name]
|
|
705
|
+
found = false
|
|
706
|
+
while ancestor
|
|
707
|
+
if ancestor == base_shape_name
|
|
708
|
+
found = true
|
|
709
|
+
break
|
|
710
|
+
end
|
|
711
|
+
ancestor = parent_of[ancestor]
|
|
712
|
+
end
|
|
713
|
+
found
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# For a polymorphic id-only stub declared as a base class, ask the store which
|
|
718
|
+
# concrete subclass URI actually holds this id, and return that concrete model
|
|
719
|
+
# class. Returns nil when the declared class has no subclasses (not polymorphic)
|
|
720
|
+
# or no matching subject exists. Write-path only — issues a SPARQL query.
|
|
721
|
+
def resolve_polymorphic_class(stub, sparql, cache = {})
|
|
722
|
+
return nil unless solis_model?(stub) && shallow_stub?(stub) && stub.id
|
|
723
|
+
|
|
724
|
+
base_name = stub.class.name
|
|
725
|
+
# Key by declared class + id: the same id may be referenced through different
|
|
726
|
+
# declared relation types, so a nil for one base must not shadow another.
|
|
727
|
+
cache_key = "#{base_name}|#{stub.id}"
|
|
728
|
+
return cache[cache_key] if cache.key?(cache_key)
|
|
729
|
+
|
|
730
|
+
subclass_names = descendant_shape_names(base_name)
|
|
731
|
+
return cache[cache_key] = nil if subclass_names.empty?
|
|
732
|
+
|
|
733
|
+
graph_name = stub.class.graph_name
|
|
734
|
+
candidates = ([base_name] + subclass_names).uniq.map { |name| "#{graph_name}#{name.tableize}/#{stub.id}" }
|
|
735
|
+
values = candidates.map { |u| "<#{u}>" }.join(' ')
|
|
736
|
+
result = sparql.query("SELECT ?s WHERE { VALUES ?s { #{values} } . ?s ?p ?o } LIMIT 1")
|
|
737
|
+
uri = result.first && result.first[:s] && result.first[:s].to_s
|
|
738
|
+
|
|
739
|
+
klass = nil
|
|
740
|
+
unless uri.nil?
|
|
741
|
+
concrete_name = uri.sub(graph_name, '').split('/').first.classify
|
|
742
|
+
klass = self.class.graph.shape_as_model(concrete_name) if self.class.graph.shape?(concrete_name)
|
|
743
|
+
end
|
|
744
|
+
cache[cache_key] = klass
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Walk the relation tree and re-type every polymorphic base-class id-only stub
|
|
748
|
+
# to its concrete subclass (resolved from the store), so existence checks and
|
|
749
|
+
# emitted reference URIs use the subclass's storage path. Write-path only.
|
|
750
|
+
def resolve_polymorphic_references!(entity, sparql, cache = {}, visited = Set.new)
|
|
751
|
+
return entity if visited.include?(entity.object_id)
|
|
752
|
+
visited << entity.object_id
|
|
753
|
+
entity.class.metadata[:attributes].each do |attr, meta|
|
|
754
|
+
next if meta[:node_kind].nil?
|
|
755
|
+
val = entity.instance_variable_get("@#{attr}")
|
|
756
|
+
next if val.nil?
|
|
757
|
+
if val.is_a?(Array)
|
|
758
|
+
entity.instance_variable_set("@#{attr}", val.map { |v| retype_polymorphic_stub(v, sparql, cache, visited) })
|
|
759
|
+
else
|
|
760
|
+
entity.instance_variable_set("@#{attr}", retype_polymorphic_stub(val, sparql, cache, visited))
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
entity
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Resolve a single relation value: re-type a polymorphic base stub to its
|
|
767
|
+
# concrete subclass, then recurse. Non-model values pass through unchanged.
|
|
768
|
+
def retype_polymorphic_stub(v, sparql, cache, visited)
|
|
769
|
+
return v unless solis_model?(v)
|
|
770
|
+
concrete = resolve_polymorphic_class(v, sparql, cache)
|
|
771
|
+
v = concrete.new({ id: v.id }) if concrete && concrete != v.class
|
|
772
|
+
resolve_polymorphic_references!(v, sparql, cache, visited)
|
|
773
|
+
v
|
|
774
|
+
end
|
|
775
|
+
|
|
660
776
|
# Load the full stored entity for this model's class by id. Returns nil when absent.
|
|
661
777
|
def load_original(id)
|
|
662
778
|
self.query.filter({ language: self.class.language, filters: { id: [id] } })
|
data/lib/solis/version.rb
CHANGED