lutaml-model 0.8.106 → 0.8.108

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: cc1bb0f95876011fac44764d7ea55c334d24ea2a20911e46f4200448cf50cc1e
4
- data.tar.gz: 23f02dbcf3dac25a19a50f4a8bf75b81d642b64514c0aeb9a820d727be63406f
3
+ metadata.gz: 50f81556cebfd601f3ff6ede463ed9f20d31a0e2c12e8c1362c655b5dd9408e8
4
+ data.tar.gz: c9ec847109db4b87bbb4e4e46fde663eb83d7b9eae54d43f55e1c04024345013
5
5
  SHA512:
6
- metadata.gz: 8d649b8eef84ef0cfa60ec5fec9e70b0fa45cdc44508325f12c7a17d66e3ca437535c3bf56bc725217f73d305046c9102d7dba067148f4efdec8996d2f2f8ec2
7
- data.tar.gz: d81707bac3fb9bf9ebc3091c421253e3b9b8abd085891e03debcd19674af75ae5e6dc5f1b724fbb05e160847694dcc8752e9eb1fb17425a394c1babc67777273
6
+ metadata.gz: cbce6698e13f0d52da843a92c4bae8ca69db7f11cdbd654f1dd924cd171f31ad227da0a610557a17de11856de2030460d5b497a248c3b37b7de76ca286398520
7
+ data.tar.gz: d4113a27b55316ad49f5a9263ac3b1d15616c3c7c65408faa1b0a4741ed1966fcd173a8519d85b19dec66a406ce168caf6439703b5e687318ae74dc586ee586d
data/Gemfile CHANGED
@@ -49,7 +49,7 @@ gem "rexml"
49
49
  # exact-URI attribute rows, nested-attribute capture, wildcard rows,
50
50
  # walk-side ns retention and leniency, :unqualified rows, and the
51
51
  # address-based text attach all ride it.
52
- gem "leptris", ">= 1.9.336.0"
52
+ gem "leptris", ">= 1.9.351.1"
53
53
  gem "teptris"
54
54
  gem "yeptris" unless Gem.win_platform?
55
55
  # TODO: revert rng branch to main when lutaml/rng#32 is merged
@@ -26,6 +26,17 @@ module Lutaml
26
26
  end
27
27
  private_class_method :require_engine
28
28
 
29
+ # True where the yeptris engine loads (libyeptris present for
30
+ # this platform). Idempotent; a missing or broken install
31
+ # reports false instead of raising, so optional-engine callers
32
+ # can gate on it.
33
+ def self.available?
34
+ require_engine
35
+ true
36
+ rescue LoadError
37
+ false
38
+ end
39
+
29
40
  # The parse-error contract is the standard adapter's: invalid
30
41
  # input raises JSON::ParserError regardless of the engine, so
31
42
  # consumers' rescue ladders (and this gem's own
@@ -389,12 +389,18 @@ module Lutaml
389
389
 
390
390
  def cast_value(value, register)
391
391
  if list_like?(value)
392
- build_collection(cast_items(value, register), register: register)
392
+ items = cast_items(value, register)
393
393
  elsif coerce_to_collection?(value)
394
- build_collection(cast_items([value], register), register: register)
394
+ items = cast_items([value], register)
395
395
  else
396
- cast_element(value, register)
396
+ return cast_element(value, register)
397
397
  end
398
+ # cast_items already returns a fresh array, so the plain-Array
399
+ # collection needs no re-wrap — build_collection's
400
+ # collection_class.new would copy it again.
401
+ return items if !custom_collection? && collection_class == ::Array
402
+
403
+ build_collection(items, register: register)
398
404
  end
399
405
 
400
406
  # Plain Arrays count as list input. On singular attributes
@@ -37,7 +37,11 @@ module Lutaml
37
37
  @conversion_cache = nil
38
38
  @configured = false
39
39
  @xml_plan_fast_path = true
40
- @xml_plan_ordered_path = false
40
+ # Opt-in until leptris#1743 lands: the native ordered lane
41
+ # runs away (28+ CPU-min / 14GB RSS on sts-ruby's
42
+ # ISO_13849-1 document) — flipping this on by default regressed
43
+ # the sts bench cell from 4m41s to a 10-minute kill.
44
+ @xml_plan_ordered_path = false
41
45
  @element_order_tracking = true
42
46
  end
43
47
 
@@ -223,6 +223,7 @@ module Lutaml
223
223
  # Clear memoized attribute merge (see .attributes)
224
224
  @merged_attributes_cache = nil
225
225
  @instantiate_writers = nil
226
+ @instantiate_attr_pairs = nil
226
227
 
227
228
  # Clear per-Attribute type caches (stale entries from GC'd TypeContext objects)
228
229
  class_attributes.each_value(&:clear_type_cache)
@@ -402,7 +403,10 @@ module Lutaml
402
403
  attrs_by = attributes(register_id)
403
404
  writers = instantiate_writers(register_id)
404
405
  given = 0
405
- attrs_by.each do |name, attr|
406
+ # Memoized pair array: Hash#each mints a [name, attr] pair
407
+ # per attribute per instance (~5 allocs/instance on
408
+ # hydration-heavy corpora); Array#each reuses its elements.
409
+ instantiate_attr_pairs(register_id).each do |name, attr|
406
410
  next if attr.derived?
407
411
 
408
412
  # Symbol keys hit directly (native-extension callers);
@@ -468,6 +472,14 @@ module Lutaml
468
472
  end
469
473
  end
470
474
 
475
+ # [name, attr] pairs in attributes order — instantiate's loop
476
+ # iterates this instead of the attributes Hash so the per-call
477
+ # pair array disappears (see the instantiate comment).
478
+ def instantiate_attr_pairs(register_id = nil)
479
+ (@instantiate_attr_pairs ||= {})[register_id] ||=
480
+ attributes(register_id).to_a.freeze
481
+ end
482
+
471
483
  # Define register-specific attribute methods on the class itself.
472
484
  #
473
485
  # Called once per (class, register) combination. Replaces per-instance
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.106"
5
+ VERSION = "0.8.108"
6
6
  end
7
7
  end
@@ -45,7 +45,7 @@ module Lutaml
45
45
  element_buckets(node, plan[:contested_names])
46
46
  end
47
47
  attr_kwargs = attributes_kwargs(plan, value, node)
48
- child_kwargs, children, delegates =
48
+ child_kwargs, children, delegates, run_children =
49
49
  children_kwargs(model_class, plan, value, buckets, register)
50
50
  plan[:collection_defaults].each do |name|
51
51
  next if attr_kwargs.key?(name) || child_kwargs.key?(name)
@@ -88,7 +88,8 @@ module Lutaml
88
88
  child.lutaml_parent = instance
89
89
  child.lutaml_root ||= instance.lutaml_root || instance
90
90
  end
91
- interpret_deferred(model_class, plan, value, instance, node, buckets)
91
+ interpret_deferred(model_class, plan, value, instance, node,
92
+ buckets, run_children)
92
93
  route_delegates(delegates, instance)
93
94
  instance
94
95
  end
@@ -148,8 +149,12 @@ module Lutaml
148
149
  kwargs = {}
149
150
  sole_claimants = sole_claimant_names(plan)
150
151
  ns_attr_names = plan[:ns_attr_names]
151
- plan_attr_rows(plan).each do |rule, attr, name|
152
- v = value.attribute(name)
152
+ rows = plan_attr_rows(plan)
153
+ # #1670 batch face (leptris 1.9.351.1): one FFI crossing
154
+ # per node instead of one per (node x attribute name).
155
+ bound = value.attributes_snapshot
156
+ rows.each do |rule, attr, name|
157
+ v = bound[name]
153
158
  if v.nil? && node && sole_claimants.include?(name)
154
159
  # #754/#790 parity: a sole-claimant attribute rule binds
155
160
  # any qualification (the interpretive matcher's lenient
@@ -239,7 +244,8 @@ module Lutaml
239
244
  def children_kwargs(_model_class, plan, value, buckets = nil,
240
245
  register = nil)
241
246
  register ||= Lutaml::Model::Config.default_register
242
- grouped, tagged = group_children(value, plan[:row_tags])
247
+ grouped, tagged, run_children = group_children(value,
248
+ plan[:row_tags])
243
249
  buckets = nil unless buckets && plan[:needs_nodes]
244
250
  kwargs = {}
245
251
  children = []
@@ -250,9 +256,17 @@ module Lutaml
250
256
  row_tags = plan[:row_tags]
251
257
  wire_names = row_wire_names(plan)
252
258
  row_memo = child_row_plans(plan, register)
259
+ # Array#each with a top-of-loop counter: each_with_index
260
+ # mints an enumerator per hydrated node — 14.7K allocations
261
+ # per parse on the pnas guardrail — and next must keep its
262
+ # continue semantics.
263
+ rows = plan[:rows]
264
+ idx = -1
253
265
  # rubocop:disable Metrics/BlockLength -- one row-kind
254
266
  # dispatch table; splitting it would scatter the contract
255
- plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate, _row_type), idx|
267
+ rows.each do |row|
268
+ idx += 1
269
+ rule, attr, kind, spelling, delegate, _row_type = row
256
270
  tag = row_tags&.[](idx)
257
271
  name = wire_names[idx]
258
272
  case kind
@@ -284,7 +298,7 @@ module Lutaml
284
298
  when :wildcard_any
285
299
  # interpreted post-instance (interpret_deferred)
286
300
  when :content
287
- runs = content_runs(value)
301
+ runs = content_runs(value, run_children)
288
302
  if runs.empty? && !attr.collection?
289
303
  # An empty element's content binds through the rule's
290
304
  # value map for scalar content attributes: "" for the
@@ -375,11 +389,17 @@ module Lutaml
375
389
  cursor = if child_plan[:needs_nodes] && buckets
376
390
  selected
377
391
  end
378
- items = values.each_with_index
379
- .map do |v, i|
380
- call(child_type, child_plan, v,
381
- node: cursor && cursor[i], register: child_register)
382
- end
392
+ # Indexed loop: each_with_index.map mints an
393
+ # enumerator per node with nested rows.
394
+ count = values.size
395
+ items = Array.new(count)
396
+ pos = 0
397
+ while pos < count
398
+ items[pos] = call(child_type, child_plan, values[pos],
399
+ node: cursor && cursor[pos],
400
+ register: child_register)
401
+ pos += 1
402
+ end
383
403
  end
384
404
  next if items.empty?
385
405
 
@@ -402,7 +422,7 @@ module Lutaml
402
422
  attr.collection? ? values : values.first)
403
423
  end
404
424
  end
405
- [kwargs, children, delegates]
425
+ [kwargs, children, delegates, run_children]
406
426
  end
407
427
 
408
428
  # Deferred islands: raw subtrees become wrapper elements via a
@@ -413,15 +433,20 @@ module Lutaml
413
433
  # node (no fragment parse — the reparse dominated deferred
414
434
  # hydration). Ordered children only land here without a node.
415
435
  def interpret_deferred(model_class, plan, value, instance, node = nil,
416
- buckets = nil)
417
- plan[:rows].each_with_index do |row, idx|
436
+ buckets = nil, run_children = nil)
437
+ # Array#each + top counter: no per-node enumerator (see
438
+ # children_kwargs).
439
+ rows = plan[:rows]
440
+ idx = -1
441
+ rows.each do |row|
442
+ idx += 1
418
443
  rule = row[0]
419
444
  attr = row[1]
420
445
  kind = row[2]
421
446
  wire_name = row_wire_names(plan)[idx]
422
447
  case kind
423
448
  when :content_deferred
424
- runs = content_runs(value)
449
+ runs = content_runs(value, run_children)
425
450
  if runs.empty?
426
451
  # The interpretive pipeline classifies the raw ""
427
452
  # through the rule's value map: the :empty directive
@@ -587,15 +612,14 @@ module Lutaml
587
612
  end
588
613
  end
589
614
 
590
- def content_runs(value)
591
- out = []
592
- value.count.times do |i|
593
- c = value.at(i)
594
- next unless c.name.nil? && c.kind == :collection
595
-
596
- out.concat(Array.new(c.count) { |j| c.at(j).string_value })
615
+ # Content runs ride the children snapshot: the nil-name
616
+ # entries ARE the :collection run groups (one crossing per
617
+ # node via group_children, none here). The per-group
618
+ # at/count crossings are the drain itself.
619
+ def content_runs(_value, run_children)
620
+ (run_children || []).flat_map do |group|
621
+ Array.new(group.count) { |j| group.at(j).string_value }
597
622
  end
598
- out
599
623
  end
600
624
 
601
625
  # Native collection rows echo their producing row's name
@@ -663,8 +687,14 @@ module Lutaml
663
687
  grouped = {}
664
688
  want_tags = !row_tags.nil?
665
689
  tagged = want_tags ? {} : nil
690
+ run_children = nil
666
691
  names.each_with_index do |name, i|
667
- next if name.nil? # content runs, read separately
692
+ if name.nil?
693
+ # Content runs: same snapshot pass — at()/count() per
694
+ # child cost a crossing and a wrapper each.
695
+ (run_children ||= []) << children[i]
696
+ next
697
+ end
668
698
 
669
699
  child = children[i]
670
700
  (grouped[name] ||= []) << child
@@ -672,7 +702,7 @@ module Lutaml
672
702
  (tagged[tags[i]] ||= []) << child
673
703
  end
674
704
  end
675
- [grouped, tagged]
705
+ [grouped, tagged, run_children]
676
706
  end
677
707
 
678
708
  # Element children bucketed by local name, document order
@@ -8,16 +8,18 @@ module Lutaml
8
8
  # reference leptris types directly (the moxml adapters and the
9
9
  # interpretive path never enter here).
10
10
  module Leptris
11
- # Single capability floor (leptris 1.9.336.0): the child-handles
11
+ # Single capability floor (leptris 1.9.351.1): the child-handles
12
12
  # snapshot (1.9.273), exact-URI attribute rows (#1486, 1.9.289),
13
13
  # nested-attribute capture (#1563, 1.9.313), wildcard rows
14
14
  # (#1552, 1.9.313), child-row ns_uri retention and qualified
15
15
  # attribute leniency (#1585/#1586, 1.9.320), the :unqualified ns
16
- # form (#1560, 1.9.323), and build_result with the address-based
17
- # text attach (#408, 1.9.334/1.9.336). Bundles resolving an
18
- # older gem keep the interpretive path — PlanCompiler.compile
19
- # answers nil — the opt-out protocol.
20
- MIN_LEPTRIS_VERSION = "1.9.336.0"
16
+ # form (#1560, 1.9.323), build_result with the address-based
17
+ # text attach (#408, 1.9.334/1.9.336), and the plan-value
18
+ # attributes batch face (PlanValue#attributes_snapshot,
19
+ # leptris-ruby#447). Bundles resolving an older gem keep the
20
+ # interpretive path — PlanCompiler.compile answers nil — the
21
+ # opt-out protocol.
22
+ MIN_LEPTRIS_VERSION = "1.9.351.1"
21
23
 
22
24
  # Memoized: the compiler consults this on every cache hit (per
23
25
  # child row per node), and the comparison minted a Gem::Version
@@ -40,6 +40,17 @@ module Lutaml
40
40
  end
41
41
  private_class_method :require_engine
42
42
 
43
+ # True where the yeptris engine loads (libyeptris present for
44
+ # this platform). Idempotent; a missing or broken install
45
+ # reports false instead of raising, so optional-engine callers
46
+ # can gate on it.
47
+ def self.available?
48
+ require_engine
49
+ true
50
+ rescue LoadError
51
+ false
52
+ end
53
+
43
54
  # The parse-error contract is the standard adapter's: invalid
44
55
  # input raises Psych::SyntaxError regardless of the engine, so
45
56
  # consumers' rescue ladders (and this gem's own
@@ -1,16 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
- begin
5
- require "lutaml/json/adapter/yeptris_adapter"
6
- rescue LoadError
7
- # yeptris ships no platform gem here (e.g. Windows): every example skips.
8
- end
9
-
10
- YEPTRIS_AVAILABLE = defined?(Yeptris)
3
+ require "lutaml/json/adapter/yeptris_adapter"
11
4
 
12
- RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Object) do
13
- before { skip "yeptris is not available on this platform" unless YEPTRIS_AVAILABLE }
5
+ RSpec.describe Lutaml::Json::Adapter::YeptrisAdapter do
6
+ before { skip "yeptris is not available on this platform" unless described_class.available? }
14
7
 
15
8
  let(:attributes) { { "name" => "John", "age" => 30, "roles" => %w[admin dev] } }
16
9
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
+ require "open3"
4
5
 
5
6
  RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for with a configured type" do
6
7
  after do
@@ -22,17 +23,39 @@ RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for with a configured t
22
23
  expect(adapter.name).to include("Json::Standard")
23
24
  end
24
25
 
26
+ # Runs in a subprocess: whether the engine is already loaded is a
27
+ # process-global fact, so any in-process assertion would depend on
28
+ # spec file order (the yeptris adapter faces load it).
25
29
  it "does not load the optional yeptris engine when a standard adapter is configured" do
26
- skip "yeptris already loaded by an earlier spec" if Object.const_defined?(:Yeptris)
30
+ probe = <<~RUBY
31
+ require "lutaml/model"
27
32
 
28
- Lutaml::Model::Config.configure do |config|
29
- config.yaml_adapter_type = :standard
30
- config.json_adapter_type = :standard
31
- end
33
+ Lutaml::Model::Config.configure do |config|
34
+ config.yaml_adapter_type = :standard
35
+ config.json_adapter_type = :standard
36
+ end
37
+
38
+ adapter_yaml = Lutaml::Model::AdapterResolver.adapter_for(:yaml)
39
+ adapter_json = Lutaml::Model::AdapterResolver.adapter_for(:json)
32
40
 
33
- described_class.adapter_for(:yaml)
34
- described_class.adapter_for(:json)
41
+ # Parsing through the configured adapters must likewise stay on
42
+ # the standard engines.
43
+ adapter_yaml.parse("a: 1")
44
+ adapter_json.parse('{"a":1}')
45
+
46
+ exit(Object.const_defined?(:Yeptris) ? 1 : 0)
47
+ RUBY
48
+
49
+ _stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", probe)
50
+ expect(status).to be_success, stderr
51
+ end
52
+ end
35
53
 
36
- expect(Object.const_defined?(:Yeptris)).to be(false)
54
+ RSpec.describe "plan lane defaults" do
55
+ it "ships the ordered plan lane opt-in until leptris#1743 is fixed" do
56
+ # The native ordered lane runs away on sts-ruby's ISO_13849-1
57
+ # document (leptris#1743): 28+ CPU-min / 14 GB RSS vs 6.36s on the
58
+ # same parse with the lane off. Opt-in until the engine fix ships.
59
+ expect(Lutaml::Model::Config.instance.xml_plan_ordered_path).to be(false)
37
60
  end
38
61
  end
@@ -31,13 +31,14 @@ RSpec.describe "XML plan fast path" do
31
31
  let(:root_class) { classes[1] }
32
32
 
33
33
  around do |example|
34
- old = Lutaml::Model::Config.instance.xml_plan_fast_path
34
+ old_fast = Lutaml::Model::Config.instance.xml_plan_fast_path
35
+ old_ordered = Lutaml::Model::Config.instance.xml_plan_ordered_path
35
36
  Lutaml::Model::Config.instance.xml_plan_fast_path = true
36
37
  Lutaml::Model::Config.instance.xml_plan_ordered_path = true
37
38
  example.run
38
39
  ensure
39
- Lutaml::Model::Config.instance.xml_plan_fast_path = old
40
- Lutaml::Model::Config.instance.xml_plan_ordered_path = false
40
+ Lutaml::Model::Config.instance.xml_plan_fast_path = old_fast
41
+ Lutaml::Model::Config.instance.xml_plan_ordered_path = old_ordered
41
42
  end
42
43
 
43
44
  # Real-corpus crash (niso-jats): sec-in-sec nesting is a
@@ -1,16 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
- begin
5
- require "lutaml/yaml/adapter/yeptris_adapter"
6
- rescue LoadError
7
- # yeptris ships no platform gem here (e.g. Windows): every example skips.
8
- end
9
-
10
- YEPTRIS_AVAILABLE = defined?(Yeptris)
3
+ require "lutaml/yaml/adapter/yeptris_adapter"
11
4
 
12
- RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Object) do
13
- before { skip "yeptris is not available on this platform" unless YEPTRIS_AVAILABLE }
5
+ RSpec.describe Lutaml::Yaml::Adapter::YeptrisAdapter do
6
+ before { skip "yeptris is not available on this platform" unless described_class.available? }
14
7
 
15
8
  let(:attributes) { { "name" => "John", "roles" => %w[admin dev] } }
16
9
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.106
4
+ version: 0.8.108
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-12 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64