lutaml-model 0.8.105 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9d133a4a89e51d9a1ed811bd992faeac4f0a8513e283b46f079707bcc083f7e
4
- data.tar.gz: e4df7bb98a1f7518bdd9f2194934e91feea5367bbf536f75aa4353a067b65049
3
+ metadata.gz: cc1bb0f95876011fac44764d7ea55c334d24ea2a20911e46f4200448cf50cc1e
4
+ data.tar.gz: 23f02dbcf3dac25a19a50f4a8bf75b81d642b64514c0aeb9a820d727be63406f
5
5
  SHA512:
6
- metadata.gz: 7040ad58b0a31222207e14245293f8f20eb674180dcd55936de938151f16fc5260615f5ec3244769e4644af0bd6f02c9262e87717cbdff9d578a6b1365bbbb3a
7
- data.tar.gz: f5d38407f6e5ad1b87bad426f68398a6e1249b57c928aa1a94f0585b144517542f0565c38ad2f53b31bc2c80bdaf63a5425968ba883c9d222508940f9b8b1c17
6
+ metadata.gz: 8d649b8eef84ef0cfa60ec5fec9e70b0fa45cdc44508325f12c7a17d66e3ca437535c3bf56bc725217f73d305046c9102d7dba067148f4efdec8996d2f2f8ec2
7
+ data.tar.gz: d81707bac3fb9bf9ebc3091c421253e3b9b8abd085891e03debcd19674af75ae5e6dc5f1b724fbb05e160847694dcc8752e9eb1fb17425a394c1babc67777273
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.105"
5
+ VERSION = "0.8.106"
6
6
  end
7
7
  end
@@ -15,6 +15,10 @@ module Lutaml
15
15
  # (their values echo name and type_tag; native collection values
16
16
  # echo neither — leptris-ruby#220).
17
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
+
18
22
  class << self
19
23
  # plan: the compiled plan entry for model_class
20
24
  # value: the walk root PlanValue (element)
@@ -51,8 +55,8 @@ module Lutaml
51
55
  # TODO.max-perf/33: the bulk constructor — allocate +
52
56
  # compiled writers, from_hash semantics — measured ~30x on
53
57
  # hydration-heavy corpora versus the generic constructor.
54
- instance = model_class.instantiate(attr_kwargs.merge(child_kwargs),
55
- register)
58
+ attr_kwargs.update(child_kwargs)
59
+ instance = model_class.instantiate(attr_kwargs, register)
56
60
  instance.lutaml_parent = parent if parent
57
61
  instance.lutaml_root ||= parent&.lutaml_root || parent
58
62
  # Root prefix fidelity (the interpretive transform records
@@ -244,10 +248,13 @@ module Lutaml
244
248
  # the block-backed Hash is a fresh object per hydrated row.
245
249
  spellings = nil
246
250
  row_tags = plan[:row_tags]
251
+ wire_names = row_wire_names(plan)
252
+ row_memo = child_row_plans(plan, register)
247
253
  # rubocop:disable Metrics/BlockLength -- one row-kind
248
254
  # dispatch table; splitting it would scatter the contract
249
- plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate, row_type), idx|
255
+ plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate, _row_type), idx|
250
256
  tag = row_tags&.[](idx)
257
+ name = wire_names[idx]
251
258
  case kind
252
259
  when :scalar
253
260
  # Raw passthrough: the model constructor is the single
@@ -265,7 +272,7 @@ module Lutaml
265
272
  first = if tag
266
273
  tagged[tag]&.first
267
274
  else
268
- grouped.dig(rule.name.to_s, 0)
275
+ grouped.dig(name, 0)
269
276
  end
270
277
  v = first&.string_value
271
278
  v = mapped_empty_value(rule) if v.nil? && first
@@ -293,15 +300,13 @@ module Lutaml
293
300
  # walk against its own node + element_order from the
294
301
  # node's children. Without one (direct PlanHydrator
295
302
  # use), the subtree fragment-parses interpretively.
296
- if buckets
297
- child_type = row_type || attr.type(register)
298
- child_plan = PlanCompiler.compile(child_type, register)
299
- selected, = child_bucket(buckets, rule, child_plan,
300
- contested_name?(plan, rule))
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)
301
306
  items = (selected || []).map do |n|
302
307
  call(child_type, child_plan,
303
308
  child_plan[:descriptor].walk(n), node: n,
304
- register: register)
309
+ register: child_register)
305
310
  end
306
311
  unless items.empty?
307
312
  children.concat(items)
@@ -314,10 +319,10 @@ module Lutaml
314
319
  # source node casts through the attribute — the full
315
320
  # interpretive machinery on the bridged island,
316
321
  # identical semantics to the interpretive path.
317
- contested_uri = if plan[:contested_names]&.include?(rule.name.to_s)
322
+ contested_uri = if plan[:contested_names]&.include?(name)
318
323
  child_type_uri(attr, register)
319
324
  end
320
- elements = deferred_elements(value, rule, buckets,
325
+ elements = deferred_elements(value, name, buckets,
321
326
  contested_uri)
322
327
  next if elements.empty?
323
328
 
@@ -330,7 +335,7 @@ module Lutaml
330
335
  attr.collection? ? results : results.first)
331
336
  end
332
337
  when :collection_cb
333
- values = grouped[rule.name.to_s].to_a.map do |item|
338
+ values = grouped[name].to_a.map do |item|
334
339
  item&.string_value
335
340
  end
336
341
  values = mapped_capture_values(values, rule)
@@ -344,7 +349,7 @@ module Lutaml
344
349
  values = if tag
345
350
  tagged[tag].to_a.map(&:string_value)
346
351
  else
347
- native_collection(value, rule.name.to_s)
352
+ native_collection(value, name)
348
353
  end
349
354
  values = mapped_capture_values(values, rule) unless values.nil?
350
355
  unless values.nil?
@@ -354,28 +359,26 @@ module Lutaml
354
359
  spellings ||= Hash.new { |h, k| h[k] = [] }
355
360
  spellings[[rule, attr]] << grouped[spelling.to_s].to_a
356
361
  when :nested
357
- child_type = row_type || attr.type(register)
358
- child_plan = PlanCompiler.compile(child_type, register)
359
- selected, = child_bucket(buckets, rule, child_plan,
360
- contested_name?(plan, rule))
361
- if selected && plan[:contested_names]&.include?(rule.name.to_s)
362
+ child_type, child_plan, child_register, contested = row_memo[idx]
363
+ selected = child_selection(buckets, name, child_plan, contested)
364
+ if selected && contested
362
365
  # Contested name: the walk's grouped values cross-echo
363
366
  # every same-name element into every row — dispatch
364
367
  # per source node instead (the walk is row-scoped).
365
368
  items = selected.map do |n|
366
369
  call(child_type, child_plan,
367
370
  child_plan[:descriptor].walk(n), node: n,
368
- register: register)
371
+ register: child_register)
369
372
  end
370
373
  else
371
- values = grouped.fetch(rule.name.to_s, [])
374
+ values = grouped.fetch(name, [])
372
375
  cursor = if child_plan[:needs_nodes] && buckets
373
376
  selected
374
377
  end
375
378
  items = values.each_with_index
376
379
  .map do |v, i|
377
380
  call(child_type, child_plan, v,
378
- node: cursor && cursor[i], register: register)
381
+ node: cursor && cursor[i], register: child_register)
379
382
  end
380
383
  end
381
384
  next if items.empty?
@@ -411,7 +414,11 @@ module Lutaml
411
414
  # hydration). Ordered children only land here without a node.
412
415
  def interpret_deferred(model_class, plan, value, instance, node = nil,
413
416
  buckets = nil)
414
- plan[:rows].each do |rule, attr, kind, _spelling, _delegate|
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]
415
422
  case kind
416
423
  when :content_deferred
417
424
  runs = content_runs(value)
@@ -437,13 +444,13 @@ module Lutaml
437
444
  # concatenated).
438
445
  instance.public_send(:"#{attr.name}=", runs.join)
439
446
  when :raw
440
- raws = raw_strings(value, rule)
447
+ raws = raw_strings(value, wire_name)
441
448
  next if raws.empty?
442
449
 
443
450
  instance.public_send(:"#{attr.name}=",
444
451
  attr.collection? ? raws : raws.first)
445
452
  when :custom_method
446
- elements = deferred_elements(value, rule, buckets)
453
+ elements = deferred_elements(value, wire_name, buckets)
447
454
  next if elements.empty?
448
455
 
449
456
  args = attr.collection? ? elements : elements.first
@@ -453,7 +460,7 @@ module Lutaml
453
460
  when :ordered_deferred
454
461
  next if node # hydrated natively in children_kwargs
455
462
 
456
- results = deferred_elements(value, rule, buckets).map do |element|
463
+ results = deferred_elements(value, wire_name, buckets).map do |element|
457
464
  attr.cast(element, :xml, register,
458
465
  lutaml_parent: instance,
459
466
  lutaml_root: instance.lutaml_root || instance)
@@ -496,7 +503,7 @@ module Lutaml
496
503
  attr.collection? ? results : results.first,
497
504
  )
498
505
  when :polymorphic
499
- results = deferred_elements(value, rule, buckets).map do |element|
506
+ results = deferred_elements(value, wire_name, buckets).map do |element|
500
507
  attr.cast(element, :xml, register,
501
508
  polymorphic: rule.polymorphic,
502
509
  lutaml_parent: instance,
@@ -515,16 +522,16 @@ module Lutaml
515
522
  # construction only, no parse), falling back to a fragment
516
523
  # parse of the captured subtree when no node is available.
517
524
  # Both lists are document-ordered for the row's name.
518
- def deferred_elements(value, rule, buckets, contested_uri = nil)
519
- return raw_strings(value, rule).map { |raw| fragment_element(raw) } unless buckets
525
+ def deferred_elements(value, name, buckets, contested_uri = nil)
526
+ return raw_strings(value, name).map { |raw| fragment_element(raw) } unless buckets
520
527
 
521
- nodes = buckets[rule.name.to_s]
528
+ nodes = buckets[name]
522
529
  return [] unless nodes
523
530
 
524
531
  if contested_uri
525
532
  # Contested name: bind only the (URI, local) matches —
526
533
  # the type's own mapping namespace is the row identity.
527
- nodes = buckets[[contested_uri, rule.name.to_s]] || []
534
+ nodes = buckets[[contested_uri, name]] || []
528
535
  end
529
536
  nodes.map { |n| bridge_element(n) }
530
537
  end
@@ -603,11 +610,11 @@ module Lutaml
603
610
  nil
604
611
  end
605
612
 
606
- def raw_strings(value, rule)
613
+ def raw_strings(value, name)
607
614
  out = []
608
615
  value.count.times do |idx|
609
616
  child = value.at(idx)
610
- if child.name == rule.name.to_s && child.kind == :raw
617
+ if child.name == name && child.kind == :raw
611
618
  out << child.string_value
612
619
  end
613
620
  end
@@ -693,27 +700,46 @@ module Lutaml
693
700
  buckets
694
701
  end
695
702
 
696
- # (URI, local) child identity — the interpretive matcher's
697
- # typed-element dispatch: same-name rows in different
698
- # namespaces bind by the child type's exact URI (w14:docId vs
699
- # w15:docId under one model). Only CONTESTED names dispatch
700
- # exactly — a uniquely-named row binds the whole name bucket
701
- # leniently (any qualification), and the uri-keyed view is
702
- # built only for plans that carry contested names. Returns
703
- # [selected, all] where all is the name bucket (aligned with
704
- # the walk's grouped values for position filtering).
705
- def child_bucket(buckets, rule, child_plan, contested)
706
- name = rule.name.to_s
707
- all = buckets ? buckets[name] : nil
708
- return [all, all] unless buckets && all && contested
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
709
711
 
710
712
  tree_ns = child_plan[:tree][:ns]
711
713
  exact_uri = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
712
- if exact_uri
713
- qualified = buckets[[exact_uri, name]]
714
- return [qualified || [], all]
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
715
742
  end
716
- [all, all]
717
743
  end
718
744
  end
719
745
  end
@@ -19,11 +19,20 @@ module Lutaml
19
19
  # answers nil — the opt-out protocol.
20
20
  MIN_LEPTRIS_VERSION = "1.9.336.0"
21
21
 
22
+ # Memoized: the compiler consults this on every cache hit (per
23
+ # child row per node), and the comparison minted a Gem::Version
24
+ # per call — visible in the niso ordered-lane allocation profile.
25
+ # The loaded gem cannot change mid-process.
22
26
  def self.plan_path_compatible?
23
- return false unless defined?(Gem)
27
+ return @plan_path_compatible if defined?(@plan_path_compatible)
24
28
 
25
- spec = Gem.loaded_specs["leptris"]
26
- spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
29
+ @plan_path_compatible =
30
+ if defined?(Gem)
31
+ spec = Gem.loaded_specs["leptris"]
32
+ spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
33
+ else
34
+ false
35
+ end
27
36
  end
28
37
 
29
38
  autoload :NativeEmission, "lutaml/xml/leptris/native_emission"
@@ -10,15 +10,24 @@ require "lutaml/xml/adapter/nokogiri_adapter"
10
10
  # must surface in the raised message — otherwise the error names the
11
11
  # input's emptiness while the input is neither empty nor the problem
12
12
  # (lutaml-model#956).
13
+ #
14
+ # moxml#369 changed the recoverable-malformation contract: the leptris
15
+ # adapter's non-strict parse now RECOVERS (the engine's recover mode,
16
+ # leptris#1660) and returns the best-effort tree instead of failing —
17
+ # a mismatched closer yields a document, not a raise. Only inputs with
18
+ # nothing to recover into (whitespace-only) still reach the guard,
19
+ # carrying the recovery diagnostic.
13
20
  RSpec.describe "empty-document errors surface engine parse diagnostics" do
14
21
  describe Lutaml::Xml::Adapter::LeptrisAdapter do
15
- it "appends the engine diagnostic when the parse failed" do
16
- expect do
17
- described_class.parse("<root><a>x</b></root>")
18
- end.to raise_error(Lutaml::Model::InvalidFormatError) do |error|
19
- expect(error.message).to include("the XML engine reported:")
20
- expect(error.message).to include("XML parse error at byte 14")
22
+ it "returns a recovered tree for recoverable malformations (moxml#369)" do
23
+ version = Gem.loaded_specs.fetch("moxml").version
24
+ recovered = version >= Gem::Version.new("0.5.131")
25
+ unless recovered
26
+ skip "moxml predates the recover-mode wiring (0.5.131)"
21
27
  end
28
+
29
+ doc = described_class.parse("<root><a>x</b></root>")
30
+ expect(doc.root.name).to eq("root")
22
31
  end
23
32
 
24
33
  it "reports the engine diagnostic for whitespace-only input" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.105
4
+ version: 0.8.106
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.