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
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Hydrates model instances from a Descriptor#walk PlanValue tree,
|
|
7
|
+
# keyed by each value's producing row name (never position — rows
|
|
8
|
+
# for missing elements are simply absent). Native rows build
|
|
9
|
+
# constructor kwargs; deferred rows (custom methods, polymorphism,
|
|
10
|
+
# unions) capture their subtree verbatim and interpret it
|
|
11
|
+
# post-walk — the fragment parse runs the existing interpretive
|
|
12
|
+
# machinery on just that island. Delegate rules hydrate
|
|
13
|
+
# post-instance onto their target object. Collection rows route
|
|
14
|
+
# natively when a single one exists, else through callback rows
|
|
15
|
+
# (their values echo name and type_tag; native collection values
|
|
16
|
+
# echo neither — leptris-ruby#220).
|
|
17
|
+
module PlanHydrator
|
|
18
|
+
# Row kinds whose child type/plan resolve once per plan (see
|
|
19
|
+
# child_row_plans).
|
|
20
|
+
CHILD_ROW_KINDS = %i[nested ordered_deferred].freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# plan: the compiled plan entry for model_class
|
|
24
|
+
# value: the walk root PlanValue (element)
|
|
25
|
+
# node: the parsed source element (leptris); ordered/mixed
|
|
26
|
+
# plans rebuild element_order from it and ordered children
|
|
27
|
+
# hydrate natively against their own nodes
|
|
28
|
+
# declaration_plan_mode: mirrors from_xml's
|
|
29
|
+
# :import_declaration_plan option (:skip records nothing;
|
|
30
|
+
# :lazy is honored as :eager — the lazy variant pins the
|
|
31
|
+
# raw engine DOM, the retention the eager default exists
|
|
32
|
+
# to avoid)
|
|
33
|
+
def call(model_class, plan, value, parent: nil, node: nil,
|
|
34
|
+
register: nil, declaration_plan_mode: :eager)
|
|
35
|
+
# The child model's declared lutaml_default_register takes
|
|
36
|
+
# precedence over the ambient register — the same
|
|
37
|
+
# resolve_for_child contract the interpretive path applies.
|
|
38
|
+
# Without it, hydration resolves the child's symbol attribute
|
|
39
|
+
# types in the parent context and raises UnknownTypeError
|
|
40
|
+
# for ids registered only in the child's register (#876).
|
|
41
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
42
|
+
model_class, register || Lutaml::Model::Config.default_register
|
|
43
|
+
)
|
|
44
|
+
buckets = if node && plan[:needs_nodes]
|
|
45
|
+
element_buckets(node, plan[:contested_names])
|
|
46
|
+
end
|
|
47
|
+
attr_kwargs = attributes_kwargs(plan, value, node)
|
|
48
|
+
child_kwargs, children, delegates =
|
|
49
|
+
children_kwargs(model_class, plan, value, buckets, register)
|
|
50
|
+
plan[:collection_defaults].each do |name|
|
|
51
|
+
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
52
|
+
|
|
53
|
+
child_kwargs[name] = []
|
|
54
|
+
end
|
|
55
|
+
# TODO.max-perf/33: the bulk constructor — allocate +
|
|
56
|
+
# compiled writers, from_hash semantics — measured ~30x on
|
|
57
|
+
# hydration-heavy corpora versus the generic constructor.
|
|
58
|
+
attr_kwargs.update(child_kwargs)
|
|
59
|
+
instance = model_class.instantiate(attr_kwargs, register)
|
|
60
|
+
instance.lutaml_parent = parent if parent
|
|
61
|
+
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
62
|
+
# Root prefix fidelity (the interpretive transform records
|
|
63
|
+
# it from the source element): prefixed sources keep their
|
|
64
|
+
# prefixes through the serialize lane's format
|
|
65
|
+
# preservation.
|
|
66
|
+
if parent.nil? && node.respond_to?(:prefix) && node.prefix &&
|
|
67
|
+
!node.prefix.empty?
|
|
68
|
+
instance.xml_namespace_prefix = node.prefix
|
|
69
|
+
end
|
|
70
|
+
# #909: order summaries are per-instance pinned records —
|
|
71
|
+
# read-only consumers skip them via the config.
|
|
72
|
+
if node && plan[:ordered] &&
|
|
73
|
+
Lutaml::Model::Config.instance.element_order_tracking
|
|
74
|
+
instance.element_order = PlanOrder.build(node)
|
|
75
|
+
end
|
|
76
|
+
# Root declaration-plan parity (the interpretive
|
|
77
|
+
# transform's eager mode): consumers and the interpretive
|
|
78
|
+
# serialize fallback read import_declaration_plan state —
|
|
79
|
+
# plan-hydrated roots must carry the same in-scope xmlns
|
|
80
|
+
# declarations or round-trips drop them (#872). The root's
|
|
81
|
+
# xsi:schemaLocation rides the same state (the
|
|
82
|
+
# interpretive set_schema_location contract).
|
|
83
|
+
if parent.nil? && node
|
|
84
|
+
record_declaration_plan(instance, node, model_class) if declaration_plan_mode != :skip
|
|
85
|
+
record_schema_location(instance, node)
|
|
86
|
+
end
|
|
87
|
+
children.each do |child|
|
|
88
|
+
child.lutaml_parent = instance
|
|
89
|
+
child.lutaml_root ||= instance.lutaml_root || instance
|
|
90
|
+
end
|
|
91
|
+
interpret_deferred(model_class, plan, value, instance, node, buckets)
|
|
92
|
+
route_delegates(delegates, instance)
|
|
93
|
+
instance
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def record_declaration_plan(instance, node, model_class)
|
|
99
|
+
ns_data = collect_native_namespaces(node)
|
|
100
|
+
return if ns_data.empty?
|
|
101
|
+
|
|
102
|
+
plan = Lutaml::Xml::DeclarationPlan.from_input_with_locations(
|
|
103
|
+
ns_data, model_class.mappings_for(:xml)
|
|
104
|
+
)
|
|
105
|
+
instance.import_declaration_plan = plan if plan
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The interpretive parse stores the root's xsi:schemaLocation
|
|
109
|
+
# verbatim (set_schema_location); the writer re-emits it.
|
|
110
|
+
def record_schema_location(instance, node)
|
|
111
|
+
schema_loc = node.attributes.each_value.find do |a|
|
|
112
|
+
a.name.to_s.split(":", 2).last == "schemaLocation"
|
|
113
|
+
end
|
|
114
|
+
instance.raw_schema_location = schema_loc.value if schema_loc
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Same ns_data shape as ModelTransform.collect_element_namespaces,
|
|
118
|
+
# collected straight off the engine nodes — no wrapper tree.
|
|
119
|
+
# Iterates children with an inline element filter:
|
|
120
|
+
# element_children materializes a filtered array per node,
|
|
121
|
+
# which the niso allocation gate caught at scale.
|
|
122
|
+
def collect_native_namespaces(element, path = [], result = {})
|
|
123
|
+
own = element.namespace_definitions
|
|
124
|
+
unless own.empty?
|
|
125
|
+
namespaces = {}
|
|
126
|
+
own.each do |ns|
|
|
127
|
+
key = ns.prefix || :default
|
|
128
|
+
namespaces[key] = { uri: ns.href, prefix: ns.prefix,
|
|
129
|
+
format: ns.prefix ? :prefix : :default }
|
|
130
|
+
end
|
|
131
|
+
result[path.dup] = namespaces
|
|
132
|
+
end
|
|
133
|
+
element.children.each do |child|
|
|
134
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
135
|
+
|
|
136
|
+
path.push(child.name)
|
|
137
|
+
collect_native_namespaces(child, path, result)
|
|
138
|
+
path.pop
|
|
139
|
+
end
|
|
140
|
+
result
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def register
|
|
144
|
+
Lutaml::Model::Config.default_register
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def attributes_kwargs(plan, value, node = nil)
|
|
148
|
+
kwargs = {}
|
|
149
|
+
sole_claimants = sole_claimant_names(plan)
|
|
150
|
+
ns_attr_names = plan[:ns_attr_names]
|
|
151
|
+
plan_attr_rows(plan).each do |rule, attr, name|
|
|
152
|
+
v = value.attribute(name)
|
|
153
|
+
if v.nil? && node && sole_claimants.include?(name)
|
|
154
|
+
# #754/#790 parity: a sole-claimant attribute rule binds
|
|
155
|
+
# any qualification (the interpretive matcher's lenient
|
|
156
|
+
# recovery). The walk captures exact (URI, local) matches
|
|
157
|
+
# only, so the qualified spelling is read off the node.
|
|
158
|
+
v = lenient_node_attribute(node, name)
|
|
159
|
+
end
|
|
160
|
+
next if v.nil?
|
|
161
|
+
|
|
162
|
+
# (URI, local) rows mirror the interpretive matcher's
|
|
163
|
+
# ambiguity refusal: a second spelling of the same local
|
|
164
|
+
# name binds nothing (lutaml-model#744).
|
|
165
|
+
if ns_attr_names&.include?(name) &&
|
|
166
|
+
ambiguous_local_spellings?(node, name)
|
|
167
|
+
next
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
v = v.split(rule.delimiter) if rule.delimiter
|
|
171
|
+
if rule.as_list && rule.as_list[:import]
|
|
172
|
+
v = rule.as_list[:import].call(v)
|
|
173
|
+
end
|
|
174
|
+
kwargs[attr.name.to_sym] = v
|
|
175
|
+
end
|
|
176
|
+
kwargs
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# True when the node carries more than one spelling of the
|
|
180
|
+
# local name (any qualification). Only consulted for exact-URI
|
|
181
|
+
# attribute rows.
|
|
182
|
+
def ambiguous_local_spellings?(node, local_name)
|
|
183
|
+
return false unless node.respond_to?(:attributes)
|
|
184
|
+
|
|
185
|
+
seen = false
|
|
186
|
+
node.attributes.each_value do |attr|
|
|
187
|
+
name = attr.name.to_s
|
|
188
|
+
name = name.split(":").last if name.include?(":")
|
|
189
|
+
next unless name == local_name
|
|
190
|
+
|
|
191
|
+
return true if seen
|
|
192
|
+
|
|
193
|
+
seen = true
|
|
194
|
+
end
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# A captured-but-empty child marshals as a nil string value.
|
|
199
|
+
# The interpretive pipeline classifies the raw "" through the
|
|
200
|
+
# rule's value map (:empty key) — the :empty directive (the
|
|
201
|
+
# plain-rule default) keeps it as "", :nil binds nil, and any
|
|
202
|
+
# other directive means uninitialized, i.e. no binding.
|
|
203
|
+
def mapped_empty_value(rule)
|
|
204
|
+
case rule.value_map(:from)[:empty]
|
|
205
|
+
when :nil then nil
|
|
206
|
+
when :empty then ""
|
|
207
|
+
else :skip
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def mapped_capture_values(values, rule)
|
|
212
|
+
return values unless values.include?(nil)
|
|
213
|
+
|
|
214
|
+
mapped = mapped_empty_value(rule)
|
|
215
|
+
out = []
|
|
216
|
+
values.each do |item|
|
|
217
|
+
if item.nil?
|
|
218
|
+
out << mapped unless mapped == :skip
|
|
219
|
+
else
|
|
220
|
+
out << item
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
out
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Wire-name strings are plan-frozen; materializing them per
|
|
227
|
+
# hydration allocated two or three strings per attribute row
|
|
228
|
+
# per instance.
|
|
229
|
+
def plan_attr_rows(plan)
|
|
230
|
+
plan[:attr_row_names] ||= plan[:attr_rows].map do |rule, attr, row_name|
|
|
231
|
+
[rule, attr, row_name || rule.name.to_s]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Returns [kwargs, hydrated_child_instances, delegate_values]
|
|
236
|
+
# — child instances come back so the caller can decorate
|
|
237
|
+
# parent/root links once the parent exists; delegate values
|
|
238
|
+
# wait for the instance (their target object must exist).
|
|
239
|
+
def children_kwargs(_model_class, plan, value, buckets = nil,
|
|
240
|
+
register = nil)
|
|
241
|
+
register ||= Lutaml::Model::Config.default_register
|
|
242
|
+
grouped, tagged = group_children(value, plan[:row_tags])
|
|
243
|
+
buckets = nil unless buckets && plan[:needs_nodes]
|
|
244
|
+
kwargs = {}
|
|
245
|
+
children = []
|
|
246
|
+
delegates = []
|
|
247
|
+
# Lazy: models without multi-spelling rows never touch it, and
|
|
248
|
+
# the block-backed Hash is a fresh object per hydrated row.
|
|
249
|
+
spellings = nil
|
|
250
|
+
row_tags = plan[:row_tags]
|
|
251
|
+
wire_names = row_wire_names(plan)
|
|
252
|
+
row_memo = child_row_plans(plan, register)
|
|
253
|
+
# rubocop:disable Metrics/BlockLength -- one row-kind
|
|
254
|
+
# dispatch table; splitting it would scatter the contract
|
|
255
|
+
plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate, _row_type), idx|
|
|
256
|
+
tag = row_tags&.[](idx)
|
|
257
|
+
name = wire_names[idx]
|
|
258
|
+
case kind
|
|
259
|
+
when :scalar
|
|
260
|
+
# Raw passthrough: the model constructor is the single
|
|
261
|
+
# cast authority — pre-casting here doubled every cast.
|
|
262
|
+
# Class transforms still apply before assignment.
|
|
263
|
+
if tag && (vals = tagged[tag]) && vals.size > 1
|
|
264
|
+
# Interpretive parity: several captures into a
|
|
265
|
+
# non-collection attribute arrive as the full array.
|
|
266
|
+
v = vals.map(&:string_value)
|
|
267
|
+
if v.include?(nil)
|
|
268
|
+
v = v.map { |item| item.nil? ? mapped_empty_value(rule) : item }
|
|
269
|
+
v.reject! { |item| item == :skip }
|
|
270
|
+
end
|
|
271
|
+
else
|
|
272
|
+
first = if tag
|
|
273
|
+
tagged[tag]&.first
|
|
274
|
+
else
|
|
275
|
+
grouped.dig(name, 0)
|
|
276
|
+
end
|
|
277
|
+
v = first&.string_value
|
|
278
|
+
v = mapped_empty_value(rule) if v.nil? && first
|
|
279
|
+
end
|
|
280
|
+
unless v.nil? || v == :skip
|
|
281
|
+
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
282
|
+
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
283
|
+
end
|
|
284
|
+
when :wildcard_any
|
|
285
|
+
# interpreted post-instance (interpret_deferred)
|
|
286
|
+
when :content
|
|
287
|
+
runs = content_runs(value)
|
|
288
|
+
if runs.empty? && !attr.collection?
|
|
289
|
+
# An empty element's content binds through the rule's
|
|
290
|
+
# value map for scalar content attributes: "" for the
|
|
291
|
+
# plain :empty directive, nil for :nil, skipped for
|
|
292
|
+
# anything else (uninitialized).
|
|
293
|
+
bound = mapped_empty_value(rule)
|
|
294
|
+
assign(kwargs, delegates, delegate, rule, attr, bound) unless bound == :skip
|
|
295
|
+
else
|
|
296
|
+
assign(kwargs, delegates, delegate, rule, attr, runs)
|
|
297
|
+
end
|
|
298
|
+
when :ordered_deferred
|
|
299
|
+
# With a source node the child hydrates natively: one
|
|
300
|
+
# walk against its own node + element_order from the
|
|
301
|
+
# node's children. Without one (direct PlanHydrator
|
|
302
|
+
# use), the subtree fragment-parses interpretively.
|
|
303
|
+
if buckets && row_memo[idx]
|
|
304
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
305
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
306
|
+
items = (selected || []).map do |n|
|
|
307
|
+
call(child_type, child_plan,
|
|
308
|
+
child_plan[:descriptor].walk(n), node: n,
|
|
309
|
+
register: child_register)
|
|
310
|
+
end
|
|
311
|
+
unless items.empty?
|
|
312
|
+
children.concat(items)
|
|
313
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
314
|
+
attr.collection? ? items : items.first)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
when :child_deferred
|
|
318
|
+
# Uncompilable child (cycles, opted-out shapes): each
|
|
319
|
+
# source node casts through the attribute — the full
|
|
320
|
+
# interpretive machinery on the bridged island,
|
|
321
|
+
# identical semantics to the interpretive path.
|
|
322
|
+
contested_uri = if plan[:contested_names]&.include?(name)
|
|
323
|
+
child_type_uri(attr, register)
|
|
324
|
+
end
|
|
325
|
+
elements = deferred_elements(value, name, buckets,
|
|
326
|
+
contested_uri)
|
|
327
|
+
next if elements.empty?
|
|
328
|
+
|
|
329
|
+
results = elements.map do |element|
|
|
330
|
+
attr.cast(element, :xml, register)
|
|
331
|
+
end
|
|
332
|
+
unless results.empty?
|
|
333
|
+
children.concat(results)
|
|
334
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
335
|
+
attr.collection? ? results : results.first)
|
|
336
|
+
end
|
|
337
|
+
when :collection_cb
|
|
338
|
+
values = grouped[name].to_a.map do |item|
|
|
339
|
+
item&.string_value
|
|
340
|
+
end
|
|
341
|
+
values = mapped_capture_values(values, rule)
|
|
342
|
+
if rule.transform.is_a?(Class)
|
|
343
|
+
values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
|
|
344
|
+
end
|
|
345
|
+
unless values.empty?
|
|
346
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
347
|
+
end
|
|
348
|
+
when :collection_native
|
|
349
|
+
values = if tag
|
|
350
|
+
tagged[tag].to_a.map(&:string_value)
|
|
351
|
+
else
|
|
352
|
+
native_collection(value, name)
|
|
353
|
+
end
|
|
354
|
+
values = mapped_capture_values(values, rule) unless values.nil?
|
|
355
|
+
unless values.nil?
|
|
356
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
357
|
+
end
|
|
358
|
+
when :spelling
|
|
359
|
+
spellings ||= Hash.new { |h, k| h[k] = [] }
|
|
360
|
+
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
361
|
+
when :nested
|
|
362
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
363
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
364
|
+
if selected && contested
|
|
365
|
+
# Contested name: the walk's grouped values cross-echo
|
|
366
|
+
# every same-name element into every row — dispatch
|
|
367
|
+
# per source node instead (the walk is row-scoped).
|
|
368
|
+
items = selected.map do |n|
|
|
369
|
+
call(child_type, child_plan,
|
|
370
|
+
child_plan[:descriptor].walk(n), node: n,
|
|
371
|
+
register: child_register)
|
|
372
|
+
end
|
|
373
|
+
else
|
|
374
|
+
values = grouped.fetch(name, [])
|
|
375
|
+
cursor = if child_plan[:needs_nodes] && buckets
|
|
376
|
+
selected
|
|
377
|
+
end
|
|
378
|
+
items = values.each_with_index
|
|
379
|
+
.map do |v, i|
|
|
380
|
+
call(child_type, child_plan, v,
|
|
381
|
+
node: cursor && cursor[i], register: child_register)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
next if items.empty?
|
|
385
|
+
|
|
386
|
+
children.concat(items)
|
|
387
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
388
|
+
attr.collection? ? items : items.first)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
# rubocop:enable Metrics/BlockLength
|
|
392
|
+
unless !spellings || spellings.empty?
|
|
393
|
+
spellings.each do |(rule, attr), groups|
|
|
394
|
+
# Interpretive order for shared-attribute groups is
|
|
395
|
+
# spelling-group order (first spelling's matches, then
|
|
396
|
+
# the next's), not interleaved document order.
|
|
397
|
+
values = groups.compact.flatten.map(&:string_value)
|
|
398
|
+
next if values.empty?
|
|
399
|
+
|
|
400
|
+
delegate = delegate_of(plan, rule)
|
|
401
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
402
|
+
attr.collection? ? values : values.first)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
[kwargs, children, delegates]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Deferred islands: raw subtrees become wrapper elements via a
|
|
409
|
+
# fragment parse, then the interpretive machinery runs on just
|
|
410
|
+
# that island — custom method invocation with an
|
|
411
|
+
# element-shaped argument, or the polymorphic/union cast.
|
|
412
|
+
# With the source node available the island wraps the ORIGINAL
|
|
413
|
+
# node (no fragment parse — the reparse dominated deferred
|
|
414
|
+
# hydration). Ordered children only land here without a node.
|
|
415
|
+
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
416
|
+
buckets = nil)
|
|
417
|
+
plan[:rows].each_with_index do |row, idx|
|
|
418
|
+
rule = row[0]
|
|
419
|
+
attr = row[1]
|
|
420
|
+
kind = row[2]
|
|
421
|
+
wire_name = row_wire_names(plan)[idx]
|
|
422
|
+
case kind
|
|
423
|
+
when :content_deferred
|
|
424
|
+
runs = content_runs(value)
|
|
425
|
+
if runs.empty?
|
|
426
|
+
# The interpretive pipeline classifies the raw ""
|
|
427
|
+
# through the rule's value map: the :empty directive
|
|
428
|
+
# (the plain-rule default) binds "" — the attribute is
|
|
429
|
+
# still marked set, which is what the #856 mark is
|
|
430
|
+
# about; :nil binds nil; other directives leave the
|
|
431
|
+
# binding unset.
|
|
432
|
+
instance.value_set_for(attr.name)
|
|
433
|
+
case rule.value_map(:from)[:empty]
|
|
434
|
+
when :nil
|
|
435
|
+
instance.public_send(:"#{attr.name}=", nil)
|
|
436
|
+
when :empty
|
|
437
|
+
instance.public_send(:"#{attr.name}=", "")
|
|
438
|
+
end
|
|
439
|
+
next
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Non-collection content attrs hold the joined text
|
|
443
|
+
# (the interpretive path assigns element text, the runs
|
|
444
|
+
# concatenated).
|
|
445
|
+
instance.public_send(:"#{attr.name}=", runs.join)
|
|
446
|
+
when :raw
|
|
447
|
+
raws = raw_strings(value, wire_name)
|
|
448
|
+
next if raws.empty?
|
|
449
|
+
|
|
450
|
+
instance.public_send(:"#{attr.name}=",
|
|
451
|
+
attr.collection? ? raws : raws.first)
|
|
452
|
+
when :custom_method
|
|
453
|
+
elements = deferred_elements(value, wire_name, buckets)
|
|
454
|
+
next if elements.empty?
|
|
455
|
+
|
|
456
|
+
args = attr.collection? ? elements : elements.first
|
|
457
|
+
rule.deserialize(instance, args,
|
|
458
|
+
model_class.attributes(register),
|
|
459
|
+
model_class)
|
|
460
|
+
when :ordered_deferred
|
|
461
|
+
next if node # hydrated natively in children_kwargs
|
|
462
|
+
|
|
463
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
464
|
+
attr.cast(element, :xml, register,
|
|
465
|
+
lutaml_parent: instance,
|
|
466
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
467
|
+
end
|
|
468
|
+
next if results.empty?
|
|
469
|
+
|
|
470
|
+
instance.public_send(:"#{attr.name}=",
|
|
471
|
+
attr.collection? ? results : results.first)
|
|
472
|
+
when :wildcard_any
|
|
473
|
+
# map_any_element: every child element no named row
|
|
474
|
+
# claims, in document order, bridged as wrappers — the
|
|
475
|
+
# interpretive pass's contract. The wildcard row's
|
|
476
|
+
# type_tag marks its captures in the walk.
|
|
477
|
+
next unless node
|
|
478
|
+
|
|
479
|
+
claimed = plan[:rows].filter_map do |wr, _wa, _wk, spelling, _wd|
|
|
480
|
+
next if wr.equal?(rule) || wr.name.nil?
|
|
481
|
+
|
|
482
|
+
spelling ? spelling.to_s : wr.name.to_s
|
|
483
|
+
end.uniq
|
|
484
|
+
elements = []
|
|
485
|
+
node.children.each do |child|
|
|
486
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
487
|
+
next if claimed.include?(child.name)
|
|
488
|
+
|
|
489
|
+
elements << bridge_element(child)
|
|
490
|
+
end
|
|
491
|
+
next if elements.empty?
|
|
492
|
+
|
|
493
|
+
# The interpretive pass casts each collected element
|
|
494
|
+
# through the attribute: String keeps the wrapper,
|
|
495
|
+
# Serializable types build model instances.
|
|
496
|
+
results = elements.map do |element|
|
|
497
|
+
attr.cast(element, :xml, register,
|
|
498
|
+
lutaml_parent: instance,
|
|
499
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
500
|
+
end
|
|
501
|
+
instance.public_send(
|
|
502
|
+
:"#{attr.name}=",
|
|
503
|
+
attr.collection? ? results : results.first,
|
|
504
|
+
)
|
|
505
|
+
when :polymorphic
|
|
506
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
507
|
+
attr.cast(element, :xml, register,
|
|
508
|
+
polymorphic: rule.polymorphic,
|
|
509
|
+
lutaml_parent: instance,
|
|
510
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
511
|
+
end
|
|
512
|
+
next if results.empty?
|
|
513
|
+
|
|
514
|
+
instance.public_send(:"#{attr.name}=",
|
|
515
|
+
attr.collection? ? results : results.first)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Wrapper elements for deferred rows: the ORIGINAL source
|
|
521
|
+
# nodes bridged through moxml's wrapper cache (wrapper
|
|
522
|
+
# construction only, no parse), falling back to a fragment
|
|
523
|
+
# parse of the captured subtree when no node is available.
|
|
524
|
+
# Both lists are document-ordered for the row's name.
|
|
525
|
+
def deferred_elements(value, name, buckets, contested_uri = nil)
|
|
526
|
+
return raw_strings(value, name).map { |raw| fragment_element(raw) } unless buckets
|
|
527
|
+
|
|
528
|
+
nodes = buckets[name]
|
|
529
|
+
return [] unless nodes
|
|
530
|
+
|
|
531
|
+
if contested_uri
|
|
532
|
+
# Contested name: bind only the (URI, local) matches —
|
|
533
|
+
# the type's own mapping namespace is the row identity.
|
|
534
|
+
nodes = buckets[[contested_uri, name]] || []
|
|
535
|
+
end
|
|
536
|
+
nodes.map { |n| bridge_element(n) }
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def child_type_uri(attr, register)
|
|
540
|
+
type = attr.type(register)
|
|
541
|
+
return nil unless type.is_a?(Class) &&
|
|
542
|
+
type.include?(::Lutaml::Model::Serialize)
|
|
543
|
+
|
|
544
|
+
type.mappings_for(:xml, register)&.namespace_uri
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def contested_name?(plan, rule)
|
|
548
|
+
plan[:contested_names]&.include?(rule.name.to_s) || false
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def bridge_element(node)
|
|
552
|
+
Lutaml::Xml::LeptrisElement.new(
|
|
553
|
+
Moxml::Node.wrap(node, bridge_context),
|
|
554
|
+
)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def bridge_context
|
|
558
|
+
@bridge_context ||= Moxml::Context.new(:leptris)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def route_delegates(delegates, instance)
|
|
562
|
+
delegates.each do |rule, delegate_attr, attr, v|
|
|
563
|
+
target = instance.public_send(rule.delegate)
|
|
564
|
+
unless target
|
|
565
|
+
# the interpretive path instantiates absent delegate
|
|
566
|
+
# targets; mirror it
|
|
567
|
+
target = delegate_attr.type(register).new
|
|
568
|
+
instance.public_send(:"#{rule.delegate}=", target)
|
|
569
|
+
end
|
|
570
|
+
target.public_send(:"#{attr.name}=", v)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def delegate_of(plan, rule)
|
|
575
|
+
entry = plan[:rows].find { |r, _, _k, _| r.equal?(rule) }
|
|
576
|
+
entry && entry[4]
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Delegate rules hold their values for post-instance routing
|
|
580
|
+
# (the target object must exist first); plain rules land in
|
|
581
|
+
# kwargs directly.
|
|
582
|
+
def assign(kwargs, delegates, delegate, rule, attr, value)
|
|
583
|
+
if delegate
|
|
584
|
+
delegates << [rule, delegate, attr, value]
|
|
585
|
+
else
|
|
586
|
+
kwargs[attr.name.to_sym] = value
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def content_runs(value)
|
|
591
|
+
out = []
|
|
592
|
+
value.count.times do |i|
|
|
593
|
+
c = value.at(i)
|
|
594
|
+
next unless c.name.nil? && c.kind == :collection
|
|
595
|
+
|
|
596
|
+
out.concat(Array.new(c.count) { |j| c.at(j).string_value })
|
|
597
|
+
end
|
|
598
|
+
out
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Native collection rows echo their producing row's name
|
|
602
|
+
# (leptris 1.9.178).
|
|
603
|
+
def native_collection(value, name)
|
|
604
|
+
value.count.times do |i|
|
|
605
|
+
c = value.at(i)
|
|
606
|
+
next unless c.kind == :collection && c.name == name
|
|
607
|
+
|
|
608
|
+
return Array.new(c.count) { |j| c.at(j).string_value }
|
|
609
|
+
end
|
|
610
|
+
nil
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def raw_strings(value, name)
|
|
614
|
+
out = []
|
|
615
|
+
value.count.times do |idx|
|
|
616
|
+
child = value.at(idx)
|
|
617
|
+
if child.name == name && child.kind == :raw
|
|
618
|
+
out << child.string_value
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
out
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Fragment deferral: parse the captured subtree back into a
|
|
625
|
+
# wrapper element. Ancestor namespace context is absent (the
|
|
626
|
+
# compiler only defers on namespace-free model chains).
|
|
627
|
+
def fragment_element(raw)
|
|
628
|
+
Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Attribute names claimed by exactly one attr row — the only
|
|
632
|
+
# names eligible for #754/#790 lenient binding (multi-claimant
|
|
633
|
+
# names stay exact; per #841 leniency is sole-claimant-only).
|
|
634
|
+
def sole_claimant_names(plan)
|
|
635
|
+
# Frozen with the plan: recomputing the tally per hydration
|
|
636
|
+
# showed in the #876-era profile as Enumerable#find/tally churn.
|
|
637
|
+
plan[:sole_claimants] ||= plan[:attr_rows]
|
|
638
|
+
.map { |rule, _attr, row_name| row_name || rule.name.to_s }.tally
|
|
639
|
+
.select { |_n, c| c == 1 }.keys
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Local-name attribute lookup on the source node, qualified
|
|
643
|
+
# spellings included (w:val matches rule "val").
|
|
644
|
+
def lenient_node_attribute(node, local_name)
|
|
645
|
+
return nil unless node.respond_to?(:attributes)
|
|
646
|
+
|
|
647
|
+
node.attributes.each_value do |attr|
|
|
648
|
+
# Leptris::XML::Attr carries the raw (possibly prefixed)
|
|
649
|
+
# name; strip the prefix for the local-name comparison.
|
|
650
|
+
name = attr.name.to_s
|
|
651
|
+
name = name.split(":").last if name.include?(":")
|
|
652
|
+
return attr.value if name == local_name
|
|
653
|
+
end
|
|
654
|
+
nil
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def group_children(value, row_tags = nil)
|
|
658
|
+
# One crossing per subtree: names/tags/children come back in
|
|
659
|
+
# parallel arrays from leptris_plan_value_children_snapshot.
|
|
660
|
+
# The plan-path version gate (PlanCompiler.compile) already
|
|
661
|
+
# guarantees leptris >= 1.9.273.0.
|
|
662
|
+
names, tags, children = value.children_snapshot
|
|
663
|
+
grouped = {}
|
|
664
|
+
want_tags = !row_tags.nil?
|
|
665
|
+
tagged = want_tags ? {} : nil
|
|
666
|
+
names.each_with_index do |name, i|
|
|
667
|
+
next if name.nil? # content runs, read separately
|
|
668
|
+
|
|
669
|
+
child = children[i]
|
|
670
|
+
(grouped[name] ||= []) << child
|
|
671
|
+
if want_tags && tags[i] != 0
|
|
672
|
+
(tagged[tags[i]] ||= []) << child
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
[grouped, tagged]
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Element children bucketed by local name, document order
|
|
679
|
+
# preserved — the node-side mirror of group_children for
|
|
680
|
+
# ordered-child hydration. Only built when the plan's subtree
|
|
681
|
+
# needs source nodes; namespace-qualified models never get
|
|
682
|
+
# here (the compiler keeps them interpretive).
|
|
683
|
+
# Contested names need (URI, local) node buckets; every other
|
|
684
|
+
# plan hydrates from the name view alone — the qualified keys
|
|
685
|
+
# are one array + hash write per child and measured as an
|
|
686
|
+
# allocation regression on element-heavy corpora (niso gate).
|
|
687
|
+
def element_buckets(node, contested_names = nil)
|
|
688
|
+
buckets = {}
|
|
689
|
+
qualified = contested_names && !contested_names.empty?
|
|
690
|
+
node.children.each do |child|
|
|
691
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
692
|
+
|
|
693
|
+
name = child.name
|
|
694
|
+
(buckets[name] ||= []) << child
|
|
695
|
+
if qualified
|
|
696
|
+
uri = child.namespace&.href
|
|
697
|
+
(buckets[[uri, name]] ||= []) << child if uri
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
buckets
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Selected source nodes for a child row: contested names
|
|
704
|
+
# dispatch by (URI, local); every other row binds the whole
|
|
705
|
+
# name bucket leniently (any qualification). The pair form
|
|
706
|
+
# this replaces allocated a fresh array per row per node on
|
|
707
|
+
# the ordered walk (niso gate).
|
|
708
|
+
def child_selection(buckets, name, child_plan, contested)
|
|
709
|
+
all = buckets && buckets[name]
|
|
710
|
+
return all unless buckets && all && contested
|
|
711
|
+
|
|
712
|
+
tree_ns = child_plan[:tree][:ns]
|
|
713
|
+
exact_uri = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
714
|
+
exact_uri ? (buckets[[exact_uri, name]] || []) : all
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Per-(plan, register) child-row tuples, computed once and
|
|
718
|
+
# reused across every node the walk hands this plan: the
|
|
719
|
+
# per-node type lookup, compile hit, child register
|
|
720
|
+
# resolution, and contested flag were each a measurable slice
|
|
721
|
+
# of the ordered-lane allocation profile (niso gate).
|
|
722
|
+
def child_row_plans(plan, register)
|
|
723
|
+
plan[:child_row_memo] ||= {}
|
|
724
|
+
plan[:child_row_memo][register] ||= plan[:rows].map do |rule, attr, kind, _spelling, _delegate, row_type|
|
|
725
|
+
next nil unless CHILD_ROW_KINDS.include?(kind)
|
|
726
|
+
|
|
727
|
+
child_type = row_type || attr.type(register)
|
|
728
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
729
|
+
child_register = Lutaml::Model::Register.resolve_for_child(
|
|
730
|
+
child_type, register
|
|
731
|
+
)
|
|
732
|
+
[child_type, child_plan, child_register,
|
|
733
|
+
contested_name?(plan, rule)]
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Plan-frozen wire names: rule.name.to_s minted a fresh string
|
|
738
|
+
# per row per node in the hot loop.
|
|
739
|
+
def row_wire_names(plan)
|
|
740
|
+
plan[:row_wire_names] ||= plan[:rows].map do |rule, _attr, _kind, _spelling, _delegate, _row_type|
|
|
741
|
+
rule.name.to_s
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|