lutaml-model 0.8.36 → 0.8.38
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/bench/bench_sts.rb +9 -3
- data/bench/gate_config.rb +5 -4
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/key_value/transform.rb +52 -5
- data/lib/lutaml/key_value/transformation.rb +10 -2
- data/lib/lutaml/model/adapter_resolver.rb +14 -2
- data/lib/lutaml/model/attribute.rb +15 -9
- data/lib/lutaml/model/choice.rb +6 -5
- data/lib/lutaml/model/format_registry.rb +3 -1
- data/lib/lutaml/model/liquefiable.rb +23 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +46 -11
- data/lib/lutaml/model/sequence.rb +47 -2
- data/lib/lutaml/model/serialize/attribute_definition.rb +176 -47
- data/lib/lutaml/model/serialize/deserialization_context.rb +1 -0
- data/lib/lutaml/model/serialize.rb +54 -10
- data/lib/lutaml/model/services/transformer.rb +30 -11
- data/lib/lutaml/model/store.rb +70 -32
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +1 -0
- data/lib/lutaml/xml/html_entities.rb +290 -0
- data/lib/lutaml/xml/mapping.rb +72 -0
- data/lib/lutaml/xml/model_transform.rb +210 -72
- data/lib/lutaml/xml/parse_session.rb +38 -0
- data/lib/lutaml/yaml/format.rb +1 -0
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +38 -0
- data/spec/lutaml/model/custom_method_context_spec.rb +62 -0
- data/spec/lutaml/model/liquefiable_lazy_spec.rb +36 -0
- data/spec/lutaml/model/mapping_type_validation_spec.rb +61 -0
- data/spec/lutaml/model/sequence_choice_compositor_spec.rb +66 -0
- data/spec/lutaml/model/store_spec.rb +13 -11
- data/spec/lutaml/model/yaml_stream_spec.rb +70 -0
- data/spec/lutaml/xml/html_entities_spec.rb +45 -0
- metadata +9 -1
|
@@ -12,6 +12,100 @@ module Lutaml
|
|
|
12
12
|
#
|
|
13
13
|
# @param attr [Attribute] The attribute to define methods for
|
|
14
14
|
# @param register [Symbol, nil] The register for type resolution
|
|
15
|
+
# Compile (per class, cached) the method that seeds every attribute
|
|
16
|
+
# with its "no data arrived" sentinel: collections share the frozen
|
|
17
|
+
# LAZY_EMPTY_COLLECTION, scalars share the UninitializedClass
|
|
18
|
+
# singleton. Compiled once instead of walked per instance — the
|
|
19
|
+
# grammars-compile-don't-interpret rule applied to object state.
|
|
20
|
+
# Compile (per class and register, on demand) the method that
|
|
21
|
+
# seeds every attribute with its "no data arrived" sentinel:
|
|
22
|
+
# collections share the frozen LAZY_EMPTY_COLLECTION, scalars
|
|
23
|
+
# share the UninitializedClass singleton. Compiled once instead
|
|
24
|
+
# of walked per instance — grammars compile, they don't
|
|
25
|
+
# interpret.
|
|
26
|
+
def compiled_state_defaults_name!(register_id)
|
|
27
|
+
@state_defaults_names ||= {}
|
|
28
|
+
name = @state_defaults_names[register_id]
|
|
29
|
+
return name if name
|
|
30
|
+
|
|
31
|
+
method_name = :"__init_state_defaults_#{register_id}"
|
|
32
|
+
compile_state_defaults!(method_name, register_id)
|
|
33
|
+
@state_defaults_names[register_id] = method_name
|
|
34
|
+
method_name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def compile_state_defaults!(method_name, register_id = nil)
|
|
38
|
+
attrs = attributes(register_id)
|
|
39
|
+
|
|
40
|
+
if attrs.empty?
|
|
41
|
+
define_method(method_name) do
|
|
42
|
+
# no attributes to seed
|
|
43
|
+
end
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
lines = attrs.map do |name, attr|
|
|
48
|
+
if attr.collection?
|
|
49
|
+
"@#{name} = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION"
|
|
50
|
+
else
|
|
51
|
+
"@#{name} = Lutaml::Model::UninitializedClass.instance"
|
|
52
|
+
end
|
|
53
|
+
end.join("\n")
|
|
54
|
+
|
|
55
|
+
# class_eval interpolates per-attribute `@name = <sentinel>` lines:
|
|
56
|
+
# def __init_state_defaults_default
|
|
57
|
+
# @id = Lutaml::Model::UninitializedClass.instance
|
|
58
|
+
# @items = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
|
|
59
|
+
# end
|
|
60
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
61
|
+
def #{method_name}
|
|
62
|
+
#{lines}
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Historical getter shape for punctuation-named attributes and
|
|
68
|
+
# any name the compiled form cannot express.
|
|
69
|
+
def define_reflective_attribute_methods(name, attr)
|
|
70
|
+
if attr.collection?
|
|
71
|
+
define_method(name) do |*args|
|
|
72
|
+
if args.empty?
|
|
73
|
+
materialize_lazy_collection(name)
|
|
74
|
+
else
|
|
75
|
+
# Builder-style: g.member(item) appends to collection
|
|
76
|
+
value = args.first
|
|
77
|
+
current = instance_variable_get(:"@#{name}") || []
|
|
78
|
+
new_value = current.is_a?(Array) ? current + [value] : value
|
|
79
|
+
instance_variable_set(:"@#{name}", new_value)
|
|
80
|
+
record_mutation(name, value)
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
define_method(name) do |*args|
|
|
86
|
+
if args.empty?
|
|
87
|
+
instance_variable_get(:"@#{name}")
|
|
88
|
+
else
|
|
89
|
+
public_send(:"#{name}=", args.first)
|
|
90
|
+
args.first
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def invalidate_state_defaults!
|
|
97
|
+
# Opal's method_defined? takes no inherit flag (see the
|
|
98
|
+
# setter_defined check in define_regular_attribute_methods).
|
|
99
|
+
(@state_defaults_names ||= {}).each_key do |compiled|
|
|
100
|
+
defined_now = if Lutaml::Model.opal?
|
|
101
|
+
method_defined?(compiled)
|
|
102
|
+
else
|
|
103
|
+
method_defined?(compiled, false)
|
|
104
|
+
end
|
|
105
|
+
remove_method(compiled) if defined_now
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
15
109
|
def define_attribute_methods(attr, register = nil)
|
|
16
110
|
name = attr.name
|
|
17
111
|
register_id = extract_register_id(register)
|
|
@@ -106,35 +200,54 @@ module Lutaml
|
|
|
106
200
|
#
|
|
107
201
|
# @param name [Symbol] The attribute name
|
|
108
202
|
# @param attr [Attribute] The attribute definition
|
|
203
|
+
# Plain identifier names compile to `@name` reads; punctuation
|
|
204
|
+
# names (`mixed?`) keep the reflective path (@name is not valid
|
|
205
|
+
# Ruby for them).
|
|
206
|
+
PLAIN_NAME = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
|
|
207
|
+
|
|
109
208
|
def define_regular_attribute_methods(name, attr)
|
|
110
|
-
|
|
111
|
-
|
|
209
|
+
unless name.to_s.match?(PLAIN_NAME)
|
|
210
|
+
return define_reflective_attribute_methods(name, attr)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Getters compile with a sentinel default argument instead of a
|
|
214
|
+
# `*args` splat: the splat allocated an (almost always empty)
|
|
215
|
+
# Array on EVERY read — the largest per-call allocation source
|
|
216
|
+
# on instance-heavy parses (TODO.max-perf/06). The optional-arg
|
|
217
|
+
# form allocates nothing, and the read is a direct @ivar.
|
|
112
218
|
if attr.collection?
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
219
|
+
# class_eval interpolates, e.g.:
|
|
220
|
+
# def items(arg = Lutaml::Model::Serialize::NO_ARG)
|
|
221
|
+
# if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
222
|
+
# materialize_lazy_collection(:items)
|
|
223
|
+
# else
|
|
224
|
+
# ... builder append ...
|
|
225
|
+
# end
|
|
226
|
+
# end
|
|
227
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
228
|
+
def #{name}(arg = Lutaml::Model::Serialize::NO_ARG)
|
|
229
|
+
if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
230
|
+
materialize_lazy_collection(:#{name})
|
|
231
|
+
else
|
|
232
|
+
current = @#{name} || []
|
|
233
|
+
new_value = current.is_a?(Array) ? current + [arg] : arg
|
|
234
|
+
@#{name} = new_value
|
|
235
|
+
record_mutation(:#{name}, arg)
|
|
236
|
+
arg
|
|
237
|
+
end
|
|
124
238
|
end
|
|
125
|
-
|
|
239
|
+
RUBY
|
|
126
240
|
else
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
args.first
|
|
241
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
242
|
+
def #{name}(arg = Lutaml::Model::Serialize::NO_ARG)
|
|
243
|
+
if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
244
|
+
@#{name}
|
|
245
|
+
else
|
|
246
|
+
public_send(:"#{name}=", arg)
|
|
247
|
+
arg
|
|
248
|
+
end
|
|
136
249
|
end
|
|
137
|
-
|
|
250
|
+
RUBY
|
|
138
251
|
end
|
|
139
252
|
|
|
140
253
|
enum_shorthand_names = instance_variable_get(:@__enum_shorthand_names__) || Set.new
|
|
@@ -146,35 +259,50 @@ module Lutaml
|
|
|
146
259
|
else
|
|
147
260
|
method_defined?(:"#{name}=", false)
|
|
148
261
|
end
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
262
|
+
return if setter_defined && !enum_shorthand_names.include?(name.to_s)
|
|
263
|
+
|
|
264
|
+
# class_eval'd bodies cannot close over locals; a hidden
|
|
265
|
+
# define_method accessor holds the Attribute handle.
|
|
266
|
+
attr_reader_method = :"__attribute_definition_#{name}"
|
|
267
|
+
unless method_defined?(attr_reader_method, false)
|
|
268
|
+
define_method(attr_reader_method) { attr }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if attr.collection?
|
|
272
|
+
# class_eval interpolates, e.g.:
|
|
273
|
+
# def items=(value)
|
|
274
|
+
# value_set_for(:items)
|
|
275
|
+
# value = ATTR.cast_value(value, lutaml_register)
|
|
276
|
+
# current = @items
|
|
277
|
+
# ... sentinel preservation ...
|
|
278
|
+
# record_mutation_collection(:items, value)
|
|
279
|
+
# end
|
|
280
|
+
# The compiled body writes @items directly: the define_method
|
|
281
|
+
# form interpolated "@items" name strings per call — one per
|
|
282
|
+
# setter invocation on instance-heavy parses (TODO.max-perf/06).
|
|
283
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
284
|
+
def #{name}=(value)
|
|
285
|
+
value_set_for(:#{name})
|
|
286
|
+
value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
|
|
287
|
+
current = @#{name}
|
|
288
|
+
if current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
|
|
161
289
|
(value.nil? || Lutaml::Model::Utils.uninitialized?(value))
|
|
162
290
|
# Sentinel stays — no allocation for truly empty collections
|
|
163
291
|
else
|
|
164
|
-
|
|
292
|
+
@#{name} = value
|
|
165
293
|
end
|
|
166
|
-
|
|
167
|
-
# number of <name> elements that will be emitted.
|
|
168
|
-
record_mutation_collection(name, value)
|
|
294
|
+
record_mutation_collection(:#{name}, value)
|
|
169
295
|
end
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
296
|
+
RUBY
|
|
297
|
+
else
|
|
298
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
299
|
+
def #{name}=(value)
|
|
300
|
+
value_set_for(:#{name})
|
|
301
|
+
value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
|
|
302
|
+
@#{name} = value
|
|
303
|
+
record_mutation(:#{name}, value)
|
|
176
304
|
end
|
|
177
|
-
|
|
305
|
+
RUBY
|
|
178
306
|
end
|
|
179
307
|
end
|
|
180
308
|
|
|
@@ -200,6 +328,7 @@ module Lutaml
|
|
|
200
328
|
attr = Attribute.new(name, type, options)
|
|
201
329
|
@attributes[name] = attr
|
|
202
330
|
@merged_attributes_cache = nil
|
|
331
|
+
invalidate_state_defaults!
|
|
203
332
|
define_attribute_methods(attr)
|
|
204
333
|
|
|
205
334
|
attr
|
|
@@ -33,6 +33,11 @@ module Lutaml
|
|
|
33
33
|
# The getter materializes a real Array on first access.
|
|
34
34
|
LAZY_EMPTY_COLLECTION = [].freeze
|
|
35
35
|
|
|
36
|
+
# Sentinel distinguishing "getter called with no argument" from
|
|
37
|
+
# builder-syntax `g.attr(value)` where value may be nil. Compiled
|
|
38
|
+
# getters default to it instead of a `*args` splat.
|
|
39
|
+
NO_ARG = Object.new.freeze
|
|
40
|
+
|
|
36
41
|
INTERNAL_ATTRIBUTES = %i[@using_default @lutaml_register @lutaml_parent @lutaml_root
|
|
37
42
|
@register_records].freeze
|
|
38
43
|
|
|
@@ -73,6 +78,49 @@ module Lutaml
|
|
|
73
78
|
end
|
|
74
79
|
end
|
|
75
80
|
|
|
81
|
+
# lutaml-model#436: multi-document streams. `from_<f>_stream`
|
|
82
|
+
# parses each document separately; `discriminator:` receives each
|
|
83
|
+
# parsed document hash and returns the model class to build with,
|
|
84
|
+
# enabling polymorphic streams. `to_<f>_stream` joins instances
|
|
85
|
+
# into one stream document.
|
|
86
|
+
def self.register_stream_methods(format)
|
|
87
|
+
ClassMethods.define_method(:"from_#{format}_stream") do |data, options = {}|
|
|
88
|
+
docs = Lutaml::Model::Serialize.stream_documents(data, format, options)
|
|
89
|
+
discriminator = options[:discriminator]
|
|
90
|
+
docs.map do |doc|
|
|
91
|
+
target = discriminator ? discriminator.call(doc) : self
|
|
92
|
+
target.send(:"of_#{format}", doc, options)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
ClassMethods.define_method(:"to_#{format}_stream") do |instances, options = {}|
|
|
97
|
+
instances.map do |instance|
|
|
98
|
+
instance.send(:"to_#{format}", options)
|
|
99
|
+
end.join("---\n")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.stream_documents(data, format, options)
|
|
104
|
+
require "yaml"
|
|
105
|
+
if YAML.respond_to?(:load_stream, true)
|
|
106
|
+
begin
|
|
107
|
+
docs = YAML.load_stream(data,
|
|
108
|
+
aliases: options.fetch(:aliases, true))
|
|
109
|
+
rescue ArgumentError
|
|
110
|
+
# Older Psych builds without the aliases keyword: aliases are
|
|
111
|
+
# on by default there.
|
|
112
|
+
docs = YAML.load_stream(data)
|
|
113
|
+
end
|
|
114
|
+
docs.is_a?(Array) ? docs.compact : [docs].compact
|
|
115
|
+
else
|
|
116
|
+
data.split(/^---\s*$/).filter_map do |chunk|
|
|
117
|
+
chunk.strip.empty? ? nil : YAML.safe_load(chunk)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
raise Lutaml::Model::InvalidFormatError.new(format, e.message)
|
|
122
|
+
end
|
|
123
|
+
|
|
76
124
|
def self.register_to_format_method(format)
|
|
77
125
|
ClassMethods.define_method(:"to_#{format}") do |instance, options = {}|
|
|
78
126
|
to(format, instance, options)
|
|
@@ -108,16 +156,12 @@ module Lutaml
|
|
|
108
156
|
@using_default = nil
|
|
109
157
|
@lutaml_register = register
|
|
110
158
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
self.class.attributes(register).each do |name, attr|
|
|
118
|
-
instance_variable_set(:"@#{name}",
|
|
119
|
-
attr.collection? ? LAZY_EMPTY_COLLECTION : Lutaml::Model::UninitializedClass.instance)
|
|
120
|
-
end
|
|
159
|
+
# Seed every attribute through the per-(class, register) compiled
|
|
160
|
+
# method: plain `@x = sentinel` writes, no per-call "@#{name}"
|
|
161
|
+
# string interpolation, no instance_variable_set. The previous
|
|
162
|
+
# generic walk interpolated a name string per attribute per
|
|
163
|
+
# instance — 581k allocations on the ISO-13849 profile.
|
|
164
|
+
public_send(self.class.compiled_state_defaults_name!(self.class.extract_register_id(register)))
|
|
121
165
|
end
|
|
122
166
|
|
|
123
167
|
# Complete deserialization initialization after allocation.
|
|
@@ -2,8 +2,8 @@ module Lutaml
|
|
|
2
2
|
module Model
|
|
3
3
|
class Transformer
|
|
4
4
|
class << self
|
|
5
|
-
def call(value, rule, attribute, format: nil)
|
|
6
|
-
new(rule, attribute, format).call(value)
|
|
5
|
+
def call(value, rule, attribute, format: nil, context: nil)
|
|
6
|
+
new(rule, attribute, format, context).call(value)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
private
|
|
@@ -15,7 +15,7 @@ module Lutaml
|
|
|
15
15
|
transform.is_a?(::Hash) ? transform[direction] : transform
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def apply_static(value, sources, direction, format)
|
|
18
|
+
def apply_static(value, sources, direction, format, context = nil)
|
|
19
19
|
methods = sources.filter_map do |obj|
|
|
20
20
|
get_transform_static(obj, direction)
|
|
21
21
|
end
|
|
@@ -34,16 +34,23 @@ module Lutaml
|
|
|
34
34
|
tc.public_send(apply_direction, v, format)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
methods.reduce(result)
|
|
37
|
+
methods.reduce(result) do |tv, m|
|
|
38
|
+
if m.arity == 2
|
|
39
|
+
m.call(tv, context)
|
|
40
|
+
else
|
|
41
|
+
m.call(tv)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
41
|
-
attr_reader :rule, :attribute, :format
|
|
47
|
+
attr_reader :rule, :attribute, :format, :context
|
|
42
48
|
|
|
43
|
-
def initialize(rule, attribute, format = nil)
|
|
49
|
+
def initialize(rule, attribute, format = nil, context = nil)
|
|
44
50
|
@rule = rule
|
|
45
51
|
@attribute = attribute
|
|
46
52
|
@format = format
|
|
53
|
+
@context = context
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
def call(value)
|
|
@@ -61,7 +68,19 @@ module Lutaml
|
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
methods.reduce(result) do |transformed_value, method|
|
|
64
|
-
method
|
|
71
|
+
invoke_transform(method, transformed_value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# lutaml-model#550: `with:` custom methods opt into the options
|
|
76
|
+
# passed to `from_*` / `to_*` by declaring a second parameter
|
|
77
|
+
# (exact arity 2); one-parameter methods keep the historical
|
|
78
|
+
# single-argument call.
|
|
79
|
+
def invoke_transform(method, value)
|
|
80
|
+
if method.arity == 2
|
|
81
|
+
method.call(value, context)
|
|
82
|
+
else
|
|
83
|
+
method.call(value)
|
|
65
84
|
end
|
|
66
85
|
end
|
|
67
86
|
|
|
@@ -87,8 +106,8 @@ module Lutaml
|
|
|
87
106
|
|
|
88
107
|
class ImportTransformer < Transformer
|
|
89
108
|
class << self
|
|
90
|
-
def call(value, rule, attribute, format: nil)
|
|
91
|
-
apply_static(value, [rule, attribute], :import, format)
|
|
109
|
+
def call(value, rule, attribute, format: nil, context: nil)
|
|
110
|
+
apply_static(value, [rule, attribute], :import, format, context)
|
|
92
111
|
end
|
|
93
112
|
end
|
|
94
113
|
|
|
@@ -103,8 +122,8 @@ module Lutaml
|
|
|
103
122
|
|
|
104
123
|
class ExportTransformer < Transformer
|
|
105
124
|
class << self
|
|
106
|
-
def call(value, rule, attribute, format: nil)
|
|
107
|
-
apply_static(value, [attribute, rule], :export, format)
|
|
125
|
+
def call(value, rule, attribute, format: nil, context: nil)
|
|
126
|
+
apply_static(value, [attribute, rule], :export, format, context)
|
|
108
127
|
end
|
|
109
128
|
end
|
|
110
129
|
|
data/lib/lutaml/model/store.rb
CHANGED
|
@@ -6,15 +6,28 @@ require "weakref" unless Lutaml::Model.opal?
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Model
|
|
8
8
|
class Store
|
|
9
|
-
# Compact dead
|
|
9
|
+
# Compact dead index entries once a class bucket grows past this size.
|
|
10
10
|
COMPACTION_THRESHOLD = 1000
|
|
11
11
|
|
|
12
12
|
# Once the threshold is exceeded, only compact every Nth subsequent
|
|
13
|
-
# register call. Amortises the O(N)
|
|
13
|
+
# register call. Amortises the O(N) prune over N inserts so
|
|
14
14
|
# register stays O(1) per call rather than O(N) per call (O(N^2)
|
|
15
15
|
# cumulatively for the class).
|
|
16
16
|
COMPACTION_INTERVAL = 1000
|
|
17
17
|
|
|
18
|
+
WeakBucket = if Lutaml::Model.opal?
|
|
19
|
+
# Opal has neither WeakRef nor ObjectSpace::WeakMap; documents in
|
|
20
|
+
# the browser are small, so strong references are the fallback.
|
|
21
|
+
::Hash
|
|
22
|
+
else
|
|
23
|
+
# CRuby: WeakMap holds keys weakly with zero Ruby-level
|
|
24
|
+
# allocation per insert and no per-object finalizer — the WeakRef
|
|
25
|
+
# approach allocated one finalizer-registering object per
|
|
26
|
+
# instance (lutaml-model#695) and dominated GC cycles on
|
|
27
|
+
# instance-heavy parses.
|
|
28
|
+
::ObjectSpace::WeakMap
|
|
29
|
+
end
|
|
30
|
+
|
|
18
31
|
class << self
|
|
19
32
|
def instance
|
|
20
33
|
@instance ||= new
|
|
@@ -42,8 +55,10 @@ module Lutaml
|
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
def initialize
|
|
45
|
-
|
|
46
|
-
#
|
|
58
|
+
# WeakMap-based buckets: value presence marks liveness, no
|
|
59
|
+
# per-instance allocation (lutaml-model#695).
|
|
60
|
+
@store = ::Hash.new { |hash, key| hash[key] = WeakBucket.new }
|
|
61
|
+
# Nested index: { model_key => { reference_key => { value => object } } }
|
|
47
62
|
# Grouped by model_key so register only iterates this class's own indices.
|
|
48
63
|
@index = {}
|
|
49
64
|
@inserts_since_compaction = ::Hash.new(0)
|
|
@@ -52,11 +67,10 @@ module Lutaml
|
|
|
52
67
|
|
|
53
68
|
def register(object)
|
|
54
69
|
model_key = object.class.to_s
|
|
55
|
-
|
|
56
|
-
refs << WeakRef.new(object)
|
|
70
|
+
@store[model_key][object] = true
|
|
57
71
|
@inserts_since_compaction[model_key] += 1
|
|
58
72
|
|
|
59
|
-
compact_if_needed(
|
|
73
|
+
compact_if_needed(model_key)
|
|
60
74
|
|
|
61
75
|
update_existing_indices(object, model_key)
|
|
62
76
|
end
|
|
@@ -78,25 +92,26 @@ module Lutaml
|
|
|
78
92
|
obj
|
|
79
93
|
end
|
|
80
94
|
|
|
95
|
+
def live_objects(model_key)
|
|
96
|
+
bucket = @store[model_key]
|
|
97
|
+
return [] unless bucket
|
|
98
|
+
|
|
99
|
+
live_objects_for(bucket)
|
|
100
|
+
end
|
|
101
|
+
|
|
81
102
|
def clear
|
|
82
|
-
@store = ::Hash.new { |hash, key| hash[key] =
|
|
103
|
+
@store = ::Hash.new { |hash, key| hash[key] = WeakBucket.new }
|
|
83
104
|
@index = {}
|
|
84
105
|
@inserts_since_compaction = ::Hash.new(0)
|
|
85
106
|
@compaction_count = 0
|
|
86
107
|
end
|
|
87
108
|
|
|
88
109
|
def store
|
|
89
|
-
@store.transform_values
|
|
90
|
-
refs.each_with_object([]) do |ref, alive|
|
|
91
|
-
alive << ref.__getobj__ if ref.weakref_alive?
|
|
92
|
-
rescue WeakRef::RefError
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
end
|
|
110
|
+
@store.transform_values { |refs| live_objects_for(refs) }
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
def refs_for(model_key)
|
|
99
|
-
|
|
114
|
+
LiveView.new(self, model_key)
|
|
100
115
|
end
|
|
101
116
|
|
|
102
117
|
def inserts_since_compaction
|
|
@@ -117,15 +132,15 @@ module Lutaml
|
|
|
117
132
|
@index[model_key] ||= {}
|
|
118
133
|
end
|
|
119
134
|
|
|
120
|
-
# Build index for a (model_class, reference_key) pair by scanning
|
|
135
|
+
# Build index for a (model_class, reference_key) pair by scanning
|
|
136
|
+
# live instances. Index values hold WeakRefs: the index must not
|
|
137
|
+
# pin objects (only the rare classes with resolved reference keys
|
|
138
|
+
# pay this; the per-instance bucket stays allocation-free WeakMap).
|
|
121
139
|
def build_index(model_indices, model_key, reference_key)
|
|
122
140
|
entries = model_indices[reference_key] = {}
|
|
123
|
-
|
|
124
|
-
obj = ref.__getobj__
|
|
141
|
+
each_live(model_key) do |obj|
|
|
125
142
|
value = obj.public_send(reference_key)
|
|
126
143
|
entries[value] = WeakRef.new(obj) if value
|
|
127
|
-
rescue WeakRef::RefError
|
|
128
|
-
next
|
|
129
144
|
end
|
|
130
145
|
end
|
|
131
146
|
|
|
@@ -136,11 +151,9 @@ module Lutaml
|
|
|
136
151
|
model_indices = @index[model_key]
|
|
137
152
|
return unless model_indices
|
|
138
153
|
|
|
139
|
-
model_indices.
|
|
154
|
+
model_indices.each_key do |reference_key|
|
|
140
155
|
value = object.public_send(reference_key)
|
|
141
|
-
|
|
142
|
-
rescue WeakRef::RefError
|
|
143
|
-
next
|
|
156
|
+
model_indices[reference_key][value] = WeakRef.new(object) if value
|
|
144
157
|
end
|
|
145
158
|
end
|
|
146
159
|
|
|
@@ -150,17 +163,11 @@ module Lutaml
|
|
|
150
163
|
nil
|
|
151
164
|
end
|
|
152
165
|
|
|
153
|
-
def compact_if_needed(
|
|
154
|
-
return unless refs.size > COMPACTION_THRESHOLD
|
|
166
|
+
def compact_if_needed(model_key)
|
|
155
167
|
return unless @inserts_since_compaction[model_key] >= COMPACTION_INTERVAL
|
|
156
168
|
|
|
157
169
|
@inserts_since_compaction[model_key] = 0
|
|
158
170
|
@compaction_count += 1
|
|
159
|
-
refs.reject! do |ref|
|
|
160
|
-
!ref.weakref_alive?
|
|
161
|
-
rescue WeakRef::RefError
|
|
162
|
-
true
|
|
163
|
-
end
|
|
164
171
|
prune_index(model_key)
|
|
165
172
|
end
|
|
166
173
|
|
|
@@ -177,6 +184,37 @@ module Lutaml
|
|
|
177
184
|
entries.empty?
|
|
178
185
|
end
|
|
179
186
|
end
|
|
187
|
+
|
|
188
|
+
def each_live(model_key, &block)
|
|
189
|
+
@store[model_key]&.each_key(&block)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def live_objects_for(bucket)
|
|
193
|
+
# WeakMap#each_key requires a block on CRuby; WeakMap#each
|
|
194
|
+
# yields key/value pairs, and Hash buckets enumerate the same
|
|
195
|
+
# way (value is `true` for WeakMap, the object for the Opal
|
|
196
|
+
# fallback keyed by object identity).
|
|
197
|
+
bucket.map { |key, _value| key }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Array-shaped live view over a class's WeakMap bucket, for
|
|
201
|
+
# specs/debugging (refs_for used to expose the raw array).
|
|
202
|
+
class LiveView
|
|
203
|
+
include Enumerable
|
|
204
|
+
|
|
205
|
+
def initialize(store, model_key)
|
|
206
|
+
@store = store
|
|
207
|
+
@model_key = model_key
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def each(&block)
|
|
211
|
+
@store.live_objects(@model_key).each(&block)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def size
|
|
215
|
+
@store.live_objects(@model_key).size
|
|
216
|
+
end
|
|
217
|
+
end
|
|
180
218
|
end
|
|
181
219
|
end
|
|
182
220
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/format.rb
CHANGED