lutaml-model 0.8.56 → 0.8.106
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/.github/scripts/test_installed_gem.rb +58 -0
- data/.github/workflows/downstream-performance.yml +28 -3
- data/.github/workflows/js-pr-check.yml +4 -0
- data/.github/workflows/js-sync.yml +4 -0
- data/.github/workflows/opal.yml +9 -0
- data/.github/workflows/rake.yml +6 -41
- data/.github/workflows/release.yml +41 -0
- data/Gemfile +17 -3
- data/bench/bench_alloc_audit.rb +56 -0
- data/bench/bench_common.rb +11 -0
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/gem-integration.adoc +85 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +8 -1
- data/docs/_guides/native-engines.adoc +23 -5
- data/docs/_guides/schema-generation.adoc +25 -0
- data/docs/_guides/type-registers-contexts.adoc +126 -0
- data/lib/compat/opal/lutaml_model_boot.rb +7 -5
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
- data/lib/lutaml/key_value/data_model/element.rb +5 -1
- data/lib/lutaml/key_value/mapping_rule.rb +7 -0
- data/lib/lutaml/key_value/transform.rb +29 -24
- data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
- data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
- data/lib/lutaml/key_value/transformation.rb +64 -116
- data/lib/lutaml/model/adapter_resolver.rb +7 -3
- data/lib/lutaml/model/attribute.rb +74 -15
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
- data/lib/lutaml/model/render_policy.rb +26 -8
- data/lib/lutaml/model/schema/base_schema.rb +15 -0
- data/lib/lutaml/model/schema/generator/definition.rb +52 -8
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
- data/lib/lutaml/model/schema.rb +4 -2
- data/lib/lutaml/model/serializable.rb +33 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
- data/lib/lutaml/model/serialize/initialization.rb +54 -7
- data/lib/lutaml/model/serialize/model_import.rb +60 -0
- data/lib/lutaml/model/serialize.rb +14 -6
- data/lib/lutaml/model/type/time.rb +6 -0
- data/lib/lutaml/model/type/value.rb +9 -7
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/utils.rb +18 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
- data/lib/lutaml/xml/adapter_loader.rb +5 -2
- data/lib/lutaml/xml/format.rb +18 -16
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
- data/lib/lutaml/xml/leptris.rb +46 -0
- data/lib/lutaml/xml/mapping.rb +71 -18
- data/lib/lutaml/xml/model_transform.rb +231 -8
- data/lib/lutaml/xml/namespace.rb +18 -8
- data/lib/lutaml/xml/namespace_collector.rb +17 -1
- data/lib/lutaml/xml/namespace_needs.rb +77 -49
- data/lib/lutaml/xml/parse_session.rb +7 -19
- data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
- data/lib/lutaml/xml/transformation.rb +15 -1
- data/lib/lutaml/xml/type/serializers.rb +8 -1
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/lutaml-model.gemspec +7 -4
- data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
- data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
- data/spec/lutaml/key_value/transformation_spec.rb +87 -21
- data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
- data/spec/lutaml/model/collection_validation_spec.rb +265 -0
- data/spec/lutaml/model/desc_spec.rb +50 -0
- data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
- data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
- data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
- data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
- data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
- data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
- data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
- data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
- data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
- data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
- data/spec/person_spec.rb +8 -0
- metadata +48 -17
- data/lib/lutaml/xml/plan_compiler.rb +0 -332
- data/lib/lutaml/xml/plan_hydrator.rb +0 -367
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -252
- data/lib/lutaml/xml/plan_walk.rb +0 -30
|
@@ -67,11 +67,22 @@ When the XML adapter resolves to leptris, XML (de)serialization
|
|
|
67
67
|
compiles the model's mapping into a leptris descriptor plan and runs
|
|
68
68
|
the whole document through one native pass — measured 6.7x parse and
|
|
69
69
|
4x serialize with -76% / -80% allocations on the plan-path corpus.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
Namespace-qualified models ride the path too: serialization spells
|
|
71
|
+
prefixed element and attribute names through the same namespace
|
|
72
|
+
planning the interpretive writer uses, and every `(prefix, uri)`
|
|
73
|
+
pair is declared once on the output root. Models the plan cannot
|
|
74
|
+
express exactly (same-name discriminator partitions, deferred-rule
|
|
75
|
+
shapes) and shapes with custom semantics (custom methods,
|
|
76
|
+
polymorphism, unions, ordered children) hydrate through the
|
|
77
|
+
interpretive pipeline or a per-subtree fragment of it, unchanged.
|
|
78
|
+
|
|
79
|
+
Version floor note: leptris 1.9.320+ retains child-row namespace
|
|
80
|
+
URIs engine-side and matches plain attribute rows against
|
|
81
|
+
namespace-qualified wire spellings. On older engines, models whose
|
|
82
|
+
plans carry exact-URI child rows or plain attribute rows under a
|
|
83
|
+
namespaced chain parse interpretively for correctness — a per-model
|
|
84
|
+
fallback, not an error. Bundles resolving leptris 1.9.320+ get the
|
|
85
|
+
full fast path.
|
|
75
86
|
|
|
76
87
|
The path is on by default and is a no-op without leptris (standard
|
|
77
88
|
adapters keep the classic pipeline). Opt out for the whole process:
|
|
@@ -106,6 +117,13 @@ Lutaml::Model::Config.toml_adapter_type = :teptris
|
|
|
106
117
|
raising `Psych::DisallowedClass`.
|
|
107
118
|
* **JSON**: `yeptris` parsing targets exact `JSON.parse` semantics;
|
|
108
119
|
generation stays on the `json` gem.
|
|
120
|
+
* **Error classes**: parse failures keep the standard adapters' contract
|
|
121
|
+
on every engine — the yeptris adapters re-raise the engine's syntax
|
|
122
|
+
errors as `Psych::SyntaxError` (YAML, with the engine's line, column,
|
|
123
|
+
and problem text) and `JSON::ParserError` (JSON), and the model layer
|
|
124
|
+
wraps them as `InvalidFormatError` just as it does for Psych and the
|
|
125
|
+
`json` gem. Code that rescues those classes does not change when a
|
|
126
|
+
native engine is added to the bundle.
|
|
109
127
|
* **TOML**: the teptris adapter mirrors tomlib value semantics — offset
|
|
110
128
|
and local datetimes become `Time`, dates become `Date`, local times
|
|
111
129
|
stay `String`. Parse failures raise `Teptris::ParseError` with line
|
|
@@ -901,3 +901,28 @@ The generated schema will include validation constraints:
|
|
|
901
901
|
</define>
|
|
902
902
|
</grammar>
|
|
903
903
|
----
|
|
904
|
+
|
|
905
|
+
== Documentation metadata (desc)
|
|
906
|
+
|
|
907
|
+
Models and attributes carry documentation strings through `desc` —
|
|
908
|
+
set on the model class or on any `attribute` declaration, read back
|
|
909
|
+
by documentation and schema tooling. Serialization is never affected
|
|
910
|
+
by it.
|
|
911
|
+
|
|
912
|
+
[source,ruby]
|
|
913
|
+
----
|
|
914
|
+
class Title < Lutaml::Model::Serializable
|
|
915
|
+
desc "This is a title."
|
|
916
|
+
|
|
917
|
+
attribute :type, :string, desc: "This attribute is used for..."
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
Title.desc # => "This is a title."
|
|
921
|
+
Title.attributes[:type].desc # => "This attribute is used for..."
|
|
922
|
+
----
|
|
923
|
+
|
|
924
|
+
Subclasses inherit the nearest ancestor's class description; an
|
|
925
|
+
explicit `desc` overrides it. A consumer override-free `desc` value
|
|
926
|
+
does not change any generated schema today — wiring `desc` into the
|
|
927
|
+
generated `description` fields is deliberate product scope, not a
|
|
928
|
+
free rider on this option.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Type Registers and Contexts
|
|
3
|
+
nav_order: 20
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
= Type registers and contexts
|
|
7
|
+
|
|
8
|
+
Lutaml::Model resolves every attribute type — Symbol or class —
|
|
9
|
+
through a per-application *register*: a named table of types scoped
|
|
10
|
+
so that different class trees can share mapping definitions without
|
|
11
|
+
colliding. Registers are the mechanism behind multi-version model
|
|
12
|
+
suites and model substitution.
|
|
13
|
+
|
|
14
|
+
== Registers
|
|
15
|
+
|
|
16
|
+
A register maps type ids to classes. Classes declare the register
|
|
17
|
+
they belong to:
|
|
18
|
+
|
|
19
|
+
[source,ruby]
|
|
20
|
+
----
|
|
21
|
+
module MyModels
|
|
22
|
+
class Mi < Lutaml::Model::Serializable
|
|
23
|
+
def self.lutaml_default_register
|
|
24
|
+
:my_models
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
Symbol attribute types resolve in the declaring class's register,
|
|
31
|
+
so the same symbol can mean different classes per tree — the
|
|
32
|
+
multi-version pattern below builds on this.
|
|
33
|
+
|
|
34
|
+
== Contexts: derived registers with substitutions
|
|
35
|
+
|
|
36
|
+
A *context* is a derived type-resolution scope: it carries a
|
|
37
|
+
registry, fallback registers, and — the part that powers model
|
|
38
|
+
substitution — a list of type substitutions. Contexts are created
|
|
39
|
+
through the global registry:
|
|
40
|
+
|
|
41
|
+
[source,ruby]
|
|
42
|
+
----
|
|
43
|
+
Lutaml::Model::GlobalContext.create_context(
|
|
44
|
+
id: :custom_models,
|
|
45
|
+
fallback_to: [:my_models],
|
|
46
|
+
substitutions: [
|
|
47
|
+
{ from_type: MyModels::Mover, to_type: Overset },
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
instance = MyModels::Math.from_xml(xml, context: :custom_models)
|
|
52
|
+
instance.mover_value.first # => #<Overset>
|
|
53
|
+
----
|
|
54
|
+
|
|
55
|
+
Every attribute whose type resolves to `MyModels::Mover` — at any
|
|
56
|
+
depth of the tree — hydrates `Overset` in that context. Substitution
|
|
57
|
+
is type-based, so "swap this class everywhere it is referenced" and
|
|
58
|
+
"swap it only inside this one path" are the same operation scoped by
|
|
59
|
+
context: create two contexts with different substitution lists.
|
|
60
|
+
|
|
61
|
+
Contexts are cheap and disposable:
|
|
62
|
+
|
|
63
|
+
[source,ruby]
|
|
64
|
+
----
|
|
65
|
+
Lutaml::Model::GlobalContext.unregister_context(:custom_models)
|
|
66
|
+
----
|
|
67
|
+
|
|
68
|
+
=== The custom-models convenience
|
|
69
|
+
|
|
70
|
+
A whole-table convenience reads exactly like a class-swap hash and
|
|
71
|
+
derives the context for you:
|
|
72
|
+
|
|
73
|
+
[source,ruby]
|
|
74
|
+
----
|
|
75
|
+
models = { MyModels::Mover => Overset, MyModels::Mi => BoldMi }
|
|
76
|
+
Lutaml::Model::GlobalContext.create_context(
|
|
77
|
+
id: :custom, fallback_to: [:my_models],
|
|
78
|
+
substitutions: models.map { |from, to| { from_type: from, to_type: to } },
|
|
79
|
+
)
|
|
80
|
+
MyModels::Math.from_xml(xml, context: :custom)
|
|
81
|
+
----
|
|
82
|
+
|
|
83
|
+
Substitute classes should inherit from the class they replace (or
|
|
84
|
+
provide its full attribute surface) so mappings and serialization
|
|
85
|
+
keep working.
|
|
86
|
+
|
|
87
|
+
=== Substituting a symbol entry instead of a class
|
|
88
|
+
|
|
89
|
+
Registers accept direct class registrations under a type id — useful
|
|
90
|
+
when a tree declares `attribute :row, :row` and you want the symbol
|
|
91
|
+
to resolve to your own class within a derived register:
|
|
92
|
+
|
|
93
|
+
[source,ruby]
|
|
94
|
+
----
|
|
95
|
+
derived = Lutaml::Model::Register.new(:derived, fallback: [:my_models])
|
|
96
|
+
derived.register_model(NewRow, id: :row)
|
|
97
|
+
MyModels::Doc.from_xml(xml, register: derived)
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
(`register:` is the pre-context spelling; prefer `context:` with
|
|
101
|
+
substitutions in new code.)
|
|
102
|
+
|
|
103
|
+
== Multi-version class trees
|
|
104
|
+
|
|
105
|
+
The flagship pattern combines registers with per-version classes
|
|
106
|
+
and shared mapping modules:
|
|
107
|
+
|
|
108
|
+
* a `Base::Mrow` module holds the mapping DSL once;
|
|
109
|
+
* each version declares `class Mrow` mixing the module in and
|
|
110
|
+
pointing `lutaml_default_register` at its version's register;
|
|
111
|
+
* attributes use symbol types, so `attribute :mrow, :mrow` means
|
|
112
|
+
the right class per tree;
|
|
113
|
+
* a substitution context swaps individual classes for one call
|
|
114
|
+
without re-opening or redefining anything.
|
|
115
|
+
|
|
116
|
+
Nothing re-opens generated classes, and each version's tree stays
|
|
117
|
+
independent — no hidden delegation.
|
|
118
|
+
|
|
119
|
+
== Rules of thumb
|
|
120
|
+
|
|
121
|
+
* Prefer contexts over global configuration: substitutions scoped
|
|
122
|
+
to a context cannot leak into other resolutions.
|
|
123
|
+
* Substitute classes should inherit the original; substitution then
|
|
124
|
+
preserves every mapping declared on the base.
|
|
125
|
+
* Clean up exploratory contexts with `unregister_context` in test
|
|
126
|
+
teardowns — contexts are global state.
|
|
@@ -147,6 +147,7 @@ require "lutaml/xml/format"
|
|
|
147
147
|
require "lutaml/xml/format_chooser"
|
|
148
148
|
require "lutaml/xml/hoisting_algorithm"
|
|
149
149
|
require "lutaml/xml/html_entities"
|
|
150
|
+
require "lutaml/xml/leptris"
|
|
150
151
|
require "lutaml/xml/listener"
|
|
151
152
|
require "lutaml/xml/mapping"
|
|
152
153
|
require "lutaml/xml/mapping_rule"
|
|
@@ -170,11 +171,6 @@ require "lutaml/xml/oga"
|
|
|
170
171
|
require "lutaml/xml/parse_session"
|
|
171
172
|
require "lutaml/xml/parsed_namespace_declaration"
|
|
172
173
|
require "lutaml/xml/parsed_namespace_set"
|
|
173
|
-
require "lutaml/xml/plan_compiler"
|
|
174
|
-
require "lutaml/xml/plan_hydrator"
|
|
175
|
-
require "lutaml/xml/plan_order"
|
|
176
|
-
require "lutaml/xml/plan_serializer"
|
|
177
|
-
require "lutaml/xml/plan_walk"
|
|
178
174
|
require "lutaml/xml/polymorphic_value_handler"
|
|
179
175
|
require "lutaml/xml/qualified_inheritance_strategy"
|
|
180
176
|
require "lutaml/xml/rexml"
|
|
@@ -386,6 +382,12 @@ require "lutaml/xml/error/namespace_mismatch_error"
|
|
|
386
382
|
require "lutaml/xml/error/schema_validation_error"
|
|
387
383
|
require "lutaml/xml/error/xml_configuration_error"
|
|
388
384
|
require "lutaml/xml/error/xml_error"
|
|
385
|
+
require "lutaml/xml/leptris/native_emission"
|
|
386
|
+
require "lutaml/xml/leptris/plan_compiler"
|
|
387
|
+
require "lutaml/xml/leptris/plan_hydrator"
|
|
388
|
+
require "lutaml/xml/leptris/plan_order"
|
|
389
|
+
require "lutaml/xml/leptris/plan_serializer"
|
|
390
|
+
require "lutaml/xml/leptris/plan_walk"
|
|
389
391
|
require "lutaml/xml/oga/element"
|
|
390
392
|
require "lutaml/xml/ox/element"
|
|
391
393
|
require "lutaml/xml/rexml/element"
|
|
@@ -12,9 +12,32 @@ module Lutaml
|
|
|
12
12
|
# load-only), so everything else inherits from the standard adapter.
|
|
13
13
|
class YeptrisAdapter < StandardAdapter
|
|
14
14
|
def self.parse(json, _options = {})
|
|
15
|
-
|
|
15
|
+
require_engine
|
|
16
16
|
::Yeptris::JSON.load(json)
|
|
17
|
+
rescue ::Yeptris::JSON::ParseError => e
|
|
18
|
+
raise parser_error(e)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The engine require sits outside parse's rescue so a broken
|
|
22
|
+
# install still surfaces its LoadError (the detection fallback's
|
|
23
|
+
# contract) instead of a NameError from the rescue clause.
|
|
24
|
+
def self.require_engine
|
|
25
|
+
require "yeptris"
|
|
26
|
+
end
|
|
27
|
+
private_class_method :require_engine
|
|
28
|
+
|
|
29
|
+
# The parse-error contract is the standard adapter's: invalid
|
|
30
|
+
# input raises JSON::ParserError regardless of the engine, so
|
|
31
|
+
# consumers' rescue ladders (and this gem's own
|
|
32
|
+
# format_error_types conversion to InvalidFormatError) hold on
|
|
33
|
+
# every adapter. Without the json gem the engine error
|
|
34
|
+
# propagates unchanged.
|
|
35
|
+
def self.parser_error(error)
|
|
36
|
+
return error unless defined?(::JSON::ParserError)
|
|
37
|
+
|
|
38
|
+
::JSON::ParserError.new(error.message)
|
|
17
39
|
end
|
|
40
|
+
private_class_method :parser_error
|
|
18
41
|
end
|
|
19
42
|
end
|
|
20
43
|
end
|
|
@@ -193,7 +193,11 @@ module Lutaml
|
|
|
193
193
|
#
|
|
194
194
|
# @return [Hash]
|
|
195
195
|
def children_to_hash
|
|
196
|
-
|
|
196
|
+
# Built in place: map+reduce(:merge) allocated the mapped
|
|
197
|
+
# array plus one merged hash per reduce step (TODO.perf 16).
|
|
198
|
+
h = {}
|
|
199
|
+
@children.each { |child| h.update(child.to_hash) }
|
|
200
|
+
h
|
|
197
201
|
end
|
|
198
202
|
|
|
199
203
|
# Convert children to Array
|
|
@@ -83,6 +83,13 @@ module Lutaml
|
|
|
83
83
|
def root_mapping?
|
|
84
84
|
name == "root_mapping"
|
|
85
85
|
end
|
|
86
|
+
|
|
87
|
+
# The writer symbol for this rule's target, interned once per
|
|
88
|
+
# rule: the interpolation allocated a String and interned it per
|
|
89
|
+
# row per document in the transform's hot loop.
|
|
90
|
+
def kv_setter
|
|
91
|
+
@kv_setter ||= :"#{to}="
|
|
92
|
+
end
|
|
86
93
|
end
|
|
87
94
|
end
|
|
88
95
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module Lutaml
|
|
2
2
|
module KeyValue
|
|
3
|
+
MISSING = Object.new.freeze
|
|
3
4
|
class Transform < Lutaml::Model::Transform
|
|
4
5
|
def data_to_model(data, format, options = {})
|
|
5
6
|
# Use child's own default register if it has one
|
|
@@ -55,13 +56,9 @@ module Lutaml
|
|
|
55
56
|
# transformation_for returns nil for cyclic dependencies or :building sentinel
|
|
56
57
|
# Fall back to legacy approach in these cases
|
|
57
58
|
if transformation.is_a?(Lutaml::KeyValue::Transformation)
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# The to_hash method returns {"__root__" => {actual_hash}}
|
|
62
|
-
kv_hash = kv_element.to_hash
|
|
63
|
-
# For root element, return just the content hash
|
|
64
|
-
return kv_hash["__root__"] || kv_hash
|
|
59
|
+
# transform emits the output hash directly (TODO.perf 19) —
|
|
60
|
+
# the __root__ Element tree and its to_hash flatten are gone.
|
|
61
|
+
return transformation.transform(instance, options)
|
|
65
62
|
end
|
|
66
63
|
end
|
|
67
64
|
|
|
@@ -176,18 +173,21 @@ module Lutaml
|
|
|
176
173
|
.substitution_for(type).any?
|
|
177
174
|
|
|
178
175
|
if type.is_a?(Class) && type.include?(Lutaml::Model::Serialize)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
child_register = Lutaml::Model::Register
|
|
177
|
+
.resolve_for_child(type, register)
|
|
178
|
+
child_rows = kv_group_plan(type, format, child_register)
|
|
182
179
|
return false unless child_rows
|
|
183
180
|
|
|
184
181
|
rows ||= []
|
|
185
|
-
|
|
182
|
+
# child_register rides the row: the plan was BUILT under it,
|
|
183
|
+
# and rebuilding the resolution per model row per document
|
|
184
|
+
# cost a Concurrent::Map hop each time (TODO.perf 14).
|
|
185
|
+
rows << [rule, attr, :model, type, child_rows, child_register]
|
|
186
186
|
elsif type.is_a?(Class) && type < Lutaml::Model::Type::Value &&
|
|
187
187
|
!attr.value_policy.whole_value?(type) &&
|
|
188
188
|
!Lutaml::Model::Attribute.custom_from_probe?(type)
|
|
189
189
|
rows ||= []
|
|
190
|
-
rows << [rule, attr, :scalar, nil, nil]
|
|
190
|
+
rows << [rule, attr, :scalar, nil, nil, nil]
|
|
191
191
|
else
|
|
192
192
|
return false
|
|
193
193
|
end
|
|
@@ -206,21 +206,24 @@ module Lutaml
|
|
|
206
206
|
def kv_group_build(rows, doc, format, register, options)
|
|
207
207
|
setters = []
|
|
208
208
|
children = []
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
# Lazy: the present-key hot path never pushes, and this runs
|
|
210
|
+
# once per hydrated model instance.
|
|
211
|
+
absent = nil
|
|
212
|
+
rows.each do |rule, attr, kind, type, child_rows, child_register|
|
|
213
|
+
# One hash pass per row: fetch with a sentinel distinguishes
|
|
214
|
+
# absent keys from present-nil values (both read as nil), so
|
|
215
|
+
# the separate string_or_symbol_key? probe goes away.
|
|
216
|
+
v = Lutaml::Model::Utils.fetch_str_or_sym(doc, rule.name, MISSING)
|
|
217
|
+
if v.equal?(MISSING)
|
|
218
|
+
(absent ||= []) << rule
|
|
213
219
|
next
|
|
214
220
|
end
|
|
215
|
-
v = Lutaml::Model::Utils.fetch_str_or_sym(doc, rule.name)
|
|
216
221
|
|
|
217
222
|
if kind == :scalar
|
|
218
|
-
setters << [
|
|
223
|
+
setters << [rule.kv_setter, v]
|
|
219
224
|
next
|
|
220
225
|
end
|
|
221
226
|
|
|
222
|
-
child_register = Lutaml::Model::Register.resolve_for_child(type,
|
|
223
|
-
register)
|
|
224
227
|
if attr.collection?
|
|
225
228
|
# A present-but-nil collection reaches the per-rule walk,
|
|
226
229
|
# which owns the sentinel interplay for that edge
|
|
@@ -239,7 +242,7 @@ module Lutaml
|
|
|
239
242
|
|
|
240
243
|
built << child
|
|
241
244
|
end
|
|
242
|
-
setters << [
|
|
245
|
+
setters << [rule.kv_setter, built]
|
|
243
246
|
children.concat(built)
|
|
244
247
|
else
|
|
245
248
|
return nil unless v.is_a?(::Hash)
|
|
@@ -249,13 +252,13 @@ module Lutaml
|
|
|
249
252
|
options)
|
|
250
253
|
return nil if child.nil?
|
|
251
254
|
|
|
252
|
-
setters << [
|
|
255
|
+
setters << [rule.kv_setter, child]
|
|
253
256
|
children << child
|
|
254
257
|
end
|
|
255
258
|
end
|
|
256
259
|
instance = model_class.new(lutaml_register: register)
|
|
257
260
|
setters.each { |name, value| instance.public_send(name, value) }
|
|
258
|
-
absent
|
|
261
|
+
absent&.each do |rule|
|
|
259
262
|
process_mapping_rule(doc, instance, format, rule, options, nil)
|
|
260
263
|
end
|
|
261
264
|
children.each do |child|
|
|
@@ -342,7 +345,9 @@ options)
|
|
|
342
345
|
end
|
|
343
346
|
|
|
344
347
|
def valid_mapping?(rule, options)
|
|
345
|
-
|
|
348
|
+
# rule is always Lutaml::Model::MappingRule; serialize? is
|
|
349
|
+
# statically defined (the hydrate-only marker).
|
|
350
|
+
return false unless rule.serialize?
|
|
346
351
|
|
|
347
352
|
only = options[:only]
|
|
348
353
|
except = options[:except]
|
|
@@ -140,15 +140,11 @@ options = {})
|
|
|
140
140
|
nil_mapping = to_map[:nil]
|
|
141
141
|
|
|
142
142
|
if nil_mapping == :empty || converted_from_nil_to_empty
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
parent.add_child(element)
|
|
143
|
+
puts " HNC-EMPTY #{rule.serialized_name}" if ENV["T19"]
|
|
144
|
+
parent[rule.serialized_name] = []
|
|
147
145
|
elsif nil_mapping == :nil || converted_from_empty_to_nil
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
parent.add_child(element)
|
|
146
|
+
puts " HNC-NIL #{rule.serialized_name}" if ENV["T19"]
|
|
147
|
+
parent[rule.serialized_name] = nil
|
|
152
148
|
end
|
|
153
149
|
# else: Skip nil collection (default behavior)
|
|
154
150
|
end
|
|
@@ -158,8 +154,6 @@ options = {})
|
|
|
158
154
|
key_attribute = root_mappings.key(:key)
|
|
159
155
|
value_attribute = root_mappings.key(:value)
|
|
160
156
|
|
|
161
|
-
parent.value ||= {} if parent.value.nil?
|
|
162
|
-
|
|
163
157
|
if key_attribute && value_attribute
|
|
164
158
|
handle_root_mappings_key_value(parent, items, key_attribute,
|
|
165
159
|
value_attribute, rule, options)
|
|
@@ -213,7 +207,7 @@ options = {})
|
|
|
213
207
|
else
|
|
214
208
|
attr_value
|
|
215
209
|
end
|
|
216
|
-
parent
|
|
210
|
+
parent[key_value.to_s] = serialized_value if serialized_value
|
|
217
211
|
end
|
|
218
212
|
end
|
|
219
213
|
|
|
@@ -336,7 +330,7 @@ options)
|
|
|
336
330
|
end
|
|
337
331
|
end
|
|
338
332
|
|
|
339
|
-
parent
|
|
333
|
+
parent[key_value.to_s] = item_hash unless item_hash.empty?
|
|
340
334
|
end
|
|
341
335
|
end
|
|
342
336
|
|
|
@@ -362,10 +356,7 @@ options)
|
|
|
362
356
|
end
|
|
363
357
|
end
|
|
364
358
|
|
|
365
|
-
|
|
366
|
-
rule.serialized_name, keyed_hash
|
|
367
|
-
)
|
|
368
|
-
parent.add_child(element)
|
|
359
|
+
parent[rule.serialized_name] = keyed_hash
|
|
369
360
|
end
|
|
370
361
|
|
|
371
362
|
# Find value attribute from child_mappings.
|
|
@@ -490,18 +481,11 @@ options)
|
|
|
490
481
|
|
|
491
482
|
# Create an array collection element (default).
|
|
492
483
|
def create_array_collection_element(parent, rule, items, options)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
coll_element.value = []
|
|
497
|
-
else
|
|
498
|
-
items.each do |item|
|
|
499
|
-
child_value = create_value_for_item(rule, item, options)
|
|
500
|
-
coll_element.add_child(child_value) unless child_value.nil?
|
|
501
|
-
end
|
|
484
|
+
built = items.each_with_object([]) do |item, memo|
|
|
485
|
+
child_value = create_value_for_item(rule, item, options)
|
|
486
|
+
memo << child_value unless child_value.nil?
|
|
502
487
|
end
|
|
503
|
-
|
|
504
|
-
parent.add_child(coll_element)
|
|
488
|
+
parent[rule.serialized_name] = built
|
|
505
489
|
end
|
|
506
490
|
|
|
507
491
|
# Create a value element for a collection item.
|
|
@@ -554,8 +538,7 @@ options)
|
|
|
554
538
|
def serialize_nested_model(value, attr_type, options)
|
|
555
539
|
child_transformation = create_transformation(attr_type)
|
|
556
540
|
child_root = child_transformation.transform(value, options)
|
|
557
|
-
|
|
558
|
-
child_hash["__root__"]
|
|
541
|
+
child_root.empty? ? nil : child_root
|
|
559
542
|
end
|
|
560
543
|
|
|
561
544
|
# Serialize a primitive value.
|
|
@@ -151,8 +151,19 @@ model_class: nil)
|
|
|
151
151
|
return value.public_send(:"to_#{format}")
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
# Wrap value in type and call to_#{format}
|
|
155
154
|
if rule.attribute_type.is_a?(Class) && rule.attribute_type < Lutaml::Model::Type::Value
|
|
155
|
+
plan = serialize_plan(rule)
|
|
156
|
+
# Identity only under identity_fast_type's exact condition
|
|
157
|
+
# (no custom to AND no custom from): a custom from means the
|
|
158
|
+
# wrapper constructor normalizes the value, so the raw value
|
|
159
|
+
# must still pass through it.
|
|
160
|
+
if plan[:identity_type] && value.instance_of?(plan[:identity_type])
|
|
161
|
+
return value
|
|
162
|
+
end
|
|
163
|
+
if (to_fn = plan[:to_fn])
|
|
164
|
+
return to_fn.call(rule.attribute_type.new(value))
|
|
165
|
+
end
|
|
166
|
+
|
|
156
167
|
wrapped_value = rule.attribute_type.new(value)
|
|
157
168
|
wrapped_value.public_send(:"to_#{format}")
|
|
158
169
|
else
|
|
@@ -187,9 +198,23 @@ model_class: nil)
|
|
|
187
198
|
reference: reference_type?(rule),
|
|
188
199
|
union: ::Lutaml::Model::Type::Union.rule?(rule),
|
|
189
200
|
nested: nested_model?(rule),
|
|
201
|
+
# The custom to_<format> serializer, resolved once per rule:
|
|
202
|
+
# the default path is pure identity (to_<format> returns the
|
|
203
|
+
# wrapped .value), so values without a custom serializer skip
|
|
204
|
+
# the wrap-allocate-cast ceremony entirely, and custom ones
|
|
205
|
+
# skip the per-value registry lookup.
|
|
206
|
+
to_fn: custom_to_fn(rule),
|
|
190
207
|
}
|
|
191
208
|
end
|
|
192
209
|
|
|
210
|
+
def custom_to_fn(rule)
|
|
211
|
+
type = rule.attribute_type
|
|
212
|
+
return nil unless type.is_a?(::Class) && type < ::Lutaml::Model::Type::Value
|
|
213
|
+
|
|
214
|
+
::Lutaml::Model::Type::Value
|
|
215
|
+
.format_type_serializer_for(@format, type)&.fetch(:to, nil)
|
|
216
|
+
end
|
|
217
|
+
|
|
193
218
|
# Builtin scalars whose type registers no custom to_<format>
|
|
194
219
|
# serializer serialize as the value itself — Type::Value#to_<format>'s
|
|
195
220
|
# default — without the wrap-allocate-cast ceremony per field.
|
|
@@ -278,12 +303,10 @@ model_class: nil)
|
|
|
278
303
|
# @param options [Hash] Serialization options
|
|
279
304
|
# @return [Hash, nil] The serialized hash or nil if empty
|
|
280
305
|
def transform_nested_model(value, child_transformation, options)
|
|
306
|
+
# transform emits the content hash directly (TODO.perf 19);
|
|
307
|
+
# nil for empty keeps render_nil: false working.
|
|
281
308
|
child_root = child_transformation.transform(value, options)
|
|
282
|
-
|
|
283
|
-
result = child_hash["__root__"]
|
|
284
|
-
|
|
285
|
-
# Return nil for empty hashes (allows render_nil: false to work)
|
|
286
|
-
result.nil? || result.empty? ? nil : result
|
|
309
|
+
child_root.empty? ? nil : child_root
|
|
287
310
|
end
|
|
288
311
|
|
|
289
312
|
# Check if the rule defines a Reference type.
|