lutaml-model 0.8.37 → 0.8.39
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/docs/_guides/custom-methods.adoc +71 -0
- data/docs/_guides/index.adoc +1 -0
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/key_value/transform.rb +51 -5
- data/lib/lutaml/key_value/transformation.rb +10 -2
- data/lib/lutaml/model/attribute.rb +5 -1
- data/lib/lutaml/model/compiled_rule.rb +9 -0
- data/lib/lutaml/model/format_registry.rb +3 -1
- data/lib/lutaml/model/mapping/mapping_rule.rb +51 -11
- data/lib/lutaml/model/serialize/attribute_definition.rb +143 -65
- data/lib/lutaml/model/serialize/deserialization_context.rb +1 -0
- data/lib/lutaml/model/serialize.rb +66 -17
- data/lib/lutaml/model/services/transformer.rb +30 -11
- data/lib/lutaml/model/type/string.rb +4 -1
- 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 +181 -53
- data/lib/lutaml/xml/parse_session.rb +24 -11
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -7
- 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 +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15f9489eb4db353b481f160703e848e35b730cef179d32874941fc537ed962d3
|
|
4
|
+
data.tar.gz: f0067a5ffc4d66c8312d9cc478d9bd7d1771d9b639111c3158956b2d98e4468e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f1372cbf492e141015524382a3ac98cdc5f484441d70c593b7ad67b9021bfa959732700679534bb3690f86c064e3a1a82269833c5c73794fe87af24f21d239b
|
|
7
|
+
data.tar.gz: '048d412becdba7a8a99070740663d0d0a33e88d02018708818ce816d1ba4669a1f26efbf08d0c67390818c5efede0e362ef8f1785536c4233a2fa6dadd07da61'
|
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,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
= Custom serialization methods
|
|
2
|
+
|
|
3
|
+
:toc:
|
|
4
|
+
|
|
5
|
+
Lutaml::Model maps serialization constructs (`map_element`, `map_attribute`, `map_content`, key-value `map`) to *model attributes*. When the wire shape and the model shape disagree, custom methods bridge the gap at the mapping site instead of hand-rolled `to_h`/`from_h` code.
|
|
6
|
+
|
|
7
|
+
== `with:` — per-rule custom methods
|
|
8
|
+
|
|
9
|
+
Declare both directions on the rule. The methods live on the model (instance methods); the names are symbols.
|
|
10
|
+
|
|
11
|
+
[source,ruby]
|
|
12
|
+
----
|
|
13
|
+
class Requirement
|
|
14
|
+
include Lutaml::Model::Serialize
|
|
15
|
+
|
|
16
|
+
attribute :guidance, :string
|
|
17
|
+
attribute :purpose, :string
|
|
18
|
+
|
|
19
|
+
xml do
|
|
20
|
+
element "requirement"
|
|
21
|
+
map_element "component", to: :guidance,
|
|
22
|
+
with: { from: :component_from_guidance, to: :component_to_guidance }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def component_from_guidance(model, value, ctx = nil)
|
|
26
|
+
# `from` receives (model, value[, context]); assign the model attribute
|
|
27
|
+
model.guidance = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def component_to_guidance(model, parent, doc = nil, ctx = nil)
|
|
31
|
+
# `to` receives (model, parent[, doc[, context]]); write into parent/doc
|
|
32
|
+
parent.add_element("component", model.guidance, type: "guidance")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
----
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
|
|
39
|
+
* The `from` method *assigns* the attribute (it receives the whole model); the `to` method *writes* the output document.
|
|
40
|
+
* The optional trailing `context` parameter receives the options hash passed to `Model.from_xml(data, context: {...})` / `model.to_xml(context: {...})` — it is forwarded when the method declares it (lutaml-model#550).
|
|
41
|
+
* Callables work in key-value mappings: `with: { from: ->(value) { ... } }` maps and assigns the return value; an exact-arity-2 callable receives the context as its second argument.
|
|
42
|
+
|
|
43
|
+
== `transform:` — value-level transforms
|
|
44
|
+
|
|
45
|
+
`transform:` converts values (not documents). A Hash form (`transform: { from: ->(v) {...}, to: ->(v) {...} }`) wraps the value in both directions without touching assignment; a `ValueTransformer` subclass receives `from(value, format)` / `to(value, format)` and is shared by every rule that references it.
|
|
46
|
+
|
|
47
|
+
== Type-level serialization
|
|
48
|
+
|
|
49
|
+
A *type class* (a `Lutaml::Model::Type::Value` subclass) can carry its own format behavior, so reuse lives on the type rather than the parent mapping:
|
|
50
|
+
|
|
51
|
+
[source,ruby]
|
|
52
|
+
----
|
|
53
|
+
class XmiNsType < Lutaml::Model::Type::String
|
|
54
|
+
xml do
|
|
55
|
+
namespace XmiNs # bind the type's own namespace
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
----
|
|
59
|
+
|
|
60
|
+
`Lutaml::Model::Type::Value.register_format_type_serializer(format, type_class, to:, from:)` registers per-format serialize/deserialize procs for a type class; resolution walks the hierarchy.
|
|
61
|
+
|
|
62
|
+
== `of_*` and the conversion entry points
|
|
63
|
+
|
|
64
|
+
`Model.of_<format>(parsed_document, options)` is the documented seam between adapter parsing and mapping application: the adapter (`from_<format>`) parses the wire data into a document object, and `of_<format>` (class method) applies the mappings to build instances. It is public API for tooling that already holds a parsed document — e.g. re-applying mappings to a reparsed tree — and is what `from_<format>` delegates to.
|
|
65
|
+
|
|
66
|
+
The per-instance counterparts are `to_<format>` (serialize through the mappings) and `as_<format>` (render without register-specific side effects). Prefer the generated `from_*/to_*` methods; reach for `of_*` only when you interpose between parse and mapping.
|
|
67
|
+
|
|
68
|
+
== Common recipes
|
|
69
|
+
|
|
70
|
+
* One wire element, several attributes dispatched by an attribute value (`component/@type`): one `with:` pair per attribute whose `from` inspects the dispatch attribute and whose `to` emits only for its type. (Polymorphic *class* dispatch has first-class support via `polymorphic:`.)
|
|
71
|
+
* Key remapping, nesting shifts, delimiter splitting: prefer `transform:`, `child_mappings`, and `root`/`prefix` options before custom methods — the mapping stays declarative and reviewable.
|
data/docs/_guides/index.adoc
CHANGED
|
@@ -19,6 +19,7 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
|
|
|
19
19
|
|
|
20
20
|
== Key-value serialization
|
|
21
21
|
|
|
22
|
+
* link:../custom-methods[Custom Serialization Methods] - with:/transform:, type-level serialization, of_* entry points
|
|
22
23
|
* link:../keyvalue-serialization[Key-Value Serialization] - JSON/YAML/TOML/Hash
|
|
23
24
|
* link:../collection-serialization[Collection Serialization] - JSONL and YAML Stream
|
|
24
25
|
|
|
@@ -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,8 @@ format)
|
|
|
261
286
|
# only nil (non-existent) skips it (lutaml-model#746).
|
|
262
287
|
return if value.nil?
|
|
263
288
|
|
|
264
|
-
return rule.deserialize(instance, value, attributes, model_class
|
|
289
|
+
return rule.deserialize(instance, value, attributes, model_class,
|
|
290
|
+
options[:context])
|
|
265
291
|
end
|
|
266
292
|
|
|
267
293
|
value = rule.transform_value(attr, value, :from, format)
|
|
@@ -279,7 +305,8 @@ format)
|
|
|
279
305
|
if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
|
|
280
306
|
attr.valid_collection!(value, context)
|
|
281
307
|
end
|
|
282
|
-
rule.deserialize(instance, value, attributes, self
|
|
308
|
+
rule.deserialize(instance, value, attributes, self,
|
|
309
|
+
options[:context])
|
|
283
310
|
end
|
|
284
311
|
|
|
285
312
|
# Compiled rule plans (TODO.perf/07): for plain scalar rules the
|
|
@@ -291,6 +318,25 @@ format)
|
|
|
291
318
|
# unions, polymorphism) return nil and keep the interpretive path.
|
|
292
319
|
KvRulePlan = ::Struct.new(:wire, :klass, :collection)
|
|
293
320
|
|
|
321
|
+
# Memoized (per transform) wire name for plain rules — the from
|
|
322
|
+
# spelling never changes after definition.
|
|
323
|
+
KvSerializePlan = ::Struct.new(:wire)
|
|
324
|
+
|
|
325
|
+
def kv_serialize_plan(rule, attr, _instance)
|
|
326
|
+
return nil if rule.delegate || rule.raw_mapping? || rule.root_mapping? ||
|
|
327
|
+
rule.hash_mappings || rule.child_mappings ||
|
|
328
|
+
rule.has_custom_method_for_serialization? ||
|
|
329
|
+
rule.multiple_mappings? || polymorphic_rule?(rule) ||
|
|
330
|
+
(rule.transform.is_a?(Hash) && !rule.transform.empty?) ||
|
|
331
|
+
rule.transform.is_a?(Class) || attr.nil? || attr.derived? ||
|
|
332
|
+
attr.union? || attr.polymorphic? || attr.custom_collection? ||
|
|
333
|
+
attr.transform ||
|
|
334
|
+
rule.value_map(:to) != Lutaml::Model::Serialize::DEFAULT_VALUE_MAP
|
|
335
|
+
|
|
336
|
+
@kv_serialize_plans ||= {}.compare_by_identity
|
|
337
|
+
@kv_serialize_plans[rule] ||= KvSerializePlan.new(rule_from_name(rule))
|
|
338
|
+
end
|
|
339
|
+
|
|
294
340
|
def kv_rule_plan(format, rule, attr)
|
|
295
341
|
@kv_rule_plans ||= {}
|
|
296
342
|
@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
|
|
@@ -208,6 +208,15 @@ module Lutaml
|
|
|
208
208
|
# @param args [Array] Arguments (ignored for options)
|
|
209
209
|
# @param block [Proc] Block (ignored for options)
|
|
210
210
|
# @return [Object] The option value or nil
|
|
211
|
+
# Static per rule (see MappingRule#custom_method_only?). Compiled
|
|
212
|
+
# rules are frozen, so the answer is recomputed with a single
|
|
213
|
+
# to_s instead of the historical two.
|
|
214
|
+
def custom_method_only?
|
|
215
|
+
name = attribute_name.to_s
|
|
216
|
+
(name.start_with?("__") && name.end_with?("__")) ||
|
|
217
|
+
(!custom_methods.empty? && attribute_type.nil?)
|
|
218
|
+
end
|
|
219
|
+
|
|
211
220
|
def method_missing(method_name, *args, &block)
|
|
212
221
|
# Check if this is an option key
|
|
213
222
|
if options.key?(method_name)
|
|
@@ -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,16 +296,29 @@ 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
|
|
|
310
|
+
# Static per rule: placeholder `__name__` targets and inferred
|
|
311
|
+
# custom-method targets without an attribute type never change —
|
|
312
|
+
# the applier asked this per element and paid two to_s strings
|
|
313
|
+
# each time (240k allocations on the ISO-13849 serialize profile).
|
|
314
|
+
# Rules may be frozen, so no per-instance memo: one to_s instead
|
|
315
|
+
# of two is the frozen-safe diet.
|
|
316
|
+
def custom_method_only?
|
|
317
|
+
name = attribute_name.to_s
|
|
318
|
+
(name.start_with?("__") && name.end_with?("__")) ||
|
|
319
|
+
(has_custom_methods? && attribute_type.nil?)
|
|
320
|
+
end
|
|
321
|
+
|
|
302
322
|
def has_custom_method_for_serialization?
|
|
303
323
|
!custom_methods.empty? && custom_methods[:to]
|
|
304
324
|
end
|
|
@@ -416,10 +436,29 @@ module Lutaml
|
|
|
416
436
|
end
|
|
417
437
|
end
|
|
418
438
|
|
|
419
|
-
def handle_custom_method(model, value, mapper_class)
|
|
420
|
-
|
|
439
|
+
def handle_custom_method(model, value, mapper_class, context = nil)
|
|
440
|
+
custom = custom_methods[:from]
|
|
441
|
+
return if !custom || value.nil?
|
|
421
442
|
|
|
422
|
-
|
|
443
|
+
if custom.is_a?(String) || custom.is_a?(Symbol)
|
|
444
|
+
target = mapper_class.new
|
|
445
|
+
# lutaml-model#550: custom methods may declare a third context
|
|
446
|
+
# parameter to receive the options passed to `from_*`.
|
|
447
|
+
if target.method(custom).parameters.size >= 3
|
|
448
|
+
target.public_send(custom, model, value, context)
|
|
449
|
+
else
|
|
450
|
+
target.public_send(custom, model, value)
|
|
451
|
+
end
|
|
452
|
+
else
|
|
453
|
+
# Callable (proc/lambda): transforms the value and the rule
|
|
454
|
+
# assigns it. Exact arity 2 receives the `from_*` context.
|
|
455
|
+
transformed = if custom.arity == 2
|
|
456
|
+
custom.call(value, context)
|
|
457
|
+
else
|
|
458
|
+
custom.call(value)
|
|
459
|
+
end
|
|
460
|
+
assign_value(model, transformed)
|
|
461
|
+
end
|
|
423
462
|
true
|
|
424
463
|
end
|
|
425
464
|
|
|
@@ -439,7 +478,7 @@ module Lutaml
|
|
|
439
478
|
attributes[delegate].type(model.lutaml_register).new)
|
|
440
479
|
end
|
|
441
480
|
|
|
442
|
-
def handle_transform_method(model, value, attributes)
|
|
481
|
+
def handle_transform_method(model, value, attributes, context = nil)
|
|
443
482
|
attr = attributes[to]
|
|
444
483
|
# Fast path: no transforms at all (covers 95%+ of rules)
|
|
445
484
|
# transform defaults to {} which is truthy but semantically empty
|
|
@@ -460,7 +499,8 @@ module Lutaml
|
|
|
460
499
|
assign_value(model, value)
|
|
461
500
|
else
|
|
462
501
|
# Hash/proc transformers need ImportTransformer
|
|
463
|
-
transformed = ImportTransformer.call(value, self, attr
|
|
502
|
+
transformed = ImportTransformer.call(value, self, attr,
|
|
503
|
+
context: context)
|
|
464
504
|
assign_value(model, transformed)
|
|
465
505
|
end
|
|
466
506
|
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,53 @@ 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
|
+
reader_defined = if Lutaml::Model.opal?
|
|
268
|
+
method_defined?(attr_reader_method)
|
|
269
|
+
else
|
|
270
|
+
method_defined?(attr_reader_method, false)
|
|
271
|
+
end
|
|
272
|
+
define_method(attr_reader_method) { attr } unless reader_defined
|
|
273
|
+
|
|
274
|
+
if attr.collection?
|
|
275
|
+
# class_eval interpolates, e.g.:
|
|
276
|
+
# def items=(value)
|
|
277
|
+
# value_set_for(:items)
|
|
278
|
+
# value = ATTR.cast_value(value, lutaml_register)
|
|
279
|
+
# current = @items
|
|
280
|
+
# ... sentinel preservation ...
|
|
281
|
+
# record_mutation_collection(:items, value)
|
|
282
|
+
# end
|
|
283
|
+
# The compiled body writes @items directly: the define_method
|
|
284
|
+
# form interpolated "@items" name strings per call — one per
|
|
285
|
+
# setter invocation on instance-heavy parses (TODO.max-perf/06).
|
|
286
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
287
|
+
def #{name}=(value)
|
|
288
|
+
value_set_for(:#{name})
|
|
289
|
+
value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
|
|
290
|
+
current = @#{name}
|
|
291
|
+
if current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
|
|
214
292
|
(value.nil? || Lutaml::Model::Utils.uninitialized?(value))
|
|
215
293
|
# Sentinel stays — no allocation for truly empty collections
|
|
216
294
|
else
|
|
217
|
-
|
|
295
|
+
@#{name} = value
|
|
218
296
|
end
|
|
219
|
-
|
|
220
|
-
# number of <name> elements that will be emitted.
|
|
221
|
-
record_mutation_collection(name, value)
|
|
297
|
+
record_mutation_collection(:#{name}, value)
|
|
222
298
|
end
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
299
|
+
RUBY
|
|
300
|
+
else
|
|
301
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
302
|
+
def #{name}=(value)
|
|
303
|
+
value_set_for(:#{name})
|
|
304
|
+
value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
|
|
305
|
+
@#{name} = value
|
|
306
|
+
record_mutation(:#{name}, value)
|
|
229
307
|
end
|
|
230
|
-
|
|
308
|
+
RUBY
|
|
231
309
|
end
|
|
232
310
|
end
|
|
233
311
|
|