lutaml-model 0.8.84 → 0.8.85
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/lib/compat/opal/lutaml_model_boot.rb +6 -5
- data/lib/lutaml/key_value/transform.rb +3 -1
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +5 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +3 -5
- data/lib/lutaml/xml/leptris/plan_compiler.rb +372 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +441 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +273 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +32 -0
- data/lib/lutaml/xml/leptris.rb +33 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +1 -1
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/xml/plan_fast_path_spec.rb +8 -8
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- metadata +7 -6
- data/lib/lutaml/xml/plan_compiler.rb +0 -362
- data/lib/lutaml/xml/plan_hydrator.rb +0 -464
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -271
- data/lib/lutaml/xml/plan_walk.rb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fcfb57e6eb2cff5a7cd47dbe44e7e2baceb10ea5e8f8c22c13fa712d5c8a1fb
|
|
4
|
+
data.tar.gz: 35c30ed77dfee8d8a06a7a9f8f32f94178c3f092f54d949f831f6a29842d6b27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45bf3b55c3d356307035278d46709818fdd8ba4a1520741a58117f7419263b3c438f57097a8e736f1a87dac765bc6523e12479dd54b408aee44a23577f9d3726
|
|
7
|
+
data.tar.gz: 0c6a92ec24a172e016ee0484a2579efed9c3f0c5c653e85ad2509ae441bb71fcffd6c68c4f72d17a250f4587e21af6072116783cf8229cb997692c48cdcc3d1d
|
|
@@ -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,11 @@ 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/plan_compiler"
|
|
386
|
+
require "lutaml/xml/leptris/plan_hydrator"
|
|
387
|
+
require "lutaml/xml/leptris/plan_order"
|
|
388
|
+
require "lutaml/xml/leptris/plan_serializer"
|
|
389
|
+
require "lutaml/xml/leptris/plan_walk"
|
|
389
390
|
require "lutaml/xml/oga/element"
|
|
390
391
|
require "lutaml/xml/ox/element"
|
|
391
392
|
require "lutaml/xml/rexml/element"
|
|
@@ -345,7 +345,9 @@ options)
|
|
|
345
345
|
end
|
|
346
346
|
|
|
347
347
|
def valid_mapping?(rule, options)
|
|
348
|
-
|
|
348
|
+
# rule is always Lutaml::Model::MappingRule; serialize? is
|
|
349
|
+
# statically defined (the hydrate-only marker).
|
|
350
|
+
return false unless rule.serialize?
|
|
349
351
|
|
|
350
352
|
only = options[:only]
|
|
351
353
|
except = options[:except]
|
|
@@ -3,6 +3,19 @@ module Lutaml
|
|
|
3
3
|
class Mapping
|
|
4
4
|
include DeepDupable
|
|
5
5
|
|
|
6
|
+
# Uniform interface across mapping families: every mapping
|
|
7
|
+
# answers these, so consumers call them unconditionally. The
|
|
8
|
+
# families that carry the feature override (KeyValue's
|
|
9
|
+
# root_mappings, Xml's namespace_class); the base defaults make
|
|
10
|
+
# the absence explicit instead of probed.
|
|
11
|
+
def root_mappings
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def namespace_class
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
6
19
|
attr_writer :mappings
|
|
7
20
|
|
|
8
21
|
def initialize
|
|
@@ -102,11 +102,11 @@ module Lutaml
|
|
|
102
102
|
return nil unless adapter_name.to_s.end_with?("LeptrisAdapter")
|
|
103
103
|
|
|
104
104
|
register = Lutaml::Model::Config.default_register
|
|
105
|
-
plan = Lutaml::Xml::PlanCompiler.compile(self, register)
|
|
105
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
106
106
|
return nil unless plan && !plan[:namespaced] &&
|
|
107
|
-
Lutaml::Xml::PlanSerializer.serializable?(plan)
|
|
107
|
+
Lutaml::Xml::Leptris::PlanSerializer.serializable?(plan)
|
|
108
108
|
|
|
109
|
-
Lutaml::Xml::PlanSerializer.call(instance, plan)
|
|
109
|
+
Lutaml::Xml::Leptris::PlanSerializer.call(instance, plan)
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
# Whole-document native materialization (Phase 5): compile the
|
|
@@ -126,14 +126,14 @@ module Lutaml
|
|
|
126
126
|
return nil unless adapter_name.to_s.end_with?("LeptrisAdapter")
|
|
127
127
|
|
|
128
128
|
register = Lutaml::Model::Config.default_register
|
|
129
|
-
plan = Lutaml::Xml::PlanCompiler.compile(self, register)
|
|
129
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
130
130
|
return nil unless plan
|
|
131
131
|
|
|
132
132
|
root = ::Leptris::XML.parse(data.to_s).root
|
|
133
133
|
return nil if root.nil?
|
|
134
134
|
return nil unless root.name == plan[:tree][:name]
|
|
135
135
|
|
|
136
|
-
Lutaml::Xml::PlanHydrator.call(self, plan,
|
|
136
|
+
Lutaml::Xml::Leptris::PlanHydrator.call(self, plan,
|
|
137
137
|
plan[:descriptor].walk(root),
|
|
138
138
|
node: root)
|
|
139
139
|
rescue ::Leptris::XML::ParseError => e
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -22,11 +22,9 @@ end
|
|
|
22
22
|
module Lutaml
|
|
23
23
|
module Xml
|
|
24
24
|
autoload :HtmlEntities, "#{__dir__}/html_entities"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
autoload :
|
|
28
|
-
autoload :PlanSerializer, "#{__dir__}/plan_serializer"
|
|
29
|
-
autoload :PlanWalk, "#{__dir__}/plan_walk"
|
|
25
|
+
# The leptris plan fast path lives under its own namespace; its
|
|
26
|
+
# autoloads and the leptris version gate live in lutaml/xml/leptris.
|
|
27
|
+
autoload :Leptris, "#{__dir__}/leptris"
|
|
30
28
|
# Error module for XML-specific errors
|
|
31
29
|
module Error
|
|
32
30
|
autoload :XmlError, "#{__dir__}/error/xml_error"
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Phase 5 slice: compile a model's XML mapping into a
|
|
7
|
+
# Leptris::XML::Descriptor plan and materialize whole documents in
|
|
8
|
+
# one native pass (leptris_plan_walk) — no moxml wrapper tree, no
|
|
9
|
+
# per-element Ruby dispatch. Measured on the 200-item probe:
|
|
10
|
+
# walk+hydrate 8.7x faster, 82% fewer allocations than the
|
|
11
|
+
# interpretive path, hydration-equal output.
|
|
12
|
+
#
|
|
13
|
+
# A model compiles when EVERY rule is plan-shaped; richer rows
|
|
14
|
+
# defer their subtree verbatim and interpret post-walk rather
|
|
15
|
+
# than opting the whole model out:
|
|
16
|
+
# - map_attribute / map_element / content / raw rows
|
|
17
|
+
# - Value-scalar types or nested Serializables
|
|
18
|
+
# - custom methods, polymorphism, unions, and ordered/mixed
|
|
19
|
+
# children capture :raw and interpret post-walk
|
|
20
|
+
# - ordered/mixed root mappings compile; the entry points
|
|
21
|
+
# reconstruct element_order from the node surface
|
|
22
|
+
# - attributes not derived/union/polymorphic
|
|
23
|
+
#
|
|
24
|
+
# The fast path is opt-in (Config.xml_plan_fast_path) while the
|
|
25
|
+
# full semantics audit (parent links, consolidation, ordering
|
|
26
|
+
# metadata) completes.
|
|
27
|
+
module PlanCompiler
|
|
28
|
+
# TODO.perf/15: env-gated histogram of why models opt out of the
|
|
29
|
+
# plan path. Set PLAN_COMPILE_STATS=1 and read PlanCompiler.stats
|
|
30
|
+
# after a parse. Zero cost when disabled.
|
|
31
|
+
def self.stats
|
|
32
|
+
@stats ||= Hash.new(0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.opt_out!(clause)
|
|
36
|
+
@plan_stats ||= Hash.new(0)
|
|
37
|
+
@plan_stats[clause] += 1 if ENV["PLAN_COMPILE_STATS"]
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Isolated holder: suites freeze model classes; a cache on a
|
|
42
|
+
# frozen constant would be immutable (TypeProbeCache precedent).
|
|
43
|
+
PLAN_CACHE = ::Class.new do
|
|
44
|
+
class << self
|
|
45
|
+
def cache
|
|
46
|
+
@cache ||= {}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end.cache
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
def compile(model_class, register)
|
|
53
|
+
# Version gate, not a capability probe: the plan path needs
|
|
54
|
+
# leptris >= 1.9.273.0 (the child-handles snapshot). Older
|
|
55
|
+
# gems take the interpretive path via the nil opt-out.
|
|
56
|
+
return nil unless Leptris.plan_path_compatible?
|
|
57
|
+
|
|
58
|
+
# A child model's declared lutaml_default_register takes
|
|
59
|
+
# precedence over the ambient (parent) register — the same
|
|
60
|
+
# contract the interpretive path applies through
|
|
61
|
+
# Register.resolve_for_child. Without this, plan compilation
|
|
62
|
+
# resolves the child's symbol attribute types in the parent
|
|
63
|
+
# context and raises UnknownTypeError for ids registered only
|
|
64
|
+
# in the child's own register (#876).
|
|
65
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
66
|
+
model_class, register
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
key = [model_class, register]
|
|
70
|
+
return PLAN_CACHE[key] if PLAN_CACHE.key?(key)
|
|
71
|
+
|
|
72
|
+
# Cycle guard: a self-referential model (JATS sec-in-sec) must
|
|
73
|
+
# resolve to nil — the interpretive pipeline owns it. The guard
|
|
74
|
+
# is THREAD-LOCAL (a shared in-progress set races: a concurrent
|
|
75
|
+
# same-key compile would cache false permanently — the #828
|
|
76
|
+
# lesson); the nil at the cycle point is NOT cached — the
|
|
77
|
+
# outermost build completes and caches the real verdict.
|
|
78
|
+
stack = (Thread.current[:plan_compiler_stack] ||= [])
|
|
79
|
+
return nil if stack.include?(key)
|
|
80
|
+
|
|
81
|
+
stack.push(key)
|
|
82
|
+
begin
|
|
83
|
+
PLAN_CACHE[key] = build(model_class, register)
|
|
84
|
+
ensure
|
|
85
|
+
stack.pop
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def build(model_class, register)
|
|
92
|
+
return nil unless model_class.is_a?(Class) &&
|
|
93
|
+
model_class.include?(::Lutaml::Model::Serialize)
|
|
94
|
+
|
|
95
|
+
mapping = model_class.mappings_for(:xml, register)
|
|
96
|
+
return nil unless compilable_mapping?(mapping)
|
|
97
|
+
|
|
98
|
+
rows = []
|
|
99
|
+
attr_rows = [] # [[rule, attr]] hydration metadata
|
|
100
|
+
plan_attrs = [] # [{name:, kind:}] rows for the engine plan
|
|
101
|
+
compiled = [] # [rule, attr, kind, spelling, delegate_target]
|
|
102
|
+
cdata = false
|
|
103
|
+
mixed_content = false
|
|
104
|
+
needs_nodes = false
|
|
105
|
+
collection_defaults = [] # collection attrs with element rows
|
|
106
|
+
tag = 100 # type_tag echo space for callback-routed rows
|
|
107
|
+
model_ns = plan_namespace(model_class, mapping, register)
|
|
108
|
+
|
|
109
|
+
# Collection rows are NATIVE since leptris 1.9.178 —
|
|
110
|
+
# collection values echo name and type_tag (the #220 gap is
|
|
111
|
+
# closed), so any number routes without callbacks.
|
|
112
|
+
true
|
|
113
|
+
|
|
114
|
+
mapping.mappings(register).each do |rule|
|
|
115
|
+
delegate_target = nil
|
|
116
|
+
attr = if rule.delegate
|
|
117
|
+
delegate_target = model_class.attributes(register)[rule.delegate]
|
|
118
|
+
t = delegate_target&.type(register)
|
|
119
|
+
if t.is_a?(Class) && t.include?(::Lutaml::Model::Serialize)
|
|
120
|
+
t.attributes(register)[rule.to]
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
model_class.attributes(register)[rule.to]
|
|
124
|
+
end
|
|
125
|
+
return opt_out!(:attr_nil) if attr.nil?
|
|
126
|
+
return opt_out!(:derived) if attr.derived?
|
|
127
|
+
|
|
128
|
+
# Partition rows (TODO 34 step 2) ride native predicates
|
|
129
|
+
# only in the plain-capture shape; any other when_attribute
|
|
130
|
+
# shape stays interpretive (the suite pins nested-target
|
|
131
|
+
# partitions to the interpretive path).
|
|
132
|
+
unless rule.when_attribute.empty?
|
|
133
|
+
t = attr.type(register)
|
|
134
|
+
plain_partition = !rule.delegate &&
|
|
135
|
+
!rule.has_custom_method_for_deserialization? &&
|
|
136
|
+
!rule.polymorphic_mapping? &&
|
|
137
|
+
!attr.polymorphic? && !attr.union? &&
|
|
138
|
+
!(t.is_a?(Class) &&
|
|
139
|
+
t.include?(::Lutaml::Model::Serialize))
|
|
140
|
+
return opt_out!(:non_plain_partition) unless plain_partition
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Interpretive hydration materializes every mapped
|
|
144
|
+
# collection, present or not; the fast path mirrors with
|
|
145
|
+
# constructor-time empty arrays.
|
|
146
|
+
unless rule.attribute? || !attr.collection?
|
|
147
|
+
collection_defaults << attr.name.to_sym
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Fragment deferrals lose ancestor namespace context —
|
|
151
|
+
# ns-qualified models keep those rules interpretive.
|
|
152
|
+
fragment_needed = rule.has_custom_method_for_deserialization? ||
|
|
153
|
+
rule.polymorphic_mapping? || attr.polymorphic? || attr.union?
|
|
154
|
+
return opt_out!(:fragment_with_ns) if fragment_needed && model_ns
|
|
155
|
+
|
|
156
|
+
if rule.attribute?
|
|
157
|
+
return opt_out!(:non_scalar) unless scalar_type?(attr, register)
|
|
158
|
+
# Attribute plan rows are local-name keyed; a type-level
|
|
159
|
+
# namespace makes the attribute (URI, local)-identified
|
|
160
|
+
# (lutaml-model#744) — the interpretive matcher owns it
|
|
161
|
+
# until plan rows carry namespace identity.
|
|
162
|
+
return opt_out!(:attr_type_ns) if attr.type_namespace_class(register)
|
|
163
|
+
|
|
164
|
+
attr_rows << [rule, attr]
|
|
165
|
+
plan_attrs << { name: rule.name.to_s }
|
|
166
|
+
elsif rule.content_mapping?
|
|
167
|
+
return opt_out!(:multi_content) if content_rows(rows) >= 1
|
|
168
|
+
|
|
169
|
+
mixed_content = true
|
|
170
|
+
compiled << [rule, attr,
|
|
171
|
+
attr.collection? ? :content : :content_deferred,
|
|
172
|
+
nil, delegate_target]
|
|
173
|
+
rows << { name: "__content__#{compiled.size}", kind: :content }
|
|
174
|
+
elsif rule.raw_mapping? || rule.raw == :element
|
|
175
|
+
compiled << [rule, attr, :raw, nil, delegate_target]
|
|
176
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
177
|
+
elsif rule.has_custom_method_for_deserialization?
|
|
178
|
+
needs_nodes = true
|
|
179
|
+
compiled << [rule, attr, :custom_method, nil, delegate_target]
|
|
180
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
181
|
+
elsif rule.polymorphic_mapping? || attr.polymorphic? || attr.union?
|
|
182
|
+
type = attr.type(register)
|
|
183
|
+
return opt_out!(:non_serializable_poly) unless serializable_type?(type) || attr.union?
|
|
184
|
+
|
|
185
|
+
needs_nodes = true
|
|
186
|
+
compiled << [rule, attr, :polymorphic, nil, delegate_target]
|
|
187
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
188
|
+
else
|
|
189
|
+
type = attr.type(register)
|
|
190
|
+
if serializable_type?(type)
|
|
191
|
+
child = compile(type, register)
|
|
192
|
+
return opt_out!(:child_uncompilable) unless child
|
|
193
|
+
|
|
194
|
+
if child[:ordered]
|
|
195
|
+
# Ordered/mixed children need element_order on their
|
|
196
|
+
# instances; the walk hands back plan values, not
|
|
197
|
+
# source nodes, so the subtree defers interpretively
|
|
198
|
+
# (the fragment parse runs the full machinery,
|
|
199
|
+
# order included).
|
|
200
|
+
return opt_out!(:ordered_child_with_ns) if model_ns
|
|
201
|
+
|
|
202
|
+
needs_nodes = true
|
|
203
|
+
compiled << [rule, attr, :ordered_deferred, nil,
|
|
204
|
+
delegate_target]
|
|
205
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
206
|
+
else
|
|
207
|
+
compiled << [rule, attr, :nested, nil, delegate_target]
|
|
208
|
+
needs_nodes ||= child[:needs_nodes]
|
|
209
|
+
nested_row = { name: rule.name.to_s, kind: :nested,
|
|
210
|
+
plan: child[:tree] }
|
|
211
|
+
nested_row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
212
|
+
rows << nested_row
|
|
213
|
+
end
|
|
214
|
+
elsif rule.multiple_mappings?
|
|
215
|
+
rule.name.each do |spelling|
|
|
216
|
+
compiled << [rule, attr, :spelling, spelling.to_s,
|
|
217
|
+
delegate_target]
|
|
218
|
+
rows << { name: spelling.to_s, kind: :callback,
|
|
219
|
+
type_tag: (tag += 1) }
|
|
220
|
+
end
|
|
221
|
+
else
|
|
222
|
+
return opt_out!(:non_scalar) unless scalar_type?(attr, register)
|
|
223
|
+
|
|
224
|
+
row = { name: rule.name.to_s }
|
|
225
|
+
row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
226
|
+
# lutaml-model#88: same-name rows partitioned by the
|
|
227
|
+
# rule's discriminator ride the engine's exclusive
|
|
228
|
+
# predicate match (leptris 1.9.221+, #1272).
|
|
229
|
+
row[:when] = rule.when_attribute unless rule.when_attribute.empty?
|
|
230
|
+
|
|
231
|
+
if attr.collection?
|
|
232
|
+
compiled << [rule, attr, :collection_native, nil,
|
|
233
|
+
delegate_target]
|
|
234
|
+
rows << row.merge(kind: :collection)
|
|
235
|
+
else
|
|
236
|
+
cdata ||= rule.cdata
|
|
237
|
+
compiled << [rule, attr, :scalar, nil, delegate_target]
|
|
238
|
+
rows << row.merge(kind: :scalar)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
row_tags = partition_row_tags!(compiled, rows, tag)
|
|
245
|
+
|
|
246
|
+
# The plan serializer does not emit namespace declarations —
|
|
247
|
+
# namespaced models serialize through the interpretive
|
|
248
|
+
# writer (lutaml-model#847: standalone to_xml under leptris
|
|
249
|
+
# dropped the element xmlns entirely).
|
|
250
|
+
namespaced = !model_ns.nil? || rows.any? { |r| r[:ns] }
|
|
251
|
+
|
|
252
|
+
flags = []
|
|
253
|
+
flags << :cdata if cdata
|
|
254
|
+
flags << :mixed_content if mixed_content
|
|
255
|
+
flags << :ns_lenient if model_ns
|
|
256
|
+
tree = { name: mapping.root_element.to_s,
|
|
257
|
+
attributes: plan_attrs, children: rows }
|
|
258
|
+
tree[:ns] = model_ns if model_ns
|
|
259
|
+
tree[:flags] = flags unless flags.empty?
|
|
260
|
+
begin
|
|
261
|
+
# Lazy: the Opal boot loads this file, and leptris is a
|
|
262
|
+
# native gem there — the require only belongs on the
|
|
263
|
+
# engines-enabled path.
|
|
264
|
+
require "leptris/xml/descriptor"
|
|
265
|
+
descriptor = ::Leptris::XML::Descriptor.build(**tree)
|
|
266
|
+
rescue StandardError
|
|
267
|
+
return nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
{ descriptor: descriptor, tree: tree, rows: compiled,
|
|
271
|
+
attr_rows: attr_rows, mapping: mapping,
|
|
272
|
+
row_tags: row_tags, namespaced: namespaced,
|
|
273
|
+
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
274
|
+
needs_nodes: needs_nodes,
|
|
275
|
+
collection_defaults: collection_defaults }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def compilable_mapping?(mapping)
|
|
279
|
+
# Mapping's uniform interface: root_mappings defaults to
|
|
280
|
+
# false on the base (KeyValue overrides with its own).
|
|
281
|
+
mapping.root_element && !mapping.root_mappings
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Partition bookkeeping for when_attribute rows (#88, TODO
|
|
285
|
+
# 34 step 2): the engine matches same-name rows exclusively —
|
|
286
|
+
# first matching row wins — so predicate rows must PRECEDE any
|
|
287
|
+
# plain sibling on the same name (a plain-first order
|
|
288
|
+
# double-captures: the plain row takes everything and the
|
|
289
|
+
# predicates still claim their matches). Every row of a
|
|
290
|
+
# partitioned name gets a distinct type_tag; plan values echo
|
|
291
|
+
# it back, giving the hydrator row-exact routing with no
|
|
292
|
+
# per-occurrence re-derivation. Returns {compiled_index =>
|
|
293
|
+
# tag} (nil when the model has no partitions). compiled and
|
|
294
|
+
# rows are parallel and stay in lockstep through the reorder.
|
|
295
|
+
def partition_row_tags!(compiled, rows, tag)
|
|
296
|
+
by_name = rows.each_with_index
|
|
297
|
+
.group_by { |(row, _)| row[:name] }
|
|
298
|
+
.select { |_name, pairs| pairs.any? { |(row, _)| row[:when] } }
|
|
299
|
+
return nil if by_name.empty?
|
|
300
|
+
|
|
301
|
+
permutation = by_name.each_value.flat_map do |pairs|
|
|
302
|
+
pairs.sort_by { |(row, i)| [row[:when] ? 0 : 1, i] }
|
|
303
|
+
.map(&:last)
|
|
304
|
+
end
|
|
305
|
+
remaining = (0...rows.length).to_a - permutation
|
|
306
|
+
permutation.concat(remaining)
|
|
307
|
+
|
|
308
|
+
row_tags = {}
|
|
309
|
+
permutation.each_with_index do |old_index, new_index|
|
|
310
|
+
row = rows[old_index]
|
|
311
|
+
next unless by_name.key?(row[:name])
|
|
312
|
+
|
|
313
|
+
tag += 1
|
|
314
|
+
row[:type_tag] = tag
|
|
315
|
+
row_tags[new_index] = tag
|
|
316
|
+
end
|
|
317
|
+
rows.replace(permutation.map { |i| rows[i] })
|
|
318
|
+
compiled.replace(permutation.map { |i| compiled[i] })
|
|
319
|
+
row_tags
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Attribute for a rule — delegate rules resolve against their
|
|
323
|
+
# target model's attributes.
|
|
324
|
+
def attr_of(model_class, rule, register)
|
|
325
|
+
attr = model_class.attributes(register)[rule.to]
|
|
326
|
+
return attr if attr || !rule.delegate
|
|
327
|
+
|
|
328
|
+
target = model_class.attributes(register)[rule.delegate]
|
|
329
|
+
t = target&.type(register)
|
|
330
|
+
if t.is_a?(Class) && t.include?(::Lutaml::Model::Serialize)
|
|
331
|
+
t.attributes(register)[rule.to]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Rule-level namespace → ChildPlan ns form (leptris 1.9.178):
|
|
336
|
+
# the child binds by local name under the rule's URI with any
|
|
337
|
+
# prefix. Blank-namespace rules (xmlns="") match :none.
|
|
338
|
+
def child_ns(rule, _model_ns)
|
|
339
|
+
uri = rule.namespace
|
|
340
|
+
return { exact: uri.to_s } if uri && !uri.to_s.empty?
|
|
341
|
+
|
|
342
|
+
:none
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def content_rows(rows)
|
|
346
|
+
rows.count { |r| r[:kind] == :content }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Model-level namespace: exact URI match with lenient prefixes
|
|
350
|
+
# — children bind by local name under any prefix the document
|
|
351
|
+
# bound to the URI (#754 adoption semantics on the engine).
|
|
352
|
+
def plan_namespace(_model_class, mapping, _register)
|
|
353
|
+
# Mapping's uniform interface: namespace_class defaults
|
|
354
|
+
# to nil on the base (Xml overrides).
|
|
355
|
+
ns_class = mapping.namespace_class
|
|
356
|
+
ns_class&.uri ? { exact: ns_class.uri.to_s } : nil
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def scalar_type?(attr, register)
|
|
360
|
+
type = attr.type(register)
|
|
361
|
+
type.is_a?(Class) && type < ::Lutaml::Model::Type::Value &&
|
|
362
|
+
!attr.custom_collection?
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def serializable_type?(type)
|
|
366
|
+
type.is_a?(Class) && type.include?(::Lutaml::Model::Serialize)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|