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,702 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Serialize-side mirror of the plan fast path: builds the output
|
|
7
|
+
# tree directly through leptris's fused create_child chain — no
|
|
8
|
+
# XmlElement tree, no DeclarationPlanner pass, no builder context
|
|
9
|
+
# stack, no moxml wrapper minting. Serialized shapes must be the
|
|
10
|
+
# plain ones (attributes, scalars, collections, nested models,
|
|
11
|
+
# raw fragments, content runs); custom-method, polymorphic,
|
|
12
|
+
# union, and multi-spelling rows keep the interpretive serializer
|
|
13
|
+
# (their output flows through hash-shaped custom APIs).
|
|
14
|
+
#
|
|
15
|
+
# Ordered/mixed models serialize from element_order when the
|
|
16
|
+
# instance carries one (parsed models): text runs, comments, and
|
|
17
|
+
# element entries in document order, mirroring the interpretive
|
|
18
|
+
# order applier — including its content-attr-first text reads
|
|
19
|
+
# (mutations to the model show up) and CDATA flattening. Models
|
|
20
|
+
# without element_order fall back interpretively (return nil).
|
|
21
|
+
# Writer-added values the order snapshot does not cover ride the
|
|
22
|
+
# OrderReconciler: the walk consumes the reconciled order and
|
|
23
|
+
# unplaceable rules emit appended (the interpretive
|
|
24
|
+
# apply_remaining_rules contract).
|
|
25
|
+
module PlanSerializer
|
|
26
|
+
SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
|
|
27
|
+
nested ordered_deferred raw content
|
|
28
|
+
content_deferred].freeze
|
|
29
|
+
PLAN_SERIALIZABLE_KINDS = SERIALIZABLE_KINDS
|
|
30
|
+
CONTENT_KINDS = %i[content content_deferred].freeze
|
|
31
|
+
|
|
32
|
+
# Reconciler host: the interpretive support modules are written
|
|
33
|
+
# against the Transformation's combined surface (the reconciler
|
|
34
|
+
# consumes applier helpers such as find_rule_for_element and
|
|
35
|
+
# rule_discriminator), so the plan lane drives them through a
|
|
36
|
+
# dedicated host holding both. The helpers are pure functions
|
|
37
|
+
# of their arguments — the frozen host carries no state.
|
|
38
|
+
class ReconcilerHost
|
|
39
|
+
include(TransformationSupport::OrderedApplier)
|
|
40
|
+
include(TransformationSupport::OrderReconciler)
|
|
41
|
+
include(TransformationSupport::SkipLogic)
|
|
42
|
+
end
|
|
43
|
+
RECONCILER = ReconcilerHost.new.freeze
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# plan: the compiler's entry for instance's class
|
|
47
|
+
def call(instance, plan, register = nil)
|
|
48
|
+
# The child model's declared lutaml_default_register takes
|
|
49
|
+
# precedence over the ambient register — the resolve_for_child
|
|
50
|
+
# contract (#876); without it, child plans and attribute
|
|
51
|
+
# serialization resolve in the parent context.
|
|
52
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
53
|
+
instance.class,
|
|
54
|
+
register || Lutaml::Model::Config.default_register,
|
|
55
|
+
)
|
|
56
|
+
return nil if plan[:ordered] && instance.element_order.nil?
|
|
57
|
+
|
|
58
|
+
# The native lane first: purely namespace-less trees emit
|
|
59
|
+
# through the engine's result builder and writers, all-C
|
|
60
|
+
# after the ops list — 2.4x the DOM-mint loop on the
|
|
61
|
+
# audit corpus. Everything else keeps the DOM lane below
|
|
62
|
+
# (namespaced spellings are instance-dependent; ordered,
|
|
63
|
+
# wildcard, raw, and callback shapes are not
|
|
64
|
+
# ops-expressible).
|
|
65
|
+
return NativeEmission.emit(instance, plan, register) if NativeEmission.compatible?(plan)
|
|
66
|
+
|
|
67
|
+
doc = ::Leptris::XML::Document.create
|
|
68
|
+
ns_ctx = Namespaces.new(instance, plan, register)
|
|
69
|
+
return nil if ns_ctx.unspellable?
|
|
70
|
+
|
|
71
|
+
root = doc.create_element(
|
|
72
|
+
ns_ctx.spell(plan[:tree][:name], plan[:tree][:ns]),
|
|
73
|
+
)
|
|
74
|
+
doc.root = root
|
|
75
|
+
ns_ctx.declare_on(root)
|
|
76
|
+
# The interpretive writer's raw schemaLocation re-emission
|
|
77
|
+
# (handle_schema_location): the parse-captured string rides
|
|
78
|
+
# back as xsi:schemaLocation with its declaration.
|
|
79
|
+
if instance.is_a?(::Lutaml::Model::Serialize) &&
|
|
80
|
+
instance.raw_schema_location &&
|
|
81
|
+
!instance.raw_schema_location.empty?
|
|
82
|
+
root.set_attribute(
|
|
83
|
+
"xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"
|
|
84
|
+
)
|
|
85
|
+
root.set_attribute("xsi:schemaLocation",
|
|
86
|
+
instance.raw_schema_location)
|
|
87
|
+
end
|
|
88
|
+
built = if plan[:ordered]
|
|
89
|
+
build_ordered(root, instance, plan, doc, register, ns_ctx)
|
|
90
|
+
else
|
|
91
|
+
build(root, instance, plan, doc, register, ns_ctx)
|
|
92
|
+
end
|
|
93
|
+
return nil unless built
|
|
94
|
+
|
|
95
|
+
doc.to_xml(indent: 2, no_decl: true)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Namespace spelling for namespaced plans (#847): every ns
|
|
99
|
+
# form resolves to a wire spelling (prefix:local, or local
|
|
100
|
+
# under a default xmlns), and every distinct (prefix, uri)
|
|
101
|
+
# pair is declared exactly once on the output root in
|
|
102
|
+
# first-encounter order — the #1558 contract on the DOM
|
|
103
|
+
# path. A nested plan's own tree ns overrides the inherited
|
|
104
|
+
# one; rows without an ns form inherit.
|
|
105
|
+
# Namespace spelling sourced from the interpretive writer's
|
|
106
|
+
# own machinery: NamespaceCollector + DeclarationPlanner
|
|
107
|
+
# decide prefix-vs-default per namespace exactly as the
|
|
108
|
+
# interpretive path does — no heuristic divergence (the
|
|
109
|
+
# uniword FontScheme/Image regressions). Option-bearing
|
|
110
|
+
# calls (prefix: true) never reach here; the serializer's
|
|
111
|
+
# plain spelling is the interpretive writer's plain
|
|
112
|
+
# spelling.
|
|
113
|
+
class Namespaces
|
|
114
|
+
attr_reader :unspellable
|
|
115
|
+
|
|
116
|
+
def initialize(instance, plan, register)
|
|
117
|
+
@forms = {}
|
|
118
|
+
@hoisted = {}
|
|
119
|
+
@declared = {}
|
|
120
|
+
@unspellable = false
|
|
121
|
+
mapping = plan[:mapping]
|
|
122
|
+
# The interpretive writer plans from the BUILT element
|
|
123
|
+
# tree: absent attributes (nil values) and absent
|
|
124
|
+
# children contribute no namespace needs, so an element
|
|
125
|
+
# whose own namespace would otherwise be prefix-hoisted
|
|
126
|
+
# (W3C: prefixed attributes need a prefix declaration)
|
|
127
|
+
# falls back to default-ns format. Collecting from the
|
|
128
|
+
# instance reproduces that — collecting from nil (type
|
|
129
|
+
# analysis) hoists every declared need and diverges
|
|
130
|
+
# (`<w:b xmlns:w>` vs the interpretive `<b xmlns>` when
|
|
131
|
+
# val=true omits the only prefixed attribute).
|
|
132
|
+
ns_prefix = if instance.is_a?(::Lutaml::Model::Serialize)
|
|
133
|
+
instance.xml_namespace_prefix
|
|
134
|
+
end
|
|
135
|
+
collect_options = { mapper_class: instance.class }
|
|
136
|
+
if ns_prefix && !ns_prefix.empty?
|
|
137
|
+
collect_options[:__xml_namespace_prefix] = ns_prefix
|
|
138
|
+
end
|
|
139
|
+
needs = Lutaml::Xml::NamespaceCollector.new(register).collect(
|
|
140
|
+
instance, mapping, **collect_options
|
|
141
|
+
)
|
|
142
|
+
planner = Lutaml::Xml::DeclarationPlanner.new(register)
|
|
143
|
+
declared_plan = planner.plan(
|
|
144
|
+
instance.class, mapping, needs, options: {}
|
|
145
|
+
)
|
|
146
|
+
declared_plan.root_node&.hoisted_declarations&.each do |prefix, uri|
|
|
147
|
+
@hoisted[uri] = prefix
|
|
148
|
+
@forms[uri] = { prefix: prefix, uri: uri }
|
|
149
|
+
end
|
|
150
|
+
# Format preservation (the interpretive writer's
|
|
151
|
+
# rule): an instance parsed from a PREFIXED source keeps
|
|
152
|
+
# its prefixes — the model's own namespace hoists with
|
|
153
|
+
# the instance's recorded prefix even when the fresh
|
|
154
|
+
# planner preferred the default-ns format.
|
|
155
|
+
model_uri = plan[:tree][:ns] && plan[:tree][:ns][:exact]
|
|
156
|
+
if ns_prefix && !ns_prefix.empty? && model_uri &&
|
|
157
|
+
@hoisted.key?(model_uri)
|
|
158
|
+
@hoisted[model_uri] = ns_prefix
|
|
159
|
+
@forms[model_uri] = { prefix: ns_prefix, uri: model_uri }
|
|
160
|
+
end
|
|
161
|
+
# Source declaration preservation (the interpretive
|
|
162
|
+
# writer's stored-plan merge): prefixes the parsed source
|
|
163
|
+
# declared are kept for URIs the document emits. The
|
|
164
|
+
# planner's class-analysis path has no stored-plan
|
|
165
|
+
# access, so the merge reads the instance's recorded plan
|
|
166
|
+
# directly.
|
|
167
|
+
stored = if instance.is_a?(::Lutaml::Model::Serialize)
|
|
168
|
+
instance.import_declaration_plan
|
|
169
|
+
end
|
|
170
|
+
if stored.is_a?(Lutaml::Xml::DeclarationPlan) &&
|
|
171
|
+
stored.root_node.is_a?(
|
|
172
|
+
Lutaml::Xml::DeclarationPlan::ElementNode,
|
|
173
|
+
) && (ns_prefix.nil? || ns_prefix.empty?)
|
|
174
|
+
original_uris = stored.original_namespace_uris || {}
|
|
175
|
+
stored.root_node.hoisted_declarations.each do |prefix, uri|
|
|
176
|
+
effective_uri = original_uris[uri] || uri
|
|
177
|
+
already = @hoisted.value?(effective_uri) ||
|
|
178
|
+
@hoisted.value?(uri)
|
|
179
|
+
@hoisted[prefix] = uri if already && !@hoisted.key?(prefix)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
collect(plan[:tree])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def unspellable?
|
|
186
|
+
@unspellable
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def collect(tree)
|
|
190
|
+
check(tree[:ns])
|
|
191
|
+
(tree[:attributes] || []).each { |a| check(a[:ns]) }
|
|
192
|
+
(tree[:children] || []).each do |row|
|
|
193
|
+
check(row[:ns]) if row[:ns].is_a?(Hash)
|
|
194
|
+
collect(row[:plan]) if row[:kind] == :nested && row[:plan]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# An exact form the planner did not declare cannot be
|
|
199
|
+
# spelled — the serializer falls back interpretively.
|
|
200
|
+
def check(ns_form)
|
|
201
|
+
return if ns_form.nil? || ns_form == :none || ns_form == :any
|
|
202
|
+
|
|
203
|
+
uri = ns_form[:exact]
|
|
204
|
+
@unspellable = true if uri && !@forms.key?(uri)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The planner's hoisted prefix for a URI (nil when hoisted
|
|
208
|
+
# as the default namespace).
|
|
209
|
+
def prefix_for(uri)
|
|
210
|
+
@hoisted[uri]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The wire spelling for a local name under this form: the
|
|
214
|
+
# planner's hoisted prefix for the URI, or unprefixed when
|
|
215
|
+
# hoisted as the default namespace.
|
|
216
|
+
def spell(local, ns_form = nil)
|
|
217
|
+
uri = if ns_form.is_a?(Hash)
|
|
218
|
+
ns_form[:exact]
|
|
219
|
+
else
|
|
220
|
+
@forms.key?(ns_form) ? @forms[ns_form][:uri] : nil
|
|
221
|
+
end
|
|
222
|
+
prefix = uri ? @hoisted[uri] : nil
|
|
223
|
+
prefix ? "#{prefix}:#{local}" : local
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def form_for(ns_form)
|
|
227
|
+
return nil if ns_form.nil? || ns_form == :none
|
|
228
|
+
|
|
229
|
+
ns_form.is_a?(Hash) ? { prefix: @hoisted[ns_form[:exact]], uri: ns_form[:exact] } : @forms[ns_form]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Declaration order matches the interpretive writer: the
|
|
233
|
+
# prefixed namespaces sorted by prefix, the default
|
|
234
|
+
# namespace last.
|
|
235
|
+
def declare_on(root, _attr_form_prefix = nil)
|
|
236
|
+
@hoisted.sort_by { |_uri, prefix| [prefix ? 0 : 1, prefix.to_s] }
|
|
237
|
+
.each do |uri, prefix|
|
|
238
|
+
key = uri
|
|
239
|
+
next if key.nil? || @declared[key]
|
|
240
|
+
|
|
241
|
+
@declared[key] = true
|
|
242
|
+
root.add_namespace_definition(prefix, uri)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
248
|
+
# models serialize through build_ordered from element_order;
|
|
249
|
+
# plain models through plan rows.
|
|
250
|
+
def serializable?(plan)
|
|
251
|
+
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
252
|
+
PLAN_SERIALIZABLE_KINDS.include?(kind)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def register
|
|
259
|
+
Lutaml::Model::Config.default_register
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def build(element, instance, plan, doc, reg = nil, ns_ctx = nil,
|
|
263
|
+
inherited_ns = nil)
|
|
264
|
+
reg ||= register
|
|
265
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
266
|
+
ns_index = ns_index_for(plan)
|
|
267
|
+
nested_plans = nested_plans_for(plan)
|
|
268
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
269
|
+
value = value_of(instance, attr, delegate)
|
|
270
|
+
next unless rule.render?(value, instance)
|
|
271
|
+
|
|
272
|
+
case kind
|
|
273
|
+
when :scalar
|
|
274
|
+
# Multi-capture parity: an attribute that collected
|
|
275
|
+
# several occurrences serializes one element per item,
|
|
276
|
+
# as the interpretive writer does. A plain scalar skips
|
|
277
|
+
# the Array() wrap — the common case.
|
|
278
|
+
row_ns = ns_index[row_name(rule, spelling)] || inherited_ns
|
|
279
|
+
if value.is_a?(::Array)
|
|
280
|
+
value.each do |item|
|
|
281
|
+
add_leaf(element,
|
|
282
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
283
|
+
attr.serialize(item, :xml, reg), doc,
|
|
284
|
+
attrs: rule.when_attribute)
|
|
285
|
+
end
|
|
286
|
+
else
|
|
287
|
+
add_leaf(element,
|
|
288
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
289
|
+
attr.serialize(value, :xml, reg), doc,
|
|
290
|
+
attrs: rule.when_attribute)
|
|
291
|
+
end
|
|
292
|
+
when :collection_native, :collection_cb
|
|
293
|
+
row_ns = ns_index[rule.name.to_s] || inherited_ns
|
|
294
|
+
Array(value).each do |item|
|
|
295
|
+
add_leaf(element,
|
|
296
|
+
ns_ctx ? ns_ctx.spell(rule.name.to_s, row_ns) : rule.name.to_s,
|
|
297
|
+
attr.serialize(item, :xml, reg), doc,
|
|
298
|
+
attrs: rule.when_attribute)
|
|
299
|
+
end
|
|
300
|
+
when :nested, :ordered_deferred
|
|
301
|
+
Array(value).each do |item|
|
|
302
|
+
# Keyed by [class, row name]: distinct rows (sz, szCs)
|
|
303
|
+
# can share one class with different root names.
|
|
304
|
+
child_plan = nested_plans[[item.class, rule.name.to_s]] ||
|
|
305
|
+
(nested_plans[[item.class, rule.name.to_s]] = PlanCompiler.compile(item.class, reg))
|
|
306
|
+
child_ns = child_plan[:tree][:ns] || inherited_ns
|
|
307
|
+
# The ROW's wire name wins over the child plan's
|
|
308
|
+
# root — distinct rows (sz, szCs) share one class.
|
|
309
|
+
child = element.create_child(
|
|
310
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), child_ns) : row_name(rule, spelling),
|
|
311
|
+
)
|
|
312
|
+
ns_ctx&.collect(child_plan[:tree])
|
|
313
|
+
if child_plan[:ordered]
|
|
314
|
+
build_ordered(child, item, child_plan,
|
|
315
|
+
doc, reg, ns_ctx, child_ns) || return
|
|
316
|
+
else
|
|
317
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
when :raw
|
|
321
|
+
Array(value).each { |raw| element.add_child(raw.to_s) }
|
|
322
|
+
when :child_deferred
|
|
323
|
+
# A plan-less child carrying a value (a cycle point
|
|
324
|
+
# with data, e.g. bibliography's Author → author):
|
|
325
|
+
# the interpretive serializer owns the model — the
|
|
326
|
+
# plan lane has no faithful spelling for it. Empty
|
|
327
|
+
# deferred rows ride the fast lane untouched.
|
|
328
|
+
return nil unless Array(value).empty?
|
|
329
|
+
when :content, :content_deferred
|
|
330
|
+
Array(value).each { |run| append_text(element, run.to_s, doc) }
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Per-plan lazy indexes: the name→ns lookup and the
|
|
336
|
+
# per-class child-plan resolution are per-instance calls on
|
|
337
|
+
# the serialize hot path — memoized once per compiled plan
|
|
338
|
+
# they cost nothing per row (the old per-row children#find
|
|
339
|
+
# plus PlanCompiler key arrays were ~4 allocs/row).
|
|
340
|
+
def ns_index_for(plan)
|
|
341
|
+
plan[:ns_index] ||= begin
|
|
342
|
+
h = {}
|
|
343
|
+
(plan[:tree][:children] || []).each do |r|
|
|
344
|
+
h[r[:name]] = r[:ns] unless h.key?(r[:name])
|
|
345
|
+
end
|
|
346
|
+
h
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def nested_plans_for(plan)
|
|
351
|
+
plan[:nested_plans] ||= {}
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The row's exact element URI — the child type's namespace,
|
|
355
|
+
# the same source the interpretive matcher dispatches by for
|
|
356
|
+
# (URI, local) element identity. Nil for scalar/raw/content
|
|
357
|
+
# rows (any-qualification binding). Memoized per plan (rules
|
|
358
|
+
# are stable per compiled plan).
|
|
359
|
+
def row_exact_uri(rule, attr, kind, type, reg, plan)
|
|
360
|
+
return nil unless %i[nested ordered_deferred].include?(kind)
|
|
361
|
+
|
|
362
|
+
cache = plan[:row_exact_uris] ||= {}.compare_by_identity
|
|
363
|
+
return cache[rule] if cache.key?(rule)
|
|
364
|
+
|
|
365
|
+
child_type = type || attr.type(reg)
|
|
366
|
+
child_plan = child_type && PlanCompiler.compile(child_type, reg)
|
|
367
|
+
tree_ns = child_plan && child_plan[:tree][:ns]
|
|
368
|
+
cache[rule] = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def ordered_layout_for(plan, inherited_ns, ns_index, reg)
|
|
372
|
+
cache = plan[:ordered_layouts] ||= {}.compare_by_identity
|
|
373
|
+
return cache[inherited_ns] if cache.key?(inherited_ns)
|
|
374
|
+
|
|
375
|
+
rows_by_name = {}
|
|
376
|
+
name_keys = ::Hash.new { |h, k| h[k] = [] }
|
|
377
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate, type|
|
|
378
|
+
next unless rule.name
|
|
379
|
+
|
|
380
|
+
exact = row_exact_uri(rule, attr, kind, type, reg, plan)
|
|
381
|
+
name = rule.name.to_s
|
|
382
|
+
key = exact ? [exact, name] : [nil, name]
|
|
383
|
+
rows_by_name[key] ||= [rule, attr, kind, spelling,
|
|
384
|
+
delegate,
|
|
385
|
+
ns_index ? ns_index[name] || inherited_ns : inherited_ns]
|
|
386
|
+
name_keys[name] << key
|
|
387
|
+
end
|
|
388
|
+
name_keys.default = nil
|
|
389
|
+
cache[inherited_ns] = [rows_by_name, name_keys]
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Element_order-driven emission (the interpretive order
|
|
393
|
+
# applier's contract): text runs and comments verbatim, each
|
|
394
|
+
# element entry consuming the next item of its collection
|
|
395
|
+
# (document order). Text reads prefer the content attribute
|
|
396
|
+
# when it lines up with the text-node count, so mutations to
|
|
397
|
+
# the model are reflected; element_order text is the fallback.
|
|
398
|
+
# PIs drop (interpretive parity). A nested ordered instance
|
|
399
|
+
# without element_order (built by Ruby, not parsed) falls
|
|
400
|
+
# back to declaration order — the interpretive behavior.
|
|
401
|
+
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
402
|
+
ns_ctx = nil, inherited_ns = nil)
|
|
403
|
+
if instance.element_order.nil?
|
|
404
|
+
return build(element, instance, plan, doc, reg, ns_ctx,
|
|
405
|
+
inherited_ns)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
reg ||= register
|
|
409
|
+
ns_ctx&.collect(plan[:tree])
|
|
410
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
411
|
+
ns_index = ns_ctx ? ns_index_for(plan) : nil
|
|
412
|
+
# (URI, local) row keys — the interpretive matcher's
|
|
413
|
+
# typed-element identity: same-name rows in different
|
|
414
|
+
# namespaces (w14:docId vs w15:docId) must not share one
|
|
415
|
+
# dispatch slot. Name-keyed rows (no exact URI) bind any
|
|
416
|
+
# qualification. The layout is static per (plan, inherited
|
|
417
|
+
# ns) — the reconciliation corpus re-serializes thousands
|
|
418
|
+
# of same-shaped paragraphs, so it is memoized.
|
|
419
|
+
rows_by_name, name_keys =
|
|
420
|
+
ordered_layout_for(plan, inherited_ns, ns_index, reg)
|
|
421
|
+
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
422
|
+
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
423
|
+
end
|
|
424
|
+
# Writer-added values sit outside the order snapshot — the
|
|
425
|
+
# reconciler inserts their entries at the interpretive
|
|
426
|
+
# anchors and returns the rules it could not place for
|
|
427
|
+
# appended emission (silent drops are data loss). The
|
|
428
|
+
# reconciler speaks CompiledRule — the interpretive
|
|
429
|
+
# transformation's memoized set; the plan's MappingRule
|
|
430
|
+
# rows cannot drive it. Pristine parsed instances skip the
|
|
431
|
+
# scan entirely: the parse-built order covers every set
|
|
432
|
+
# value, and the writer-set flag is the only flow that can
|
|
433
|
+
# produce deficits.
|
|
434
|
+
rules = if instance.mutated_after_parse?
|
|
435
|
+
compiled_rules_for(instance.class, reg)
|
|
436
|
+
end
|
|
437
|
+
if rules
|
|
438
|
+
order, fallback_rules =
|
|
439
|
+
RECONCILER.reconciled_element_order(instance, rules, {})
|
|
440
|
+
else
|
|
441
|
+
order = instance.element_order
|
|
442
|
+
fallback_rules = []
|
|
443
|
+
end
|
|
444
|
+
element_indices = ::Hash.new(0)
|
|
445
|
+
content_attr = content_entry && content_entry[1]
|
|
446
|
+
content_value = content_attr &&
|
|
447
|
+
value_of(instance, content_attr, content_entry[4])
|
|
448
|
+
text_node_count = order.count { |o| o.type == "Text" }
|
|
449
|
+
use_content_index = content_value.is_a?(Array) &&
|
|
450
|
+
content_value.length == text_node_count
|
|
451
|
+
content_cdata = content_entry && content_entry[0].cdata
|
|
452
|
+
text_node_index = 0
|
|
453
|
+
|
|
454
|
+
order.each do |object|
|
|
455
|
+
case object.type
|
|
456
|
+
when "Text"
|
|
457
|
+
text = if content_attr && !content_value.nil?
|
|
458
|
+
if use_content_index
|
|
459
|
+
content_value[text_node_index].to_s
|
|
460
|
+
elsif !content_value.is_a?(Array) && text_node_count <= 1
|
|
461
|
+
content_value.to_s
|
|
462
|
+
else
|
|
463
|
+
object.text_content || object.name
|
|
464
|
+
end
|
|
465
|
+
else
|
|
466
|
+
object.text_content || object.name
|
|
467
|
+
end
|
|
468
|
+
text_node_index += 1
|
|
469
|
+
next if text.nil?
|
|
470
|
+
# Mixed content keeps whitespace; ordered-only skips it
|
|
471
|
+
next if content_attr.nil? && text.strip.empty?
|
|
472
|
+
|
|
473
|
+
element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
|
|
474
|
+
when "Comment"
|
|
475
|
+
element.add_child(doc.create_comment(object.text_content))
|
|
476
|
+
when "Element"
|
|
477
|
+
# A uniquely-named row absorbs the entry at any
|
|
478
|
+
# qualification (the interpretive matcher's lenient
|
|
479
|
+
# binding — order entries written by consumers carry no
|
|
480
|
+
# namespace). Contested names dispatch strictly by
|
|
481
|
+
# (URI, local), falling back to URI-less rows.
|
|
482
|
+
name = object.name
|
|
483
|
+
keys = name_keys[name]
|
|
484
|
+
if keys.length == 1
|
|
485
|
+
dispatch_key = keys.first
|
|
486
|
+
entry = rows_by_name[dispatch_key]
|
|
487
|
+
else
|
|
488
|
+
dispatch_key = [object.namespace_uri, name]
|
|
489
|
+
entry = rows_by_name[dispatch_key]
|
|
490
|
+
unless entry
|
|
491
|
+
dispatch_key = [nil, name]
|
|
492
|
+
entry = rows_by_name[dispatch_key]
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
next unless entry
|
|
496
|
+
|
|
497
|
+
_, attr, kind, _spelling, delegate, row_ns = entry
|
|
498
|
+
value = value_of(instance, attr, delegate)
|
|
499
|
+
case kind
|
|
500
|
+
when :collection_native, :collection_cb
|
|
501
|
+
index = element_indices[dispatch_key]
|
|
502
|
+
items = Array(value)
|
|
503
|
+
next unless index < items.length
|
|
504
|
+
|
|
505
|
+
element_indices[dispatch_key] += 1
|
|
506
|
+
add_leaf(element,
|
|
507
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
508
|
+
attr.serialize(items[index], :xml, reg), doc)
|
|
509
|
+
when :nested, :ordered_deferred
|
|
510
|
+
item = if attr.collection?
|
|
511
|
+
index = element_indices[dispatch_key]
|
|
512
|
+
element_indices[dispatch_key] += 1
|
|
513
|
+
Array(value)[index]
|
|
514
|
+
else
|
|
515
|
+
value
|
|
516
|
+
end
|
|
517
|
+
next unless item
|
|
518
|
+
|
|
519
|
+
child_plan = nested_plans_for(plan)[[item.class, object.name]] ||
|
|
520
|
+
(nested_plans_for(plan)[[item.class, object.name]] = PlanCompiler.compile(item.class, reg))
|
|
521
|
+
child_ns = child_plan[:tree][:ns] || row_ns || inherited_ns
|
|
522
|
+
child = element.create_child(
|
|
523
|
+
ns_ctx ? ns_ctx.spell(object.name, child_ns) : object.name,
|
|
524
|
+
)
|
|
525
|
+
if child_plan[:ordered]
|
|
526
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
527
|
+
doc, reg, ns_ctx, child_ns)
|
|
528
|
+
else
|
|
529
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
530
|
+
end
|
|
531
|
+
when :raw
|
|
532
|
+
element.add_child(value.to_s) unless value.nil?
|
|
533
|
+
when :child_deferred
|
|
534
|
+
# Valued cycle-point children: interpretive lane (see
|
|
535
|
+
# build).
|
|
536
|
+
return nil unless Array(value).empty?
|
|
537
|
+
when :scalar
|
|
538
|
+
add_leaf(element,
|
|
539
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
540
|
+
attr.serialize(value, :xml, reg), doc)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
return nil if !fallback_rules.empty? &&
|
|
545
|
+
!emit_uncovered_values(element, instance, plan, fallback_rules,
|
|
546
|
+
doc, reg, ns_ctx, inherited_ns)
|
|
547
|
+
|
|
548
|
+
true
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# OrderReconciler fallback: element rules the reconciled order
|
|
552
|
+
# could not place but that still hold a value — emit them
|
|
553
|
+
# appended, the ordinary way (the interpretive
|
|
554
|
+
# apply_remaining_rules contract for the plan row model).
|
|
555
|
+
def emit_uncovered_values(element, instance, plan, fallback, doc,
|
|
556
|
+
reg, ns_ctx, inherited_ns)
|
|
557
|
+
by_attr = {}
|
|
558
|
+
plan[:rows].each { |row| by_attr[row[1].name] ||= row if row[1] }
|
|
559
|
+
fallback.each do |rule|
|
|
560
|
+
entry = by_attr[rule.attribute_name]
|
|
561
|
+
next unless entry
|
|
562
|
+
|
|
563
|
+
_rule, attr, kind, _spelling, delegate, row_ns = entry
|
|
564
|
+
value = value_of(instance, attr, delegate)
|
|
565
|
+
name = rule.name.to_s
|
|
566
|
+
Array(value).each do |item|
|
|
567
|
+
case kind
|
|
568
|
+
when :collection_native, :collection_cb, :scalar
|
|
569
|
+
add_leaf(element,
|
|
570
|
+
ns_ctx ? ns_ctx.spell(name, row_ns) : name,
|
|
571
|
+
attr.serialize(item, :xml, reg), doc)
|
|
572
|
+
when :nested, :ordered_deferred
|
|
573
|
+
next if item.nil?
|
|
574
|
+
|
|
575
|
+
child_plan =
|
|
576
|
+
nested_plans_for(plan)[[item.class, name]] ||
|
|
577
|
+
(nested_plans_for(plan)[[item.class, name]] =
|
|
578
|
+
PlanCompiler.compile(item.class, reg))
|
|
579
|
+
child_ns = child_plan[:tree][:ns] || row_ns || inherited_ns
|
|
580
|
+
child = element.create_child(
|
|
581
|
+
ns_ctx ? ns_ctx.spell(name, child_ns) : name,
|
|
582
|
+
)
|
|
583
|
+
if child_plan[:ordered]
|
|
584
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
585
|
+
doc, reg, ns_ctx, child_ns)
|
|
586
|
+
else
|
|
587
|
+
built = build(child, item, child_plan, doc, reg, ns_ctx,
|
|
588
|
+
child_ns)
|
|
589
|
+
return nil unless built
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
# Valued cycle-point rows keep the walk's contract.
|
|
594
|
+
return nil if kind == :child_deferred && !Array(value).empty?
|
|
595
|
+
end
|
|
596
|
+
true
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def compiled_rules_for(klass, reg)
|
|
600
|
+
klass.transformation_for(:xml, reg)&.compiled_rules
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Attributes in document order when the instance recorded it
|
|
604
|
+
# (parsed models), else plan row order.
|
|
605
|
+
def write_attributes(element, instance, plan, reg = nil,
|
|
606
|
+
ns_ctx = nil)
|
|
607
|
+
recorded = instance.attribute_order
|
|
608
|
+
attr_prefixes = nil
|
|
609
|
+
if ns_ctx && !(plan[:tree][:attributes] || []).empty?
|
|
610
|
+
attr_prefixes = {}
|
|
611
|
+
(plan[:tree][:attributes] || []).each do |a|
|
|
612
|
+
form = a[:ns].is_a?(Hash) ? a[:ns] : nil
|
|
613
|
+
# Namespaced attributes always spell prefixed (W3C:
|
|
614
|
+
# attributes cannot use the default namespace) — the
|
|
615
|
+
# interpretive writer emits the type namespace's own
|
|
616
|
+
# prefix even when the element hoists the same URI as
|
|
617
|
+
# the default namespace (updateFields w:val under
|
|
618
|
+
# xmlns="...").
|
|
619
|
+
attr_prefixes[a[:name]] =
|
|
620
|
+
if form
|
|
621
|
+
form[:prefix] || ns_ctx.prefix_for(form[:exact])
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
if recorded && !recorded.empty?
|
|
626
|
+
by_name = {}
|
|
627
|
+
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
628
|
+
by_name[rule.name.to_s] = [rule, attr, delegate]
|
|
629
|
+
end
|
|
630
|
+
names = recorded |
|
|
631
|
+
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
632
|
+
names.each do |name|
|
|
633
|
+
write_attribute(element, instance, name, by_name[name],
|
|
634
|
+
reg, attr_prefixes && attr_prefixes[name])
|
|
635
|
+
end
|
|
636
|
+
else
|
|
637
|
+
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
638
|
+
write_attribute(element, instance, rule.name.to_s,
|
|
639
|
+
[rule, attr, delegate], reg,
|
|
640
|
+
attr_prefixes && attr_prefixes[rule.name.to_s])
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def write_attribute(element, instance, name, entry, reg = nil,
|
|
646
|
+
prefix = nil)
|
|
647
|
+
return unless entry
|
|
648
|
+
|
|
649
|
+
rule, attr, delegate = entry
|
|
650
|
+
value = value_of(instance, attr, delegate)
|
|
651
|
+
return if value.nil?
|
|
652
|
+
return unless rule.render?(value, instance)
|
|
653
|
+
|
|
654
|
+
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
655
|
+
element.set_attribute(spelled,
|
|
656
|
+
attr.serialize(value, :xml, reg || register).to_s)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Partition rows (#88) re-emit their discriminator: the wire
|
|
660
|
+
# element carries the when_attribute pairs so the round trip
|
|
661
|
+
# re-routes to the same attribute on reparse.
|
|
662
|
+
def add_leaf(element, name, value, doc, attrs: nil)
|
|
663
|
+
child = element.create_child(name)
|
|
664
|
+
attrs&.each { |k, v| child[k.to_s] = v.to_s }
|
|
665
|
+
append_text(child, value.to_s, doc) unless value.nil?
|
|
666
|
+
child
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Text attach: the address face (leptris#408 adoption)
|
|
670
|
+
# passes raw node addresses — the pointer face mints an
|
|
671
|
+
# FFI::Pointer wrapper per fresh node, the last per-row
|
|
672
|
+
# Ruby-side allocation on the DOM build.
|
|
673
|
+
def append_text(parent, text, doc)
|
|
674
|
+
node = doc.create_text_node(text)
|
|
675
|
+
status = ::Leptris::XML::FFI.leptris_element_add_child_addr(
|
|
676
|
+
parent.c_address, node.c_address
|
|
677
|
+
)
|
|
678
|
+
unless status.zero?
|
|
679
|
+
raise ::Leptris::XML::Error,
|
|
680
|
+
"text attach failed (status=#{status})"
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
node
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def value_of(instance, attr, delegate)
|
|
687
|
+
if delegate
|
|
688
|
+
target = instance.public_send(delegate)
|
|
689
|
+
target&.public_send(attr.name)
|
|
690
|
+
else
|
|
691
|
+
instance.public_send(attr.name)
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def row_name(rule, spelling)
|
|
696
|
+
(spelling || rule.name).to_s
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
end
|