lutaml-model 0.8.37 → 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/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/attribute.rb +5 -1
- data/lib/lutaml/model/format_registry.rb +3 -1
- data/lib/lutaml/model/mapping/mapping_rule.rb +39 -11
- data/lib/lutaml/model/serialize/attribute_definition.rb +140 -65
- data/lib/lutaml/model/serialize/deserialization_context.rb +1 -0
- data/lib/lutaml/model/serialize.rb +54 -15
- data/lib/lutaml/model/services/transformer.rb +30 -11
- 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 +19 -0
- data/lib/lutaml/xml/model_transform.rb +174 -53
- data/lib/lutaml/xml/parse_session.rb +24 -11
- data/lib/lutaml/yaml/format.rb +1 -0
- data/spec/lutaml/model/custom_method_context_spec.rb +62 -0
- data/spec/lutaml/model/yaml_stream_spec.rb +70 -0
- data/spec/lutaml/xml/html_entities_spec.rb +45 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad19014fa289dad72ab50f6908d425ed7708b4f7046432ac05426b65087fc713
|
|
4
|
+
data.tar.gz: 4f2d95722e22e73b3691e6eec7903d3967ec1d9cf9ea2b641b02b77cc59911ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 575ac30c297c4d8059bccb27b42b1d1e942ffef3352072f1ebdbff55039073cffb228a2e6873c55afd940bc78aaac1830c5375df0b37ac1b4eb498cea3a73fe2
|
|
7
|
+
data.tar.gz: 4d1c80a2a3fd04e5c461aec33ba2248547ae5f6c877990f457d23160c272a4f67e17ef4ab05ad450265f5c2aa8c5c637deead3c27b20a2ea1978ab4815d6d397
|
data/bench/gate_config.rb
CHANGED
|
@@ -57,10 +57,11 @@ module GateConfig
|
|
|
57
57
|
"iso-13849-1MB" => {
|
|
58
58
|
alloc_ratio: 1.05,
|
|
59
59
|
time_ratio: 1.15,
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
60
|
+
# Post-TODO.max-perf/01-03 the fixture sits at ~4.88M allocs and
|
|
61
|
+
# 14.5-16.5s on GH-hosted ubuntu runners (runner-variance band);
|
|
62
|
+
# the old 15.0 missed by 0.06s on a healthy run. 30.0 keeps the
|
|
63
|
+
# 2x-catastrophe safety net over the healthy band (#313).
|
|
64
|
+
absolute_max: 30.0,
|
|
64
65
|
},
|
|
65
66
|
"din-iso-1.1MB" => {
|
|
66
67
|
alloc_ratio: 1.05,
|
|
@@ -146,6 +146,7 @@ require "lutaml/xml/encoding_normalizer"
|
|
|
146
146
|
require "lutaml/xml/format"
|
|
147
147
|
require "lutaml/xml/format_chooser"
|
|
148
148
|
require "lutaml/xml/hoisting_algorithm"
|
|
149
|
+
require "lutaml/xml/html_entities"
|
|
149
150
|
require "lutaml/xml/listener"
|
|
150
151
|
require "lutaml/xml/mapping"
|
|
151
152
|
require "lutaml/xml/mapping_rule"
|
|
@@ -68,10 +68,33 @@ module Lutaml
|
|
|
68
68
|
|
|
69
69
|
def process_mapping_for_instance(instance, hash, format, rule, options)
|
|
70
70
|
if rule.custom_methods[:to]
|
|
71
|
-
|
|
71
|
+
to_method = rule.custom_methods[:to]
|
|
72
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
73
|
+
# parameter to receive the options passed to `to_*`.
|
|
74
|
+
if instance.method(to_method).parameters.size >= 3
|
|
75
|
+
return instance.public_send(to_method, instance, hash,
|
|
76
|
+
options[:context])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return instance.public_send(to_method, instance, hash)
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
attribute = attributes[rule.to]
|
|
83
|
+
|
|
84
|
+
# TODO.max-perf/10: plain scalar/collection rules take a fast
|
|
85
|
+
# lane — memoized wire name, shared render? semantics, one
|
|
86
|
+
# serialize dispatch — collapsing the interpretive branch
|
|
87
|
+
# probes below. Rules with any special feature stay on the
|
|
88
|
+
# full path.
|
|
89
|
+
if (plan = kv_serialize_plan(rule, attribute, instance))
|
|
90
|
+
value = instance.public_send(attribute.name)
|
|
91
|
+
if rule.render?(value, instance)
|
|
92
|
+
hash[plan.wire] = attribute.serialize(value, format,
|
|
93
|
+
lutaml_register, options)
|
|
94
|
+
end
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
75
98
|
value = rule.serialize(instance)
|
|
76
99
|
|
|
77
100
|
if rule.can_transform_to?(attribute, format)
|
|
@@ -90,7 +113,9 @@ module Lutaml
|
|
|
90
113
|
end
|
|
91
114
|
|
|
92
115
|
# Use the format parameter passed in instead of hardcoding to :json
|
|
93
|
-
value = ExportTransformer.call(value, rule, attribute,
|
|
116
|
+
value = ExportTransformer.call(value, rule, attribute,
|
|
117
|
+
format: format,
|
|
118
|
+
context: options[:context])
|
|
94
119
|
|
|
95
120
|
value = serialize_value(value, rule, attribute, format, options)
|
|
96
121
|
|
|
@@ -247,7 +272,7 @@ format)
|
|
|
247
272
|
if (plan = kv_rule_plan(format, rule, attr)) &&
|
|
248
273
|
(value = kv_fast_extract(doc, plan))
|
|
249
274
|
rule.deserialize(instance, kv_fast_cast(value, plan, instance),
|
|
250
|
-
attributes, self)
|
|
275
|
+
attributes, self, options[:context])
|
|
251
276
|
return
|
|
252
277
|
end
|
|
253
278
|
|
|
@@ -261,7 +286,9 @@ format)
|
|
|
261
286
|
# only nil (non-existent) skips it (lutaml-model#746).
|
|
262
287
|
return if value.nil?
|
|
263
288
|
|
|
264
|
-
|
|
289
|
+
warn "PROBE-264 keys=#{options.keys.inspect} ctx=#{options[:context].inspect}"
|
|
290
|
+
return rule.deserialize(instance, value, attributes, model_class,
|
|
291
|
+
options[:context])
|
|
265
292
|
end
|
|
266
293
|
|
|
267
294
|
value = rule.transform_value(attr, value, :from, format)
|
|
@@ -279,7 +306,8 @@ format)
|
|
|
279
306
|
if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
|
|
280
307
|
attr.valid_collection!(value, context)
|
|
281
308
|
end
|
|
282
|
-
rule.deserialize(instance, value, attributes, self
|
|
309
|
+
rule.deserialize(instance, value, attributes, self,
|
|
310
|
+
options[:context])
|
|
283
311
|
end
|
|
284
312
|
|
|
285
313
|
# Compiled rule plans (TODO.perf/07): for plain scalar rules the
|
|
@@ -291,6 +319,25 @@ format)
|
|
|
291
319
|
# unions, polymorphism) return nil and keep the interpretive path.
|
|
292
320
|
KvRulePlan = ::Struct.new(:wire, :klass, :collection)
|
|
293
321
|
|
|
322
|
+
# Memoized (per transform) wire name for plain rules — the from
|
|
323
|
+
# spelling never changes after definition.
|
|
324
|
+
KvSerializePlan = ::Struct.new(:wire)
|
|
325
|
+
|
|
326
|
+
def kv_serialize_plan(rule, attr, _instance)
|
|
327
|
+
return nil if rule.delegate || rule.raw_mapping? || rule.root_mapping? ||
|
|
328
|
+
rule.hash_mappings || rule.child_mappings ||
|
|
329
|
+
rule.has_custom_method_for_serialization? ||
|
|
330
|
+
rule.multiple_mappings? || polymorphic_rule?(rule) ||
|
|
331
|
+
(rule.transform.is_a?(Hash) && !rule.transform.empty?) ||
|
|
332
|
+
rule.transform.is_a?(Class) || attr.nil? || attr.derived? ||
|
|
333
|
+
attr.union? || attr.polymorphic? || attr.custom_collection? ||
|
|
334
|
+
attr.transform ||
|
|
335
|
+
rule.value_map(:to) != Lutaml::Model::Serialize::DEFAULT_VALUE_MAP
|
|
336
|
+
|
|
337
|
+
@kv_serialize_plans ||= {}.compare_by_identity
|
|
338
|
+
@kv_serialize_plans[rule] ||= KvSerializePlan.new(rule_from_name(rule))
|
|
339
|
+
end
|
|
340
|
+
|
|
294
341
|
def kv_rule_plan(format, rule, attr)
|
|
295
342
|
@kv_rule_plans ||= {}
|
|
296
343
|
@kv_rule_plans[[format, rule]] ||= build_kv_rule_plan(format, rule,
|
|
@@ -220,9 +220,17 @@ register = self.register)
|
|
|
220
220
|
|
|
221
221
|
# Handle custom serialization methods (e.g., with: { to: ... })
|
|
222
222
|
if rule.has_custom_methods? && rule.custom_methods[:to]
|
|
223
|
+
to_method = rule.custom_methods[:to]
|
|
224
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
225
|
+
# parameter to receive the options passed to `to_*`.
|
|
226
|
+
if model_instance.method(to_method).parameters.size >= 3
|
|
227
|
+
return model_instance.public_send(to_method, model_instance,
|
|
228
|
+
parent, options[:context])
|
|
229
|
+
end
|
|
230
|
+
|
|
223
231
|
# Call custom method which directly modifies the parent element
|
|
224
|
-
return model_instance.public_send(
|
|
225
|
-
|
|
232
|
+
return model_instance.public_send(to_method, model_instance,
|
|
233
|
+
parent)
|
|
226
234
|
end
|
|
227
235
|
|
|
228
236
|
# Handle delegation - extract value from delegated object
|
|
@@ -471,7 +471,11 @@ module Lutaml
|
|
|
471
471
|
@options[:ref_model_class], @options[:ref_key_attribute])
|
|
472
472
|
end
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
# The castability verdict is a class-hierarchy fact, immutable
|
|
475
|
+
# per resolved type — check once, not per value (TODO.max-perf/15).
|
|
476
|
+
checked = (@castable_types ||= {}.compare_by_identity)
|
|
477
|
+
validate_attr_type!(resolved_type) unless checked[resolved_type]
|
|
478
|
+
checked[resolved_type] = true
|
|
475
479
|
|
|
476
480
|
resolved_type.cast(value)
|
|
477
481
|
end
|
|
@@ -23,7 +23,8 @@ module Lutaml
|
|
|
23
23
|
# @param adapter_options [Hash, nil] { available: [...], default: :name }
|
|
24
24
|
def register(format, mapping_class:, adapter_class:, transformer:,
|
|
25
25
|
adapter_loader: nil, castable_type: nil, key_value: nil,
|
|
26
|
-
rdf: nil, error_types: nil, adapter_options: nil
|
|
26
|
+
rdf: nil, error_types: nil, adapter_options: nil,
|
|
27
|
+
stream_methods: false)
|
|
27
28
|
validate_registration!(format, mapping_class, transformer)
|
|
28
29
|
|
|
29
30
|
registered_formats[format] = {
|
|
@@ -44,6 +45,7 @@ module Lutaml
|
|
|
44
45
|
::Lutaml::Model::Serialize.register_format_mapping_method(format)
|
|
45
46
|
::Lutaml::Model::Serialize.register_from_format_method(format)
|
|
46
47
|
::Lutaml::Model::Serialize.register_to_format_method(format)
|
|
48
|
+
::Lutaml::Model::Serialize.register_stream_methods(format) if stream_methods
|
|
47
49
|
|
|
48
50
|
::Lutaml::Model::Attribute.format_specific_warn_names.push(:"to_#{format}")
|
|
49
51
|
|
|
@@ -268,9 +268,16 @@ module Lutaml
|
|
|
268
268
|
end
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
-
def serialize(model, parent = nil, doc = nil)
|
|
271
|
+
def serialize(model, parent = nil, doc = nil, context = nil)
|
|
272
272
|
if custom_methods[:to]
|
|
273
|
-
|
|
273
|
+
to_method = custom_methods[:to]
|
|
274
|
+
# lutaml-model#550: custom methods may declare a fourth
|
|
275
|
+
# context parameter to receive the options passed to `to_*`.
|
|
276
|
+
if model.method(to_method).parameters.size >= 4
|
|
277
|
+
model.public_send(to_method, model, parent, doc, context)
|
|
278
|
+
else
|
|
279
|
+
model.public_send(to_method, model, parent, doc)
|
|
280
|
+
end
|
|
274
281
|
else
|
|
275
282
|
value = to_value_for(model)
|
|
276
283
|
|
|
@@ -289,13 +296,14 @@ module Lutaml
|
|
|
289
296
|
end
|
|
290
297
|
end
|
|
291
298
|
|
|
292
|
-
def deserialize(model, value, attributes, mapper_class = nil
|
|
299
|
+
def deserialize(model, value, attributes, mapper_class = nil,
|
|
300
|
+
context = nil)
|
|
293
301
|
if @needs_full_deserialize
|
|
294
|
-
handle_custom_method(model, value, mapper_class) ||
|
|
302
|
+
handle_custom_method(model, value, mapper_class, context) ||
|
|
295
303
|
handle_delegate(model, value, attributes) ||
|
|
296
|
-
handle_transform_method(model, value, attributes)
|
|
304
|
+
handle_transform_method(model, value, attributes, context)
|
|
297
305
|
else
|
|
298
|
-
handle_transform_method(model, value, attributes)
|
|
306
|
+
handle_transform_method(model, value, attributes, context)
|
|
299
307
|
end
|
|
300
308
|
end
|
|
301
309
|
|
|
@@ -416,10 +424,29 @@ module Lutaml
|
|
|
416
424
|
end
|
|
417
425
|
end
|
|
418
426
|
|
|
419
|
-
def handle_custom_method(model, value, mapper_class)
|
|
420
|
-
|
|
427
|
+
def handle_custom_method(model, value, mapper_class, context = nil)
|
|
428
|
+
custom = custom_methods[:from]
|
|
429
|
+
return if !custom || value.nil?
|
|
421
430
|
|
|
422
|
-
|
|
431
|
+
if custom.is_a?(String) || custom.is_a?(Symbol)
|
|
432
|
+
target = mapper_class.new
|
|
433
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
434
|
+
# parameter to receive the options passed to `from_*`.
|
|
435
|
+
if target.method(custom).parameters.size >= 3
|
|
436
|
+
target.public_send(custom, model, value, context)
|
|
437
|
+
else
|
|
438
|
+
target.public_send(custom, model, value)
|
|
439
|
+
end
|
|
440
|
+
else
|
|
441
|
+
# Callable (proc/lambda): transforms the value and the rule
|
|
442
|
+
# assigns it. Exact arity 2 receives the `from_*` context.
|
|
443
|
+
transformed = if custom.arity == 2
|
|
444
|
+
custom.call(value, context)
|
|
445
|
+
else
|
|
446
|
+
custom.call(value)
|
|
447
|
+
end
|
|
448
|
+
assign_value(model, transformed)
|
|
449
|
+
end
|
|
423
450
|
true
|
|
424
451
|
end
|
|
425
452
|
|
|
@@ -439,7 +466,7 @@ module Lutaml
|
|
|
439
466
|
attributes[delegate].type(model.lutaml_register).new)
|
|
440
467
|
end
|
|
441
468
|
|
|
442
|
-
def handle_transform_method(model, value, attributes)
|
|
469
|
+
def handle_transform_method(model, value, attributes, context = nil)
|
|
443
470
|
attr = attributes[to]
|
|
444
471
|
# Fast path: no transforms at all (covers 95%+ of rules)
|
|
445
472
|
# transform defaults to {} which is truthy but semantically empty
|
|
@@ -460,7 +487,8 @@ module Lutaml
|
|
|
460
487
|
assign_value(model, value)
|
|
461
488
|
else
|
|
462
489
|
# Hash/proc transformers need ImportTransformer
|
|
463
|
-
transformed = ImportTransformer.call(value, self, attr
|
|
490
|
+
transformed = ImportTransformer.call(value, self, attr,
|
|
491
|
+
context: context)
|
|
464
492
|
assign_value(model, transformed)
|
|
465
493
|
end
|
|
466
494
|
true
|
|
@@ -17,16 +17,28 @@ module Lutaml
|
|
|
17
17
|
# LAZY_EMPTY_COLLECTION, scalars share the UninitializedClass
|
|
18
18
|
# singleton. Compiled once instead of walked per instance — the
|
|
19
19
|
# grammars-compile-don't-interpret rule applied to object state.
|
|
20
|
-
# Compile (per class, on demand) the method that
|
|
21
|
-
# attribute with its "no data arrived" sentinel:
|
|
22
|
-
# the frozen LAZY_EMPTY_COLLECTION, scalars
|
|
23
|
-
# UninitializedClass singleton. Compiled once instead
|
|
24
|
-
# per instance — grammars compile, they don't
|
|
25
|
-
|
|
26
|
-
|
|
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)
|
|
27
39
|
|
|
28
40
|
if attrs.empty?
|
|
29
|
-
define_method(
|
|
41
|
+
define_method(method_name) do
|
|
30
42
|
# no attributes to seed
|
|
31
43
|
end
|
|
32
44
|
return
|
|
@@ -41,28 +53,57 @@ module Lutaml
|
|
|
41
53
|
end.join("\n")
|
|
42
54
|
|
|
43
55
|
# class_eval interpolates per-attribute `@name = <sentinel>` lines:
|
|
44
|
-
# def
|
|
56
|
+
# def __init_state_defaults_default
|
|
45
57
|
# @id = Lutaml::Model::UninitializedClass.instance
|
|
46
58
|
# @items = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
|
|
47
59
|
# end
|
|
48
60
|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
49
|
-
def
|
|
61
|
+
def #{method_name}
|
|
50
62
|
#{lines}
|
|
51
63
|
end
|
|
52
64
|
RUBY
|
|
53
65
|
end
|
|
54
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
|
+
|
|
55
96
|
def invalidate_state_defaults!
|
|
56
97
|
# Opal's method_defined? takes no inherit flag (see the
|
|
57
98
|
# setter_defined check in define_regular_attribute_methods).
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
107
|
end
|
|
67
108
|
|
|
68
109
|
def define_attribute_methods(attr, register = nil)
|
|
@@ -159,35 +200,54 @@ module Lutaml
|
|
|
159
200
|
#
|
|
160
201
|
# @param name [Symbol] The attribute name
|
|
161
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
|
+
|
|
162
208
|
def define_regular_attribute_methods(name, attr)
|
|
163
|
-
|
|
164
|
-
|
|
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.
|
|
165
218
|
if attr.collection?
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
177
238
|
end
|
|
178
|
-
|
|
239
|
+
RUBY
|
|
179
240
|
else
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
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
|
|
189
249
|
end
|
|
190
|
-
|
|
250
|
+
RUBY
|
|
191
251
|
end
|
|
192
252
|
|
|
193
253
|
enum_shorthand_names = instance_variable_get(:@__enum_shorthand_names__) || Set.new
|
|
@@ -199,35 +259,50 @@ module Lutaml
|
|
|
199
259
|
else
|
|
200
260
|
method_defined?(:"#{name}=", false)
|
|
201
261
|
end
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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) &&
|
|
214
289
|
(value.nil? || Lutaml::Model::Utils.uninitialized?(value))
|
|
215
290
|
# Sentinel stays — no allocation for truly empty collections
|
|
216
291
|
else
|
|
217
|
-
|
|
292
|
+
@#{name} = value
|
|
218
293
|
end
|
|
219
|
-
|
|
220
|
-
# number of <name> elements that will be emitted.
|
|
221
|
-
record_mutation_collection(name, value)
|
|
294
|
+
record_mutation_collection(:#{name}, value)
|
|
222
295
|
end
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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)
|
|
229
304
|
end
|
|
230
|
-
|
|
305
|
+
RUBY
|
|
231
306
|
end
|
|
232
307
|
end
|
|
233
308
|
|
|
@@ -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,21 +156,12 @@ module Lutaml
|
|
|
108
156
|
@using_default = nil
|
|
109
157
|
@lutaml_register = register
|
|
110
158
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
klass = self.class
|
|
118
|
-
klass.compile_state_defaults! unless klass.method_defined?(:__init_deserialized_state_defaults)
|
|
119
|
-
__init_deserialized_state_defaults
|
|
120
|
-
else
|
|
121
|
-
self.class.attributes(register).each do |name, attr|
|
|
122
|
-
instance_variable_set(:"@#{name}",
|
|
123
|
-
attr.collection? ? LAZY_EMPTY_COLLECTION : Lutaml::Model::UninitializedClass.instance)
|
|
124
|
-
end
|
|
125
|
-
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)))
|
|
126
165
|
end
|
|
127
166
|
|
|
128
167
|
# Complete deserialization initialization after allocation.
|