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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70de43228d2b233faab386c9016139160f2b75e9f76c27386de22104df2820f5
4
- data.tar.gz: 78b4591ba8eec19bbc7fde9a1fd2d298c58094b1198352f2323067406d39b5aa
3
+ metadata.gz: 02366b5cf16f9598e6eae1c391d7b21002fe9744cc4d617bf4bb9ffc785141a8
4
+ data.tar.gz: b3e2928f228d2aa613e506c6b07ce70a765bb33d1574e61e871fd7f6d226ee43
5
5
  SHA512:
6
- metadata.gz: f1d54790ba8f6d44834134ef283b3d146002e3ca4f2e25f0fb99e84bd442878d6470d50f6da81f36a8e4165e93c07e2e3a6de2cffeb981f45b40d849a6be1037
7
- data.tar.gz: 4d358cd375296cdb737fa12300076a4154f38aa22ce940cbcb7483453691152d4cbfd25fef88131fe1150105868eb9a1d56fa193028f3bd51eff7eb853d43c09
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 hydrator's one-crossing snapshot
48
- # (leptris_plan_value_children_snapshot) landed in 1.9.273.0; older
49
- # gems fall back to accessor enumeration at runtime, but CI and dev
50
- # resolve the snapshot-capable line.
51
- gem "leptris", ">= 1.9.273.0"
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
@@ -455,6 +455,7 @@ module Lutaml
455
455
  unknown = attrs.keys.find { |k| !known.include?(k.to_sym) }
456
456
  raise Error, "unknown attribute '#{unknown}' for #{self}"
457
457
  end
458
+ instance.clear_mutated_after_parse!
458
459
  instance
459
460
  end
460
461
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.101"
5
+ VERSION = "0.8.103"
6
6
  end
7
7
  end
@@ -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