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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d9aacb8e97e1c584368f1c792a26ca48b8f885e64faa6c9d39f09f59a2b1260
4
- data.tar.gz: 1caf4c17d138b900041f758f493a1fccdb302eb27b86651911dd22c529545219
3
+ metadata.gz: 6d5d2032cb4615c4ab9d41f6ff2b89f6000fcc5bfc29ff55972305166b2f7904
4
+ data.tar.gz: 572b1eacf27a9e63579e4403c9f00c003c8dde292b28492960669aa933b8755f
5
5
  SHA512:
6
- metadata.gz: 49e3387254454c6bbea21ab51f3bbe2440022dd4f23eaffb77351aeb5e6cf967e79d2f9e6e25fc971c3614aa128afe82c6b63c6116e3f195e5bc535657e7dd8a
7
- data.tar.gz: 9e8478ee6bfe98cf1d3a7dae252383120ff93bb1684a0e8f2bb1aae1296ad6e0472b8b7a34042be1b8b9a90a02d99c8c413c487e1ed298622b22c2c83100ee8e
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
- if value.key?('id') && value['id'].match?(self.class.graph_name)
30
- inner_class = value['id'].gsub(self.class.graph_name, '').split('/').first.classify.to_s
31
- if inner_model.descendants.map(&:to_s).include?(inner_class)
32
- inner_model = self.class.graph.shape_as_model(inner_class)
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), 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.
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
@@ -1,3 +1,3 @@
1
1
  module Solis
2
- VERSION = "0.123.0"
2
+ VERSION = "0.124.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.123.0
4
+ version: 0.124.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik