lutaml-model 0.8.101 → 0.8.103
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/Gemfile +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/model/configuration.rb +7 -0
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +5 -2
- data/lib/lutaml/model/serialize/initialization.rb +1 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +185 -23
- data/lib/lutaml/xml/leptris/plan_serializer.rb +291 -61
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
- data/lib/lutaml/xml/leptris.rb +11 -94
- data/spec/lutaml/xml/plan_fast_path_spec.rb +9 -4
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02366b5cf16f9598e6eae1c391d7b21002fe9744cc4d617bf4bb9ffc785141a8
|
|
4
|
+
data.tar.gz: b3e2928f228d2aa613e506c6b07ce70a765bb33d1574e61e871fd7f6d226ee43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '096e5a0302f14999f0f0c33c5c08455cdea460bee145d0a2cbb222d5bf19157f931a8aecd9e99a2232e176db0a7c546b62908a91f851c083aedf75a4cbfb5993'
|
|
7
|
+
data.tar.gz: 79a62c6ce918c5ea416eec0a9343a9b52b65f5f9d7e234bcc94beaba817e2658d25ba680215ffaee282a8bfc4ba3f4d4fd2900ecfe5907c36f0ff317ec293028
|
data/Gemfile
CHANGED
|
@@ -44,11 +44,12 @@ gem "rexml"
|
|
|
44
44
|
# Native engines stay development-only: every adapter detects and
|
|
45
45
|
# degrades gracefully at runtime (safe requires + prefer-if-available).
|
|
46
46
|
# yeptris ships no Windows prebuilt platform gem.
|
|
47
|
-
# leptris floor: the plan
|
|
48
|
-
# (
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
47
|
+
# leptris floor: the single capability line for the plan fast path
|
|
48
|
+
# (see Lutaml::Xml::Leptris::MIN_LEPTRIS_VERSION) — child snapshots,
|
|
49
|
+
# exact-URI attribute rows, nested-attribute capture, wildcard rows,
|
|
50
|
+
# walk-side ns retention and leniency, :unqualified rows, and the
|
|
51
|
+
# address-based text attach all ride it.
|
|
52
|
+
gem "leptris", ">= 1.9.336.0"
|
|
52
53
|
gem "teptris"
|
|
53
54
|
gem "yeptris" unless Gem.win_platform?
|
|
54
55
|
# TODO: revert rng branch to main when lutaml/rng#32 is merged
|
|
@@ -382,6 +382,7 @@ require "lutaml/xml/error/namespace_mismatch_error"
|
|
|
382
382
|
require "lutaml/xml/error/schema_validation_error"
|
|
383
383
|
require "lutaml/xml/error/xml_configuration_error"
|
|
384
384
|
require "lutaml/xml/error/xml_error"
|
|
385
|
+
require "lutaml/xml/leptris/native_emission"
|
|
385
386
|
require "lutaml/xml/leptris/plan_compiler"
|
|
386
387
|
require "lutaml/xml/leptris/plan_hydrator"
|
|
387
388
|
require "lutaml/xml/leptris/plan_order"
|
|
@@ -19,6 +19,12 @@ module Lutaml
|
|
|
19
19
|
# compilable XML models (Leptris::XML::Descriptor walk). Opt-in
|
|
20
20
|
# while the full semantics audit completes.
|
|
21
21
|
attr_accessor :xml_plan_fast_path
|
|
22
|
+
# Ordered/mixed-content models on the plan path: opt-in. The
|
|
23
|
+
# per-node ordered hydration is FASTER than the interpretive
|
|
24
|
+
# lane on time but allocates above the downstream budget gates
|
|
25
|
+
# (niso pnas ratio 1.25); it ships dark until the allocation
|
|
26
|
+
# diet lands.
|
|
27
|
+
attr_accessor :xml_plan_ordered_path
|
|
22
28
|
# #909: when false, XML deserialization skips the per-instance
|
|
23
29
|
# element_order/attribute_order summaries — read-only consumers of
|
|
24
30
|
# large documents never serialize back and should not pay for
|
|
@@ -31,6 +37,7 @@ module Lutaml
|
|
|
31
37
|
@conversion_cache = nil
|
|
32
38
|
@configured = false
|
|
33
39
|
@xml_plan_fast_path = true
|
|
40
|
+
@xml_plan_ordered_path = false
|
|
34
41
|
@element_order_tracking = true
|
|
35
42
|
end
|
|
36
43
|
|
|
@@ -84,6 +84,26 @@ module Lutaml
|
|
|
84
84
|
@__order_tracking__ ? true : false
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Whether any writer setter ran on this instance. Parsed
|
|
88
|
+
# instances start pristine — every set element value is covered
|
|
89
|
+
# by the parse-built element_order, so ordered serialization
|
|
90
|
+
# can skip order reconciliation; the first writer-set marks the
|
|
91
|
+
# instance and reconciliation engages (writer-added values sit
|
|
92
|
+
# outside the parse order).
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def mutated_after_parse?
|
|
95
|
+
@__mutated_after_parse__ ? true : false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Construction-time reset: hydration routes every attribute
|
|
99
|
+
# through the generated writers (which mark mutations); a
|
|
100
|
+
# freshly built instance is definitionally not
|
|
101
|
+
# mutated-after-parse.
|
|
102
|
+
# @return [void]
|
|
103
|
+
def clear_mutated_after_parse!
|
|
104
|
+
@__mutated_after_parse__ = false
|
|
105
|
+
end
|
|
106
|
+
|
|
87
107
|
# Record a singular attribute mutation in element_order.
|
|
88
108
|
#
|
|
89
109
|
# No-op unless order tracking is enabled (i.e. the instance was
|
|
@@ -102,6 +122,7 @@ module Lutaml
|
|
|
102
122
|
# content for content-mapped attributes, ignored otherwise
|
|
103
123
|
# @return [Object] the passed value
|
|
104
124
|
def record_mutation(attribute_name, value = nil)
|
|
125
|
+
@__mutated_after_parse__ = true
|
|
105
126
|
return value unless @__order_tracking__
|
|
106
127
|
|
|
107
128
|
track_order(attribute_name, value, nil)
|
|
@@ -122,6 +143,7 @@ module Lutaml
|
|
|
122
143
|
# @param value [Object, nil] The value assigned to the collection
|
|
123
144
|
# @return [Object] the passed value
|
|
124
145
|
def record_mutation_collection(attribute_name, value)
|
|
146
|
+
@__mutated_after_parse__ = true
|
|
125
147
|
return value unless @__order_tracking__
|
|
126
148
|
return value if value.nil? || Lutaml::Model::Utils.uninitialized?(value)
|
|
127
149
|
|
|
@@ -138,7 +138,6 @@ module Lutaml
|
|
|
138
138
|
register = Lutaml::Model::Config.default_register
|
|
139
139
|
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
140
140
|
return nil unless plan
|
|
141
|
-
return nil unless Lutaml::Xml::Leptris::PlanCompiler.engine_walk_safe?(plan)
|
|
142
141
|
|
|
143
142
|
root = ::Leptris::XML.parse(data.to_s).root
|
|
144
143
|
return nil if root.nil?
|
|
@@ -146,7 +145,11 @@ module Lutaml
|
|
|
146
145
|
|
|
147
146
|
Lutaml::Xml::Leptris::PlanHydrator.call(self, plan,
|
|
148
147
|
plan[:descriptor].walk(root),
|
|
149
|
-
node: root
|
|
148
|
+
node: root,
|
|
149
|
+
declaration_plan_mode:
|
|
150
|
+
options.fetch(
|
|
151
|
+
:import_declaration_plan, :eager
|
|
152
|
+
))
|
|
150
153
|
rescue ::Leptris::XML::ParseError => e
|
|
151
154
|
raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
|
|
152
155
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# The native emission lane of the plan fast path: instead of
|
|
7
|
+
# minting the output DOM node-by-node through the Ruby
|
|
8
|
+
# create_child chain, build a plan RESULT from ops
|
|
9
|
+
# (leptris_plan_result_build), render it through the engine's
|
|
10
|
+
# plan serializer, reparse the compact output, and write it with
|
|
11
|
+
# the engine's indented document writer. Both renderings are
|
|
12
|
+
# pure C; measured 2.4x faster than the DOM-mint loop on the
|
|
13
|
+
# 500-row audit at a modest allocation increase.
|
|
14
|
+
#
|
|
15
|
+
# Byte-parity with the DOM lane holds because both lanes end at
|
|
16
|
+
# the same document writer over structurally identical
|
|
17
|
+
# documents: the compact emission escapes exactly as the DOM
|
|
18
|
+
# text nodes do, and reparsing yields one text node per leaf,
|
|
19
|
+
# matching the minted tree. The reparse is an engine-internal
|
|
20
|
+
# cost, not a Ruby one.
|
|
21
|
+
#
|
|
22
|
+
# Emission is enabled for the provably static subset: trees with
|
|
23
|
+
# no namespace form anywhere (every spelling is the unwritten
|
|
24
|
+
# local name, nothing to declare, no instance-dependent
|
|
25
|
+
# prefix/default flips) and the non-ordered build() shapes
|
|
26
|
+
# (attributes, scalars, collections, nested models). Wildcard,
|
|
27
|
+
# raw, callback, and content rows and ordered models keep the
|
|
28
|
+
# DOM lane — their emission shapes are not expressible as ops
|
|
29
|
+
# (or not verified to be) and the DOM lane is byte-exact.
|
|
30
|
+
module NativeEmission
|
|
31
|
+
ROOT_ROW = 0xFFFF_FFFF
|
|
32
|
+
EMITTABLE_KINDS = %i[scalar collection nested].freeze
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def compatible?(plan)
|
|
36
|
+
!plan[:ordered] && tree_emittable?(plan[:tree])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Pre-order depth-first, children in row order — the same
|
|
40
|
+
# walk Descriptor.flatten_plan performs, so the plan_index
|
|
41
|
+
# counters here match the engine's flattened plans array.
|
|
42
|
+
def tree_emittable?(tree)
|
|
43
|
+
return false if tree[:ns]
|
|
44
|
+
return false if (tree[:attributes] || []).any? { |a| a[:ns].is_a?(Hash) }
|
|
45
|
+
|
|
46
|
+
(tree[:children] || []).all? do |row|
|
|
47
|
+
EMITTABLE_KINDS.include?(row[:kind]) &&
|
|
48
|
+
(row[:kind] != :nested || !row[:plan] || tree_emittable?(row[:plan]))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def emit(instance, plan, register)
|
|
53
|
+
ops = [{ kind: :element, row_index: ROOT_ROW }]
|
|
54
|
+
emit_element(ops, instance, plan, register, 0)
|
|
55
|
+
ops << { kind: :end }
|
|
56
|
+
compact = plan[:descriptor].serialize(
|
|
57
|
+
plan[:descriptor].build_result(ops),
|
|
58
|
+
)
|
|
59
|
+
::Leptris::XML.parse(compact).to_xml(indent: 2, no_decl: true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# plan_index: this plan's flattened index. The engine's
|
|
65
|
+
# flatten_plan pushes each nested row's child plan exactly
|
|
66
|
+
# once — collection members share that slot — so child flat
|
|
67
|
+
# indices come from a static per-plan layout (relative to
|
|
68
|
+
# this plan's index), never from a runtime counter.
|
|
69
|
+
def emit_element(ops, instance, plan, register, plan_index)
|
|
70
|
+
my_index = plan_index
|
|
71
|
+
layout = flat_layout_for(plan)
|
|
72
|
+
|
|
73
|
+
emit_attributes(ops, instance, plan, my_index, register)
|
|
74
|
+
|
|
75
|
+
plan[:rows].each_with_index do |(rule, attr, kind, _spelling, delegate), row_index|
|
|
76
|
+
value = value_of(instance, attr, delegate)
|
|
77
|
+
next unless rule.render?(value, instance)
|
|
78
|
+
|
|
79
|
+
case kind
|
|
80
|
+
when :scalar
|
|
81
|
+
# Multi-capture parity: an attribute that collected
|
|
82
|
+
# several occurrences serializes one element per item,
|
|
83
|
+
# as the DOM lane does.
|
|
84
|
+
if value.is_a?(::Array)
|
|
85
|
+
value.each do |item|
|
|
86
|
+
ops << scalar_op(my_index, row_index, attr, item, register)
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
ops << scalar_op(my_index, row_index, attr, value, register)
|
|
90
|
+
end
|
|
91
|
+
when :collection_native, :collection_cb
|
|
92
|
+
Array(value).each do |item|
|
|
93
|
+
ops << scalar_op(my_index, row_index, attr, item, register)
|
|
94
|
+
end
|
|
95
|
+
when :nested
|
|
96
|
+
_tree, child_rel = layout[row_index]
|
|
97
|
+
next unless child_rel
|
|
98
|
+
|
|
99
|
+
Array(value).each do |item|
|
|
100
|
+
child_plan = nested_plans_for(plan)[[item.class, rule.name.to_s]] ||
|
|
101
|
+
(nested_plans_for(plan)[[item.class, rule.name.to_s]] =
|
|
102
|
+
PlanCompiler.compile(item.class, register))
|
|
103
|
+
ops << { kind: :element, plan_index: my_index,
|
|
104
|
+
row_index: row_index }
|
|
105
|
+
emit_element(ops, item, child_plan,
|
|
106
|
+
register, my_index + child_rel)
|
|
107
|
+
ops << { kind: :end }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# {nested row_index => [child tree, relative flat index]} —
|
|
114
|
+
# children of this plan occupy slots my_index+1 .. in row
|
|
115
|
+
# order, each nested row's subtree sized once.
|
|
116
|
+
def flat_layout_for(plan)
|
|
117
|
+
plan[:flat_layout] ||= begin
|
|
118
|
+
layout = {}
|
|
119
|
+
rel = 1
|
|
120
|
+
(plan[:tree][:children] || []).each_with_index do |row, i|
|
|
121
|
+
next unless row[:kind] == :nested && row[:plan]
|
|
122
|
+
|
|
123
|
+
layout[i] = [row[:plan], rel]
|
|
124
|
+
rel += subtree_size(row[:plan])
|
|
125
|
+
end
|
|
126
|
+
layout
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def subtree_size(tree)
|
|
131
|
+
1 + (tree[:children] || []).sum do |row|
|
|
132
|
+
row[:kind] == :nested && row[:plan] ? subtree_size(row[:plan]) : 0
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def emit_attributes(ops, instance, plan, plan_index, register)
|
|
137
|
+
recorded = instance.attribute_order
|
|
138
|
+
attr_rows = plan[:attr_rows]
|
|
139
|
+
if recorded && !recorded.empty?
|
|
140
|
+
by_name = {}
|
|
141
|
+
attr_rows.each { |rule, attr, _sp, delegate| by_name[rule.name.to_s] = [rule, attr, delegate] }
|
|
142
|
+
names = recorded | attr_rows.map { |rule, _a, _s, _d| rule.name.to_s }
|
|
143
|
+
else
|
|
144
|
+
names = attr_rows.map { |rule, _a, _s, _d| rule.name.to_s }
|
|
145
|
+
by_name = {}
|
|
146
|
+
attr_rows.each { |rule, attr, _sp, delegate| by_name[rule.name.to_s] = [rule, attr, delegate] }
|
|
147
|
+
end
|
|
148
|
+
tree_attrs = plan[:tree][:attributes] || []
|
|
149
|
+
names.each do |name|
|
|
150
|
+
entry = by_name[name]
|
|
151
|
+
next unless entry
|
|
152
|
+
|
|
153
|
+
_rule, attr, delegate = entry
|
|
154
|
+
value = value_of(instance, attr, delegate)
|
|
155
|
+
next if value.nil?
|
|
156
|
+
|
|
157
|
+
row_index = tree_attrs.index { |a| a[:name] == name }
|
|
158
|
+
next unless row_index
|
|
159
|
+
|
|
160
|
+
ops << { kind: :attr, plan_index: plan_index,
|
|
161
|
+
row_index: row_index, name: name,
|
|
162
|
+
value: attr.serialize(value, :xml, register).to_s }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def scalar_op(plan_index, row_index, attr, value, register)
|
|
167
|
+
{ kind: :scalar, plan_index: plan_index, row_index: row_index,
|
|
168
|
+
value: attr.serialize(value, :xml, register).to_s }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def value_of(instance, attr, delegate)
|
|
172
|
+
if delegate
|
|
173
|
+
target = instance.public_send(delegate)
|
|
174
|
+
target&.public_send(attr.name)
|
|
175
|
+
else
|
|
176
|
+
instance.public_send(attr.name)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def nested_plans_for(plan)
|
|
181
|
+
plan[:nested_plans] ||= {}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|