lutaml-model 0.8.56 → 0.8.58

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: 8eaff3e9ef7cbf71d8cd5adbf05161b81c43399c92a1c741023005722c190025
4
- data.tar.gz: bd4383a644f8878226034c191215780c38b5f1beee2cdf196e26d31067908878
3
+ metadata.gz: 9c115914d1bb1d737ad93c4ef3d5ecb9090200d8fd3558309421e23fca5a9171
4
+ data.tar.gz: a017029e28d68c4eda43e5e4a79733b6f31c4258ccfb47b0969dae50fe9f0107
5
5
  SHA512:
6
- metadata.gz: 00453571fbb8c6b2540f3d545fe53d0c664fa18301797e5192699a6dded982ca9280a4ac39dcc2a7c60f03746ee0dc986e9571c05f9f387f503ba6ab2b522d5c
7
- data.tar.gz: bdf133791492a00bb00babcc2b1037366918e6eba7bb05001d4b5748152249c7e45c47c0dbd099c72e0f99acf949febf8ae36f43bb99b43b35874112da3f6312
6
+ metadata.gz: 5a9f912ee3bdd11d2fe2266b2de6880553fe64ab95cc6891a5184bf067205a9ab76fc61faee3812ded44af10dcc430f5b7aa6acc6051c6dfdcd622c5276127cc
7
+ data.tar.gz: 61b39b97fe5c532f4aa367b2c7c9d5efe5b43acc786bfe192832d3379f8b8f7b253538550478028f676ea7b8c5e31f5dea5cda189374318bcab7ea0f1c441a32
@@ -103,7 +103,8 @@ module Lutaml
103
103
 
104
104
  register = Lutaml::Model::Config.default_register
105
105
  plan = Lutaml::Xml::PlanCompiler.compile(self, register)
106
- return nil unless plan && Lutaml::Xml::PlanSerializer.serializable?(plan)
106
+ return nil unless plan && !plan[:namespaced] &&
107
+ Lutaml::Xml::PlanSerializer.serializable?(plan)
107
108
 
108
109
  Lutaml::Xml::PlanSerializer.call(instance, plan)
109
110
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.56"
5
+ VERSION = "0.8.58"
6
6
  end
7
7
  end
@@ -213,6 +213,12 @@ module Lutaml
213
213
 
214
214
  row_tags = partition_row_tags!(compiled, rows, tag)
215
215
 
216
+ # The plan serializer does not emit namespace declarations —
217
+ # namespaced models serialize through the interpretive
218
+ # writer (lutaml-model#847: standalone to_xml under leptris
219
+ # dropped the element xmlns entirely).
220
+ namespaced = !model_ns.nil? || rows.any? { |r| r[:ns] }
221
+
216
222
  flags = []
217
223
  flags << :cdata if cdata
218
224
  flags << :mixed_content if mixed_content
@@ -233,7 +239,7 @@ module Lutaml
233
239
 
234
240
  { descriptor: descriptor, tree: tree, rows: compiled,
235
241
  attr_rows: attr_rows, mapping: mapping,
236
- row_tags: row_tags,
242
+ row_tags: row_tags, namespaced: namespaced,
237
243
  ordered: mapping.ordered? || mapping.mixed_content?,
238
244
  needs_nodes: needs_nodes,
239
245
  collection_defaults: collection_defaults }
@@ -22,7 +22,7 @@ module Lutaml
22
22
  # hydrate natively against their own nodes
23
23
  def call(model_class, plan, value, parent: nil, node: nil)
24
24
  buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
25
- attr_kwargs = attributes_kwargs(plan, value)
25
+ attr_kwargs = attributes_kwargs(plan, value, node)
26
26
  child_kwargs, children, delegates =
27
27
  children_kwargs(model_class, plan, value, buckets)
28
28
  plan[:collection_defaults].each do |name|
@@ -53,10 +53,18 @@ module Lutaml
53
53
  Lutaml::Model::Config.default_register
54
54
  end
55
55
 
56
- def attributes_kwargs(plan, value)
56
+ def attributes_kwargs(plan, value, node = nil)
57
57
  kwargs = {}
58
+ sole_claimants = sole_claimant_names(plan)
58
59
  plan[:attr_rows].each do |rule, attr|
59
60
  v = value.attribute(rule.name.to_s)
61
+ if v.nil? && node && sole_claimants.include?(rule.name.to_s)
62
+ # #754/#790 parity: a sole-claimant attribute rule binds
63
+ # any qualification (the interpretive matcher's lenient
64
+ # recovery). The walk captures exact (URI, local) matches
65
+ # only, so the qualified spelling is read off the node.
66
+ v = lenient_node_attribute(node, rule.name.to_s)
67
+ end
60
68
  next if v.nil?
61
69
 
62
70
  v = v.split(rule.delimiter) if rule.delimiter
@@ -331,6 +339,31 @@ module Lutaml
331
339
  Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
332
340
  end
333
341
 
342
+ # Attribute names claimed by exactly one attr row — the only
343
+ # names eligible for #754/#790 lenient binding (multi-claimant
344
+ # names stay exact; per #841 leniency is sole-claimant-only).
345
+ def sole_claimant_names(plan)
346
+ plan[:attr_rows].map { |rule, _attr| rule.name.to_s }
347
+ .tally
348
+ .select { |_n, c| c == 1 }
349
+ .keys
350
+ end
351
+
352
+ # Local-name attribute lookup on the source node, qualified
353
+ # spellings included (w:val matches rule "val").
354
+ def lenient_node_attribute(node, local_name)
355
+ return nil unless node.respond_to?(:attributes)
356
+
357
+ node.attributes.each_value do |attr|
358
+ # Leptris::XML::Attr carries the raw (possibly prefixed)
359
+ # name; strip the prefix for the local-name comparison.
360
+ name = attr.name.to_s
361
+ name = name.split(":").last if name.include?(":")
362
+ return attr.value if name == local_name
363
+ end
364
+ nil
365
+ end
366
+
334
367
  def group_children(value, row_tags = nil)
335
368
  grouped = {}
336
369
  tagged = {}
@@ -688,4 +688,56 @@ RSpec.describe "XML plan fast path" do
688
688
  expect(parsed.to_xml).to eq(xml)
689
689
  end
690
690
  end
691
+
692
+ # lutaml-model#847: the plan serializer does not emit namespace
693
+ # declarations — namespaced models must serialize through the
694
+ # interpretive writer even standalone, or the element xmlns is lost.
695
+ it "falls back to the interpretive writer for namespaced models" do
696
+ ns = Class.new(Lutaml::Xml::Namespace) do
697
+ uri "http://example.com/ord847"
698
+ end
699
+ stub_const("PlanFastPath::Ord847Ns", ns)
700
+ klass = Class.new(Lutaml::Model::Serializable) do
701
+ attribute :value, :boolean
702
+ xml do
703
+ element "updateFields"
704
+ namespace PlanFastPath::Ord847Ns
705
+ map_attribute "val", to: :value
706
+ end
707
+ end
708
+ stub_const("PlanFastPath::UpdateFields847", klass)
709
+
710
+ expect(klass.new(value: false).to_xml).to include("http://example.com/ord847")
711
+ plan = Lutaml::Xml::PlanCompiler.compile(
712
+ klass, Lutaml::Model::Config.default_register
713
+ )
714
+ expect(plan[:namespaced]).to be(true)
715
+ end
716
+
717
+ # lutaml-model#850: a sole-claimant attribute rule binds any
718
+ # qualification on the plan path (#754/#790 parity with the
719
+ # interpretive matcher) — the qualified spelling is read off the
720
+ # source node when the exact (URI, local) walk capture misses.
721
+ it "binds qualified spellings to sole-claimant attribute rules" do
722
+ ns = Class.new(Lutaml::Xml::Namespace) do
723
+ uri "http://example.com/w"
724
+ prefix_default "w"
725
+ end
726
+ stub_const("PlanFastPath::W850Ns", ns)
727
+ klass = Class.new(Lutaml::Model::Serializable) do
728
+ attribute :value, :boolean
729
+ xml do
730
+ element "b"
731
+ namespace PlanFastPath::W850Ns
732
+ map_attribute "val", to: :value
733
+ end
734
+ end
735
+ stub_const("PlanFastPath::Sole850", klass)
736
+
737
+ doc = %(<w:b xmlns:w="http://example.com/w" w:val="false"/>)
738
+ expect(klass.from_xml(doc).value).to be(false)
739
+ expect(klass.from_xml(
740
+ %(<b xmlns="http://example.com/w" val="false"/>),
741
+ ).value).to be(false)
742
+ end
691
743
  end
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.56
4
+ version: 0.8.58
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-09-23 00:00:00.000000000 Z
11
+ date: 2026-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64