lutaml-model 0.8.10 → 0.8.12

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +1 -0
  3. data/.github/workflows/opal.yml +31 -0
  4. data/.rspec-opal +5 -0
  5. data/.rubocop_todo.yml +68 -7
  6. data/README.adoc +53 -1
  7. data/docs/_guides/index.adoc +4 -0
  8. data/docs/_guides/jsonld-serialization.adoc +3 -1
  9. data/docs/_guides/opal.adoc +221 -0
  10. data/docs/_guides/rdf-serialization.adoc +94 -8
  11. data/docs/_guides/turtle-serialization.adoc +17 -4
  12. data/docs/_guides/xml_mappings/07_best_practices.adoc +2 -1
  13. data/docs/_pages/configuration.adoc +9 -4
  14. data/docs/_pages/index.adoc +1 -0
  15. data/docs/_pages/serialization_adapters.adoc +3 -2
  16. data/docs/index.adoc +1 -0
  17. data/lib/lutaml/hash_format/adapter/mapping.rb +2 -4
  18. data/lib/lutaml/json/adapter/mapping.rb +2 -4
  19. data/lib/lutaml/jsonl/adapter/mapping.rb +2 -4
  20. data/lib/lutaml/jsonld/transform.rb +70 -24
  21. data/lib/lutaml/key_value/adapter/hash/mapping.rb +2 -4
  22. data/lib/lutaml/key_value/adapter/json/mapping.rb +2 -4
  23. data/lib/lutaml/key_value/adapter/jsonl/mapping.rb +2 -4
  24. data/lib/lutaml/key_value/adapter/toml/mapping.rb +2 -4
  25. data/lib/lutaml/key_value/adapter/yaml/mapping.rb +2 -4
  26. data/lib/lutaml/key_value/adapter/yamls/mapping.rb +2 -4
  27. data/lib/lutaml/key_value/mapping.rb +4 -4
  28. data/lib/lutaml/model/adapter_resolver.rb +5 -8
  29. data/lib/lutaml/model/mapping/mapping.rb +12 -0
  30. data/lib/lutaml/model/store.rb +51 -4
  31. data/lib/lutaml/model/version.rb +1 -1
  32. data/lib/lutaml/rdf/mapping.rb +19 -13
  33. data/lib/lutaml/rdf/mapping_rule.rb +19 -2
  34. data/lib/lutaml/rdf/member_rule.rb +19 -2
  35. data/lib/lutaml/rdf/transform.rb +20 -11
  36. data/lib/lutaml/toml/adapter/mapping.rb +2 -4
  37. data/lib/lutaml/turtle/transform.rb +125 -53
  38. data/lib/lutaml/xml/schema/xsd.rb +5 -4
  39. data/lib/lutaml/xml/schema.rb +8 -5
  40. data/lib/lutaml/xml/xml_orderable.rb +17 -0
  41. data/lib/lutaml/xml.rb +8 -11
  42. data/lib/lutaml/yaml/adapter/mapping.rb +2 -4
  43. data/lib/lutaml/yamls/adapter/mapping.rb +7 -3
  44. data/lutaml-model.gemspec +1 -1
  45. data/spec/lutaml/jsonld/transform_spec.rb +239 -0
  46. data/spec/lutaml/model/opal_smoke_spec.rb +117 -0
  47. data/spec/lutaml/model/store_spec.rb +156 -2
  48. data/spec/lutaml/rdf/mapping_rule_spec.rb +97 -0
  49. data/spec/lutaml/rdf/mapping_spec.rb +74 -4
  50. data/spec/lutaml/rdf/member_rule_spec.rb +41 -0
  51. data/spec/lutaml/rdf/rdf_transform_spec.rb +95 -29
  52. data/spec/lutaml/turtle/mapping_spec.rb +2 -2
  53. data/spec/lutaml/turtle/transform_spec.rb +315 -0
  54. data/spec/lutaml/xml/opal_xml_spec.rb +145 -0
  55. data/spec/lutaml/xml/xml_spec.rb +64 -13
  56. data/spec/support/opal.rb +6 -0
  57. metadata +12 -4
@@ -73,9 +73,18 @@ Member models (serialized via a container's `members` declaration) can omit the
73
73
 
74
74
  === type
75
75
 
76
- Sets the RDF type (`rdf:type`) triple. Compact IRIs (e.g., `"skos:Concept"`)
77
- are resolved to full URIs via the declared namespaces. Full URIs (e.g.,
78
- `"http://example.org/MyType"`) are used as-is.
76
+ Sets the RDF type (`rdf:type`) triple. Accepts a single compact IRI or an array:
77
+
78
+ [source,ruby]
79
+ ----
80
+ type "skos:Concept"
81
+ # or multiple types:
82
+ type ["skos:Concept", "dcterms:Agent"]
83
+ ----
84
+
85
+ Compact IRIs are resolved to full URIs via the declared namespaces. Full URIs
86
+ (e.g., `"http://example.org/MyType"`) are used as-is. Each type produces a
87
+ separate `a` triple in the output.
79
88
 
80
89
  === predicate
81
90
 
@@ -85,7 +94,11 @@ Each `predicate` creates a `Lutaml::Rdf::MappingRule` value object:
85
94
  * `namespace:` — the `Lutaml::Rdf::Namespace` subclass (required, validated)
86
95
  * `to:` — the model attribute to read/write (required)
87
96
  * `lang_tagged:` (default: `false`) — if true, appends `@lang` suffix from the
88
- value's `language_code` or `language` method
97
+ value's `language_code` or `language` method. Mutually exclusive with
98
+ `uri_reference`.
99
+ * `uri_reference:` (default: `false`) — if true, serializes values as URI
100
+ objects rather than string literals. Compact IRIs are resolved via the
101
+ declared namespaces. Mutually exclusive with `lang_tagged`.
89
102
 
90
103
  Validation errors:
91
104
 
@@ -723,7 +723,8 @@ end
723
723
 
724
724
  * **Ox**: Fastest, use for performance-critical applications
725
725
  * **Nokogiri**: Most compatible, good balance
726
- * **Oga**: Pure Ruby, works with Opal, use when no native extensions allowed
726
+ * **Oga**: Pure Ruby, use when no native extensions allowed
727
+ * **REXML**: Pure Ruby (bundled with Ruby), Opal-compatible
727
728
 
728
729
  **Configure once:**
729
730
 
@@ -74,7 +74,7 @@ When an adapter is needed for a format, `AdapterResolver` follows this chain:
74
74
  | Pure Ruby. No compilation needed. Opal-compatible.
75
75
 
76
76
  | `:rexml`
77
- | Pure Ruby. Bundled with Ruby (default gem).
77
+ | Pure Ruby. Bundled with Ruby (default gem). Opal-compatible.
78
78
  |===
79
79
 
80
80
  === JSON adapters
@@ -261,8 +261,8 @@ end
261
261
 
262
262
  * **Nokogiri**: Most projects (default, best compatibility)
263
263
  * **Ox**: Performance-critical applications
264
- * **Oga**: Pure Ruby environments, Opal/JavaScript compilation
265
- * **REXML**: No extra gems, pure Ruby (bundled with Ruby)
264
+ * **Oga**: Pure Ruby environments
265
+ * **REXML**: No extra gems, pure Ruby (bundled with Ruby). Also used under Opal.
266
266
 
267
267
  === Choose JSON adapter based on
268
268
 
@@ -279,12 +279,14 @@ end
279
279
 
280
280
  Lutaml::Model supports running under https://opalrb.com[Opal] (Ruby compiled to JavaScript) with some limitations. The library detects the runtime automatically via `Lutaml::Model::RuntimeCompatibility` and adapts its behavior accordingly.
281
281
 
282
+ The XML parsing layer is provided by the https://github.com/lutaml/moxml[moxml] gem, which has been updated to support Opal. Under Opal, moxml uses the REXML adapter because Opal reimplements `strscan` and `stringio` in its stdlib, enabling REXML (pure Ruby) to compile cleanly to JavaScript.
283
+
282
284
  === Adapter defaults on Opal
283
285
 
284
286
  [cols="1,2",options="header"]
285
287
  |===
286
288
  | Format | Behavior
287
- | XML | Only `:oga` is available (auto-selected). Nokogiri, Ox, and REXML require native extensions.
289
+ | XML | Only `:rexml` is available (auto-selected). Opal reimplements strscan/stringio in its stdlib, enabling REXML to compile to JavaScript.
288
290
  | JSON | Only `:standard` is available. Oj and MultiJson require native extensions.
289
291
  | YAML | `:standard` (Psych ships with Opal's stdlib).
290
292
  | TOML | **Not available.** Both tomlib and toml-rb depend on native extensions.
@@ -299,6 +301,9 @@ The following features raise `NotImplementedError` when called under Opal:
299
301
  * `Lutaml::Model::Schema.to_relaxng` -- RELAX NG generation requires Nokogiri
300
302
  * `Lutaml::Model::Schema.from_xml` -- XML schema compilation is not supported
301
303
  * `Lutaml::Xml::Schema::Xsd::Base#to_formatted_xml` -- XSD formatted output requires the Canon gem
304
+ * XPath queries -- REXML XPath requires features not yet supported by Opal's stdlib
305
+
306
+ See the link:../guides/opal[Opal Usage Guide] for complete setup instructions and limitations.
302
307
 
303
308
  == See also
304
309
 
@@ -22,6 +22,7 @@ Fundamental concepts, configuration, and essential features of Lutaml::Model.
22
22
  * link:breaking-changes[Breaking Changes] - Version compatibility
23
23
  * link:comparison-with-shale[Comparison with Shale] - Migration guide
24
24
  * link:xml-conformance[XML Standards Conformance] - W3C spec compliance status
25
+ * link:../guides/opal[Opal Usage] - Run in the browser via Opal
25
26
  * link:troubleshooting[Troubleshooting] - Common issues and solutions
26
27
 
27
28
  == Getting started
@@ -192,8 +192,7 @@ Requires the `nokogiri` gem.
192
192
  Oga::
193
193
  (optional)
194
194
  Pure Ruby XML parser.
195
- Does not require native extensions and is suitable for
196
- https://opalrb.com[Opal] (Ruby on JavaScript).
195
+ Does not require native extensions.
197
196
  Requires the `oga` gem.
198
197
 
199
198
  Ox::
@@ -206,6 +205,8 @@ REXML::
206
205
  (optional)
207
206
  Pure Ruby XML parser, bundled as a default gem with Ruby.
208
207
  Moved from standard library to a default gem in Ruby 3.0.
208
+ Opal-compatible: Opal reimplements `strscan` and `stringio` in its stdlib,
209
+ enabling REXML to compile cleanly to JavaScript.
209
210
  Requires the `rexml` gem (bundled with Ruby by default).
210
211
 
211
212
 
data/docs/index.adoc CHANGED
@@ -19,6 +19,7 @@ Lutaml::Model is a Ruby library for creating information models with attributes
19
19
  * **Multi-format serialization** - XML, JSON, YAML, TOML, JSON-LD, Turtle, Hash, YAML Stream
20
20
  * **XML namespace support** - Full W3C namespace implementation
21
21
  * **Linked Data support** - RDF namespaces, JSON-LD, and Turtle serialization
22
+ * **Opal support** - Run in the browser via Opal (Ruby to JavaScript) with REXML adapter
22
23
  * **Validation** - Built-in validation with custom rules
23
24
  * **Schema generation** - Generate XSD, JSON Schema, YAML Schema
24
25
  * **Polymorphism** - Handle multiple types elegantly
@@ -8,10 +8,8 @@ module Lutaml
8
8
  super(:hash)
9
9
  end
10
10
 
11
- def deep_dup
12
- self.class.new.tap do |new_mapping|
13
- new_mapping.mappings = duplicate_mappings
14
- end
11
+ def dup_instance
12
+ self.class.new
15
13
  end
16
14
  end
17
15
  end
@@ -8,10 +8,8 @@ module Lutaml
8
8
  super(:json)
9
9
  end
10
10
 
11
- def deep_dup
12
- self.class.new.tap do |new_mapping|
13
- new_mapping.mappings = duplicate_mappings
14
- end
11
+ def dup_instance
12
+ self.class.new
15
13
  end
16
14
  end
17
15
  end
@@ -8,10 +8,8 @@ module Lutaml
8
8
  super(:jsonl)
9
9
  end
10
10
 
11
- def deep_dup
12
- self.class.new.tap do |new_mapping|
13
- new_mapping.mappings = duplicate_mappings
14
- end
11
+ def dup_instance
12
+ self.class.new
15
13
  end
16
14
  end
17
15
  end
@@ -35,7 +35,7 @@ module Lutaml
35
35
  value = hash[rule.predicate_name]
36
36
  next if value.nil?
37
37
 
38
- attrs[rule.to] = if rule.lang_tagged && value.is_a?(Hash)
38
+ attrs[rule.to] = if rule.kind == :lang_tagged && value.is_a?(Hash)
39
39
  flatten_language_map(value)
40
40
  else
41
41
  value
@@ -61,9 +61,8 @@ module Lutaml
61
61
  end
62
62
 
63
63
  mapping.rdf_members.each do |member_rule|
64
- collection = Array(instance.public_send(member_rule.attr_name))
65
- collection.each do |member|
66
- member_mapping = member.class.mappings[:jsonld]
64
+ each_member(instance, member_rule) do |member|
65
+ member_mapping = member_mapping_for(member, :jsonld)
67
66
  next unless member_mapping
68
67
 
69
68
  resource = build_resource_data(member_mapping, member)
@@ -78,13 +77,12 @@ module Lutaml
78
77
  context_hash = build_context_from_mapping(mapping).to_hash
79
78
 
80
79
  mapping.rdf_members.each do |member_rule|
81
- collection = Array(instance.public_send(member_rule.attr_name))
82
- next if collection.empty?
83
-
84
- member_mapping = collection.first.class.mappings[:jsonld]
85
- next unless member_mapping
80
+ each_member(instance, member_rule) do |member|
81
+ member_mapping = member_mapping_for(member, :jsonld)
82
+ next unless member_mapping
86
83
 
87
- context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
84
+ context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
85
+ end
88
86
  end
89
87
 
90
88
  context_hash
@@ -94,17 +92,30 @@ module Lutaml
94
92
  context = Context.new
95
93
  mapping.namespace_set.each { |ns| context.prefix(ns) }
96
94
  mapping.rdf_predicates.each do |pred|
97
- if pred.lang_tagged
98
- context.term(pred.predicate_name,
99
- id: pred.uri,
100
- container: :language)
101
- else
102
- context.term(pred.predicate_name, id: pred.uri)
103
- end
95
+ options = { id: pred.uri }
96
+ term_options = context_term_options(pred)
97
+ context.term(pred.predicate_name, **options, **term_options)
104
98
  end
99
+
100
+ mapping.rdf_members.each do |member_rule|
101
+ next unless member_rule.linked?
102
+
103
+ context.term(member_rule.predicate_name.to_s,
104
+ id: member_rule.linked_predicate_uri,
105
+ type: "@id")
106
+ end
107
+
105
108
  context
106
109
  end
107
110
 
111
+ def context_term_options(rule)
112
+ case rule.kind
113
+ when :uri_reference then { type: "@id" }
114
+ when :lang_tagged then { container: :language }
115
+ else {}
116
+ end
117
+ end
118
+
108
119
  def build_resource_object(mapping, instance)
109
120
  context = build_context_from_mapping(mapping).to_hash
110
121
  data = build_resource_data(mapping, instance)
@@ -114,8 +125,12 @@ module Lutaml
114
125
  def build_resource_data(mapping, instance)
115
126
  result = {}
116
127
 
117
- if mapping.rdf_type
118
- result["@type"] = resolve_type_compact(mapping)
128
+ if mapping.rdf_type.any?
129
+ result["@type"] = if mapping.rdf_type.length == 1
130
+ mapping.rdf_type.first
131
+ else
132
+ mapping.rdf_type
133
+ end
119
134
  end
120
135
 
121
136
  if mapping.rdf_subject
@@ -127,16 +142,47 @@ module Lutaml
127
142
  next if value.nil?
128
143
  next if value.is_a?(String) && value.empty?
129
144
 
130
- result[rule.predicate_name] = if rule.lang_tagged
131
- build_language_map(value)
132
- else
133
- serialize_rdf_value(value)
134
- end
145
+ result[rule.predicate_name] = serialize_value(value, rule)
146
+ end
147
+
148
+ mapping.rdf_members.each do |member_rule|
149
+ next unless member_rule.linked?
150
+
151
+ member_refs = collect_member_references(instance, member_rule)
152
+ next if member_refs.empty?
153
+
154
+ result[member_rule.predicate_name.to_s] = member_refs
135
155
  end
136
156
 
137
157
  result
138
158
  end
139
159
 
160
+ def collect_member_references(instance, member_rule)
161
+ refs = []
162
+ each_member(instance, member_rule) do |member|
163
+ member_mapping = member_mapping_for(member, :jsonld)
164
+ next unless member_mapping
165
+
166
+ refs << { "@id" => resolve_subject_uri(member_mapping, member) }
167
+ end
168
+ refs
169
+ end
170
+
171
+ def serialize_value(value, rule)
172
+ case rule.kind
173
+ when :uri_reference then serialize_uri_reference(value)
174
+ when :lang_tagged then build_language_map(value)
175
+ else serialize_rdf_value(value)
176
+ end
177
+ end
178
+
179
+ def serialize_uri_reference(value)
180
+ case value
181
+ when Array then value.map { |v| { "@id" => v.to_s } }
182
+ else { "@id" => value.to_s }
183
+ end
184
+ end
185
+
140
186
  def build_language_map(values)
141
187
  case values
142
188
  when Array
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:hash)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
  end
16
14
  end
@@ -12,10 +12,8 @@ module Lutaml
12
12
  super(:json)
13
13
  end
14
14
 
15
- def deep_dup
16
- self.class.new.tap do |new_mapping|
17
- new_mapping.mappings = duplicate_mappings
18
- end
15
+ def dup_instance
16
+ self.class.new
19
17
  end
20
18
  end
21
19
  end
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:jsonl)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
  end
16
14
  end
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:toml)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
 
16
14
  def validate!(key, to, with, render_nil, render_empty)
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:yaml)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
  end
16
14
  end
@@ -7,10 +7,8 @@ module Lutaml
7
7
  super(:yaml)
8
8
  end
9
9
 
10
- def deep_dup
11
- self.class.new.tap do |new_mapping|
12
- new_mapping.mappings = duplicate_mappings
13
- end
10
+ def dup_instance
11
+ self.class.new
14
12
  end
15
13
  end
16
14
  end
@@ -218,17 +218,17 @@ module Lutaml
218
218
  end
219
219
 
220
220
  # Writers for deep_dup in subclasses
221
- attr_writer :mappings, :register_mappings
221
+ attr_writer :register_mappings
222
222
 
223
223
  def deep_dup
224
- self.class.new(@format).tap do |new_mapping|
224
+ dup_instance.tap do |new_mapping|
225
225
  new_mapping.mappings = duplicate_mappings
226
226
  new_mapping.register_mappings = Lutaml::Model::Utils.deep_dup(@register_mappings)
227
227
  end
228
228
  end
229
229
 
230
- def duplicate_mappings
231
- Lutaml::Model::Utils.deep_dup(@mappings)
230
+ def dup_instance
231
+ self.class.new(@format)
232
232
  end
233
233
 
234
234
  def find_by_to(to)
@@ -370,15 +370,12 @@ module Lutaml
370
370
 
371
371
  # Detect available XML adapter.
372
372
  #
373
- # @return [Symbol, nil] :nokogiri, :ox, :oga, :rexml, or nil
373
+ # Delegates to moxml which is the authority on XML adapter
374
+ # availability and platform constraints (Opal, MRI, etc.).
375
+ #
376
+ # @return [Symbol] adapter type name
374
377
  def detect_xml_adapter
375
- return :oga if RuntimeCompatibility.opal?
376
- return :nokogiri if Utils.safe_load("nokogiri", :Nokogiri)
377
- return :ox if Utils.safe_load("ox", :Ox)
378
- return :oga if Utils.safe_load("oga", :Oga)
379
- return :rexml if Utils.safe_load("rexml", :REXML)
380
-
381
- nil
378
+ Moxml::Config.runtime_default_adapter
382
379
  end
383
380
 
384
381
  # Detect available TOML adapter.
@@ -3,6 +3,8 @@ module Lutaml
3
3
  class Mapping
4
4
  include DeepDupable
5
5
 
6
+ attr_writer :mappings
7
+
6
8
  def initialize
7
9
  @mappings = []
8
10
  @listeners = {} # target => [Listener, ...]
@@ -11,6 +13,16 @@ module Lutaml
11
13
  @mappings_imported = ::Hash.new { |h, k| h[k] = false }
12
14
  end
13
15
 
16
+ def deep_dup
17
+ duped = self.class.new
18
+ duped.mappings = duplicate_mappings
19
+ duped
20
+ end
21
+
22
+ def duplicate_mappings
23
+ Lutaml::Model::Utils.deep_dup(@mappings)
24
+ end
25
+
14
26
  # Get listeners for a specific target (element name/key).
15
27
  #
16
28
  # @param target [String, Symbol] The element name or key
@@ -5,9 +5,15 @@ require "weakref"
5
5
  module Lutaml
6
6
  module Model
7
7
  class Store
8
- # Compact dead WeakRef shells after this many entries per class bucket.
8
+ # Compact dead WeakRef shells once a class bucket grows past this size.
9
9
  COMPACTION_THRESHOLD = 1000
10
10
 
11
+ # Once the threshold is exceeded, only compact every Nth subsequent
12
+ # register call. Amortises the O(N) reject! over N inserts so
13
+ # register stays O(1) per call rather than O(N) per call (O(N^2)
14
+ # cumulatively for the class).
15
+ COMPACTION_INTERVAL = 1000
16
+
11
17
  class << self
12
18
  def instance
13
19
  @instance ||= new
@@ -39,14 +45,17 @@ module Lutaml
39
45
  # Nested index: { model_key => { reference_key => { value => WeakRef(object) } } }
40
46
  # Grouped by model_key so register only iterates this class's own indices.
41
47
  @index = {}
48
+ @inserts_since_compaction = ::Hash.new(0)
49
+ @compaction_count = 0
42
50
  end
43
51
 
44
52
  def register(object)
45
53
  model_key = object.class.to_s
46
54
  refs = @store[model_key]
47
55
  refs << WeakRef.new(object)
56
+ @inserts_since_compaction[model_key] += 1
48
57
 
49
- compact_if_needed(refs)
58
+ compact_if_needed(refs, model_key)
50
59
 
51
60
  update_existing_indices(object, model_key)
52
61
  end
@@ -63,12 +72,16 @@ module Lutaml
63
72
  entry = model_indices[reference_key][reference_value]
64
73
  return nil unless entry
65
74
 
66
- dereference(entry)
75
+ obj = dereference(entry)
76
+ model_indices[reference_key].delete(reference_value) unless obj
77
+ obj
67
78
  end
68
79
 
69
80
  def clear
70
81
  @store = ::Hash.new { |hash, key| hash[key] = [] }
71
82
  @index = {}
83
+ @inserts_since_compaction = ::Hash.new(0)
84
+ @compaction_count = 0
72
85
  end
73
86
 
74
87
  def store
@@ -81,6 +94,22 @@ module Lutaml
81
94
  end
82
95
  end
83
96
 
97
+ def refs_for(model_key)
98
+ @store[model_key]
99
+ end
100
+
101
+ def inserts_since_compaction
102
+ @inserts_since_compaction
103
+ end
104
+
105
+ def compaction_count
106
+ @compaction_count
107
+ end
108
+
109
+ def index_entry_count(model_key)
110
+ @index[model_key]&.sum { |_reference_key, entries| entries.size } || 0
111
+ end
112
+
84
113
  private
85
114
 
86
115
  def ensure_model_index(model_key)
@@ -120,14 +149,32 @@ module Lutaml
120
149
  nil
121
150
  end
122
151
 
123
- def compact_if_needed(refs)
152
+ def compact_if_needed(refs, model_key)
124
153
  return unless refs.size > COMPACTION_THRESHOLD
154
+ return unless @inserts_since_compaction[model_key] >= COMPACTION_INTERVAL
125
155
 
156
+ @inserts_since_compaction[model_key] = 0
157
+ @compaction_count += 1
126
158
  refs.reject! do |ref|
127
159
  !ref.weakref_alive?
128
160
  rescue WeakRef::RefError
129
161
  true
130
162
  end
163
+ prune_index(model_key)
164
+ end
165
+
166
+ def prune_index(model_key)
167
+ model_indices = @index[model_key]
168
+ return unless model_indices
169
+
170
+ model_indices.delete_if do |_reference_key, entries|
171
+ entries.delete_if do |_value, ref|
172
+ !ref.weakref_alive?
173
+ rescue WeakRef::RefError
174
+ true
175
+ end
176
+ entries.empty?
177
+ end
131
178
  end
132
179
  end
133
180
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.10"
5
+ VERSION = "0.8.12"
6
6
  end
7
7
  end
@@ -10,7 +10,7 @@ module Lutaml
10
10
  super
11
11
  @namespace_set = Lutaml::Rdf::NamespaceSet.new
12
12
  @rdf_subject = nil
13
- @rdf_type = nil
13
+ @rdf_type = []
14
14
  @rdf_predicates = []
15
15
  @rdf_members = []
16
16
  end
@@ -24,20 +24,26 @@ module Lutaml
24
24
  end
25
25
 
26
26
  def type(value)
27
- @rdf_type = value
27
+ @rdf_type = Array(value)
28
28
  end
29
29
 
30
- def predicate(name, namespace:, to:, lang_tagged: false)
30
+ def predicate(name, namespace:, to:, lang_tagged: false,
31
+ uri_reference: false)
31
32
  @rdf_predicates << Lutaml::Rdf::MappingRule.new(
32
33
  name,
33
34
  namespace: namespace,
34
35
  to: to,
35
36
  lang_tagged: lang_tagged,
37
+ uri_reference: uri_reference,
36
38
  )
37
39
  end
38
40
 
39
- def members(attr_name)
40
- @rdf_members << Lutaml::Rdf::MemberRule.new(attr_name)
41
+ def members(attr_name, predicate_name: nil, namespace: nil)
42
+ @rdf_members << Lutaml::Rdf::MemberRule.new(
43
+ attr_name,
44
+ predicate_name: predicate_name,
45
+ namespace: namespace,
46
+ )
41
47
  end
42
48
 
43
49
  def mappings(_register_id = nil)
@@ -57,14 +63,14 @@ module Lutaml
57
63
  end
58
64
 
59
65
  def deep_dup
60
- self.class.new.tap do |new_mapping|
61
- new_mapping.instance_variable_set(:@namespace_set, @namespace_set)
62
- new_mapping.instance_variable_set(:@rdf_subject, @rdf_subject)
63
- new_mapping.instance_variable_set(:@rdf_type, @rdf_type)
64
- new_mapping.instance_variable_set(:@rdf_predicates,
65
- @rdf_predicates.dup)
66
- new_mapping.instance_variable_set(:@rdf_members, @rdf_members.dup)
67
- end
66
+ dup
67
+ end
68
+
69
+ def initialize_copy(source)
70
+ super
71
+ @rdf_type = source.rdf_type.dup
72
+ @rdf_predicates = source.rdf_predicates.dup
73
+ @rdf_members = source.rdf_members.dup
68
74
  end
69
75
  end
70
76
  end