lutaml-model 0.8.56 → 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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/scripts/test_installed_gem.rb +58 -0
  3. data/.github/workflows/downstream-performance.yml +28 -3
  4. data/.github/workflows/js-pr-check.yml +4 -0
  5. data/.github/workflows/js-sync.yml +4 -0
  6. data/.github/workflows/opal.yml +9 -0
  7. data/.github/workflows/rake.yml +6 -41
  8. data/.github/workflows/release.yml +41 -0
  9. data/Gemfile +17 -3
  10. data/bench/bench_alloc_audit.rb +56 -0
  11. data/bench/bench_common.rb +11 -0
  12. data/bench/bench_merge_attempts.rb +49 -0
  13. data/docs/_guides/gem-integration.adoc +85 -0
  14. data/docs/_guides/grammar-backed-formats.adoc +69 -0
  15. data/docs/_guides/index.adoc +8 -1
  16. data/docs/_guides/native-engines.adoc +23 -5
  17. data/docs/_guides/schema-generation.adoc +25 -0
  18. data/docs/_guides/type-registers-contexts.adoc +126 -0
  19. data/lib/compat/opal/lutaml_model_boot.rb +7 -5
  20. data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
  21. data/lib/lutaml/key_value/data_model/element.rb +5 -1
  22. data/lib/lutaml/key_value/mapping_rule.rb +7 -0
  23. data/lib/lutaml/key_value/transform.rb +29 -24
  24. data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
  25. data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
  26. data/lib/lutaml/key_value/transformation.rb +64 -116
  27. data/lib/lutaml/model/adapter_resolver.rb +7 -3
  28. data/lib/lutaml/model/attribute.rb +74 -15
  29. data/lib/lutaml/model/cached_type_resolver.rb +13 -6
  30. data/lib/lutaml/model/configuration.rb +13 -0
  31. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
  33. data/lib/lutaml/model/render_policy.rb +26 -8
  34. data/lib/lutaml/model/schema/base_schema.rb +15 -0
  35. data/lib/lutaml/model/schema/generator/definition.rb +52 -8
  36. data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
  37. data/lib/lutaml/model/schema.rb +4 -2
  38. data/lib/lutaml/model/serializable.rb +33 -0
  39. data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
  40. data/lib/lutaml/model/serialize/builder.rb +22 -0
  41. data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
  42. data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
  43. data/lib/lutaml/model/serialize/initialization.rb +54 -7
  44. data/lib/lutaml/model/serialize/model_import.rb +60 -0
  45. data/lib/lutaml/model/serialize.rb +14 -6
  46. data/lib/lutaml/model/type/time.rb +6 -0
  47. data/lib/lutaml/model/type/value.rb +9 -7
  48. data/lib/lutaml/model/type_resolver.rb +7 -5
  49. data/lib/lutaml/model/utils.rb +18 -5
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/model.rb +7 -0
  52. data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
  53. data/lib/lutaml/xml/adapter_loader.rb +5 -2
  54. data/lib/lutaml/xml/format.rb +18 -16
  55. data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
  56. data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
  57. data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
  58. data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
  59. data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
  60. data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
  61. data/lib/lutaml/xml/leptris.rb +46 -0
  62. data/lib/lutaml/xml/mapping.rb +71 -18
  63. data/lib/lutaml/xml/model_transform.rb +231 -8
  64. data/lib/lutaml/xml/namespace.rb +18 -8
  65. data/lib/lutaml/xml/namespace_collector.rb +17 -1
  66. data/lib/lutaml/xml/namespace_needs.rb +77 -49
  67. data/lib/lutaml/xml/parse_session.rb +7 -19
  68. data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
  69. data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
  70. data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
  71. data/lib/lutaml/xml/transformation.rb +15 -1
  72. data/lib/lutaml/xml/type/serializers.rb +8 -1
  73. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
  74. data/lib/tasks/performance_probe_plan_walk.rb +1 -1
  75. data/lutaml-model.gemspec +7 -4
  76. data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
  77. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
  78. data/spec/lutaml/key_value/transformation_spec.rb +87 -21
  79. data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
  80. data/spec/lutaml/model/collection_validation_spec.rb +265 -0
  81. data/spec/lutaml/model/desc_spec.rb +50 -0
  82. data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
  83. data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
  84. data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
  85. data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
  86. data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
  87. data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
  88. data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
  89. data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
  90. data/spec/lutaml/model/store_spec.rb +6 -2
  91. data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
  92. data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
  93. data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
  94. data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
  95. data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
  96. data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
  97. data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
  98. data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
  99. data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
  100. data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
  101. data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
  102. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
  103. data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
  104. data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
  105. data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
  106. data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
  107. data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
  108. data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
  109. data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
  110. data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
  111. data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
  112. data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
  113. data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
  114. data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
  115. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
  116. data/spec/person_spec.rb +8 -0
  117. metadata +48 -17
  118. data/lib/lutaml/xml/plan_compiler.rb +0 -332
  119. data/lib/lutaml/xml/plan_hydrator.rb +0 -367
  120. data/lib/lutaml/xml/plan_order.rb +0 -53
  121. data/lib/lutaml/xml/plan_serializer.rb +0 -252
  122. data/lib/lutaml/xml/plan_walk.rb +0 -30
@@ -33,15 +33,18 @@ RSpec.describe "XML plan fast path" do
33
33
  around do |example|
34
34
  old = Lutaml::Model::Config.instance.xml_plan_fast_path
35
35
  Lutaml::Model::Config.instance.xml_plan_fast_path = true
36
+ Lutaml::Model::Config.instance.xml_plan_ordered_path = true
36
37
  example.run
37
38
  ensure
38
39
  Lutaml::Model::Config.instance.xml_plan_fast_path = old
40
+ Lutaml::Model::Config.instance.xml_plan_ordered_path = false
39
41
  end
40
42
 
41
43
  # Real-corpus crash (niso-jats): sec-in-sec nesting is a
42
- # self-referential model — compile must resolve it to nil instead of
43
- # recursing to SystemStackError.
44
- it "opts self-referential models out instead of recursing" do
44
+ # self-referential model — compile must not recurse to
45
+ # SystemStackError. The cycle point defers the child ROW (per-node
46
+ # interpretive islands) instead of opting the model out.
47
+ it "defers the self-referential child row instead of recursing" do
45
48
  node = Class.new(Lutaml::Model::Serializable) do
46
49
  attribute :text, :string
47
50
  xml do
@@ -57,17 +60,21 @@ RSpec.describe "XML plan fast path" do
57
60
  end
58
61
  stub_const("PlanFastPath::SelfRef", node)
59
62
 
60
- expect(Lutaml::Xml::PlanCompiler.compile(node, :default)).to be_nil
63
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(node, :default)
64
+ expect(plan).not_to be_nil
65
+ expect(plan[:rows].map { |r| r[2] }).to include(:child_deferred)
61
66
 
62
67
  xml = "<n>#{'<n>t</n>' * 200}</n>"
63
68
  parsed = node.from_xml(xml)
64
69
  expect(parsed).to be_a(node)
65
70
  end
66
71
 
67
- # lutaml-model#744: attribute plan rows are local-name keyed — a
68
- # type-namespaced attribute (xmi:type vs type) must take the
69
- # interpretive matcher, or the namespaced value is silently lost.
70
- it "opts models with type-namespaced attributes out to the interpretive path" do
72
+ # lutaml-model#744/#758: a local name claimed by BOTH a
73
+ # type-namespaced attribute row and a plain row stays interpretive —
74
+ # the walk's ns form :none matches any qualification, so the plain
75
+ # row would capture the qualified spelling too. The interpretive
76
+ # matcher dispatches the spellings correctly.
77
+ it "opts shared-local-name attribute claims out to the interpretive path" do
71
78
  xmi_ns = Class.new(Lutaml::Xml::Namespace) do
72
79
  uri "http://www.omg.org/spec/XMI/20131001"
73
80
  prefix_default "xmi"
@@ -94,7 +101,7 @@ RSpec.describe "XML plan fast path" do
94
101
 
95
102
  expect(parsed.xmi_type).to eq("uml:Parameter")
96
103
  expect(parsed.classifier_type).to eq("EAnone_void")
97
- expect(Lutaml::Xml::PlanCompiler.compile(parameter, :default)).to be_nil
104
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(parameter, :default)).to be_nil
98
105
  end
99
106
 
100
107
  it "is on by default and can be disabled" do
@@ -156,7 +163,7 @@ RSpec.describe "XML plan fast path" do
156
163
  end
157
164
  end
158
165
 
159
- expect(Lutaml::Xml::PlanCompiler.compile(klass,
166
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
160
167
  Lutaml::Model::Config.default_register)).not_to be_nil
161
168
  expect(klass.from_xml("<doc><note>hi</note></doc>").note).to eq("HI")
162
169
  end
@@ -223,14 +230,14 @@ RSpec.describe "XML plan fast path" do
223
230
  end
224
231
  end
225
232
 
226
- expect(Lutaml::Xml::PlanCompiler.compile(klass,
233
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
227
234
  Lutaml::Model::Config.default_register)).not_to be_nil
228
235
  parsed = klass.from_xml("<doc><note>hi</note></doc>")
229
236
  expect(parsed.tgt.note).to eq("hi")
230
237
  end
231
238
 
232
239
  it "routes a single collection row natively (hybrid routing)" do
233
- plan = Lutaml::Xml::PlanCompiler.compile(
240
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
234
241
  Class.new(Lutaml::Model::Serializable) do
235
242
  attribute :xs, :string, collection: true
236
243
  attribute :name, :string
@@ -400,7 +407,7 @@ RSpec.describe "XML plan fast path" do
400
407
  map_element "para", to: :para
401
408
  end
402
409
  end
403
- plan = Lutaml::Xml::PlanCompiler.compile(
410
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
404
411
  doc_class, Lutaml::Model::Config.default_register
405
412
  )
406
413
  expect(plan).not_to be_nil
@@ -623,7 +630,7 @@ RSpec.describe "XML plan fast path" do
623
630
  expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
624
631
  expect(req.purpose.map(&:text)).to eq(["p1"])
625
632
 
626
- expect(Lutaml::Xml::PlanCompiler.compile(partitioned,
633
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(partitioned,
627
634
  :default)).to be_nil
628
635
  end
629
636
 
@@ -688,4 +695,105 @@ RSpec.describe "XML plan fast path" do
688
695
  expect(parsed.to_xml).to eq(xml)
689
696
  end
690
697
  end
698
+
699
+ # lutaml-model#847: the plan serializer does not emit namespace
700
+ # declarations — namespaced models must serialize through the
701
+ # interpretive writer even standalone, or the element xmlns is lost.
702
+ it "falls back to the interpretive writer for namespaced models" do
703
+ ns = Class.new(Lutaml::Xml::Namespace) do
704
+ uri "http://example.com/ord847"
705
+ end
706
+ stub_const("PlanFastPath::Ord847Ns", ns)
707
+ klass = Class.new(Lutaml::Model::Serializable) do
708
+ attribute :value, :boolean
709
+ xml do
710
+ element "updateFields"
711
+ namespace PlanFastPath::Ord847Ns
712
+ map_attribute "val", to: :value
713
+ end
714
+ end
715
+ stub_const("PlanFastPath::UpdateFields847", klass)
716
+
717
+ expect(klass.new(value: false).to_xml).to include("http://example.com/ord847")
718
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
719
+ klass, Lutaml::Model::Config.default_register
720
+ )
721
+ expect(plan[:namespaced]).to be(true)
722
+ end
723
+
724
+ # lutaml-model#850: a sole-claimant attribute rule binds any
725
+ # qualification on the plan path (#754/#790 parity with the
726
+ # interpretive matcher) — the qualified spelling is read off the
727
+ # source node when the exact (URI, local) walk capture misses.
728
+ it "binds qualified spellings to sole-claimant attribute rules" do
729
+ ns = Class.new(Lutaml::Xml::Namespace) do
730
+ uri "http://example.com/w"
731
+ prefix_default "w"
732
+ end
733
+ stub_const("PlanFastPath::W850Ns", ns)
734
+ klass = Class.new(Lutaml::Model::Serializable) do
735
+ attribute :value, :boolean
736
+ xml do
737
+ element "b"
738
+ namespace PlanFastPath::W850Ns
739
+ map_attribute "val", to: :value
740
+ end
741
+ end
742
+ stub_const("PlanFastPath::Sole850", klass)
743
+
744
+ doc = %(<w:b xmlns:w="http://example.com/w" w:val="false"/>)
745
+ expect(klass.from_xml(doc).value).to be(false)
746
+ expect(klass.from_xml(
747
+ %(<b xmlns="http://example.com/w" val="false"/>),
748
+ ).value).to be(false)
749
+ end
750
+
751
+ # lutaml-model#856: a deferred content row with no captured runs
752
+ # must still mark its attribute set — the interpretive pipeline
753
+ # marks every applied rule (model_transform apply). Without the
754
+ # mark, the serializer's default-suppression drops a mapped reader
755
+ # that derives content from other attributes.
756
+ it "renders derived content readers when no text was captured" do
757
+ klass = Class.new(Lutaml::Model::Serializable) do
758
+ attribute :content, :string
759
+ attribute :revision_date, :string
760
+ attribute :draft, :string
761
+
762
+ xml do
763
+ element "version"
764
+ map_content to: :content
765
+ map_element "revision-date", to: :revision_date
766
+ map_element "draft", to: :draft
767
+ end
768
+
769
+ define_method(:content) do
770
+ return @content if @content && !@content.empty?
771
+
772
+ parts = [@draft, @revision_date].compact
773
+ case parts.size
774
+ when 2 then "#{@draft} (#{@revision_date})"
775
+ when 1 then parts.first
776
+ end
777
+ end
778
+ end
779
+ stub_const("PlanFastPath::DerivedContent856", klass)
780
+
781
+ parsed = nil
782
+ Lutaml::Model::Config.with_adapter(xml: :leptris) do
783
+ parsed = klass.from_xml(
784
+ "<version><revision-date>1994-01-01</revision-date><draft>PD</draft></version>",
785
+ )
786
+ expect(parsed.using_default?(:content)).to be(false)
787
+ expect(parsed.to_xml).to eq(
788
+ "<version><revision-date>1994-01-01</revision-date>" \
789
+ "<draft>PD</draft>PD (1994-01-01)</version>",
790
+ )
791
+ end
792
+ Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
793
+ expect(parsed.to_xml).to eq(
794
+ "<version><revision-date>1994-01-01</revision-date>" \
795
+ "<draft>PD</draft>PD (1994-01-01)</version>",
796
+ )
797
+ end
798
+ end
691
799
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XML plan serializer: namespaced models" do
6
+ # lutaml-model#847: the plan path used to refuse namespaced models
7
+ # outright (the xmlns dropped). The DOM emission now spells prefixed
8
+ # names, hoists every distinct (prefix, uri) declaration exactly once
9
+ # onto the output root, and prefixes type-namespaced attribute rows —
10
+ # byte-equal with the interpretive writer.
11
+
12
+ let(:wml_ns) do
13
+ Class.new(Lutaml::Xml::Namespace) do
14
+ uri "http://example.com/gate2/wml"
15
+ prefix_default "w"
16
+ end
17
+ end
18
+
19
+ let(:val_type) do
20
+ stub_const("Gate2Ns::WmlNs", wml_ns)
21
+ type = Class.new(Lutaml::Model::Type::String) do
22
+ include Lutaml::Xml::Type::Configurable
23
+
24
+ xml do
25
+ namespace Gate2Ns::WmlNs
26
+ end
27
+ end
28
+ stub_const("Gate2Ns::ValType", type)
29
+ type
30
+ end
31
+
32
+ let(:doc_class) do
33
+ val_type
34
+ body_class
35
+ model = Class.new(Lutaml::Model::Serializable) do
36
+ attribute :name, Gate2Ns::ValType
37
+ attribute :body, Gate2Ns::Body
38
+
39
+ xml do
40
+ element "document"
41
+ namespace Gate2Ns::WmlNs
42
+ map_attribute "name", to: :name
43
+ map_element "body", to: :body
44
+ end
45
+ end
46
+ stub_const("Gate2Ns::Document", model)
47
+ model
48
+ end
49
+
50
+ let(:body_class) do
51
+ val_type
52
+ model = Class.new(Lutaml::Model::Serializable) do
53
+ attribute :text, Gate2Ns::ValType, collection: true
54
+
55
+ xml do
56
+ element "body"
57
+ namespace Gate2Ns::WmlNs
58
+ map_element "t", to: :text
59
+ end
60
+ end
61
+ stub_const("Gate2Ns::Body", model)
62
+ model
63
+ end
64
+
65
+ let(:default_ns_model) do
66
+ ns = Class.new(Lutaml::Xml::Namespace) do
67
+ uri "http://example.com/gate2/default"
68
+ end
69
+ stub_const("Gate2Ns::DefaultNs", ns)
70
+ model = Class.new(Lutaml::Model::Serializable) do
71
+ attribute :name, :string
72
+
73
+ xml do
74
+ element "root"
75
+ namespace Gate2Ns::DefaultNs
76
+ map_attribute "name", to: :name
77
+ end
78
+ end
79
+ stub_const("Gate2Ns::DefaultDoc", model)
80
+ model
81
+ end
82
+
83
+ around do |example|
84
+ old = Lutaml::Model::Config.instance.xml_plan_fast_path
85
+ Lutaml::Model::Config.instance.xml_plan_fast_path = true
86
+ Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
87
+ ensure
88
+ Lutaml::Model::Config.instance.xml_plan_fast_path = old
89
+ end
90
+
91
+ def plan_to_xml(klass, instance)
92
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
93
+ klass, Lutaml::Model::Config.default_register
94
+ )
95
+ Lutaml::Xml::Leptris::PlanSerializer.call(
96
+ instance, plan, Lutaml::Model::Config.default_register
97
+ )
98
+ end
99
+
100
+ it "emits prefixed names with the declaration exactly once" do
101
+ instance = doc_class.new(
102
+ name: "d1",
103
+ body: body_class.new(text: ["hello"]),
104
+ )
105
+
106
+ out = plan_to_xml(doc_class, instance)
107
+ expect(out).to include(%(xmlns:w="http://example.com/gate2/wml"))
108
+ expect(out.scan("xmlns:w=").count).to eq(1)
109
+ expect(out).to include("<w:document")
110
+ expect(out).to include("w:name=\"d1\"")
111
+ expect(out).to include("<w:body>")
112
+ expect(out).to include("<w:t>hello</w:t>")
113
+ end
114
+
115
+ # The plan path spells the type-namespaced attribute w:name=; the
116
+ # interpretive writer emits name= unprefixed (a long-standing
117
+ # interpretive serialize gap — the unprefixed form does not rebind
118
+ # to the (URI, local) rule on reparse). Element tree, declarations,
119
+ # and values are compared; the attribute spelling divergence is
120
+ # pinned by the round-trip example below.
121
+ it "matches the interpretive element tree and declarations" do
122
+ instance = doc_class.new(
123
+ name: "d1",
124
+ body: body_class.new(text: ["hello", "world"]),
125
+ )
126
+
127
+ interpretive = Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
128
+ instance.to_xml
129
+ end
130
+
131
+ plan_out = plan_to_xml(doc_class, instance)
132
+ expect(plan_out.sub(" w:name=\"d1\"", "")).to eq(
133
+ interpretive.sub(" name=\"d1\"", "").strip,
134
+ )
135
+ end
136
+
137
+ it "round-trips through the plan parse path" do
138
+ instance = doc_class.new(
139
+ name: "d1",
140
+ body: body_class.new(text: ["hello"]),
141
+ )
142
+ xml = plan_to_xml(doc_class, instance)
143
+
144
+ reparsed = doc_class.from_xml(xml)
145
+ expect(reparsed.name).to eq("d1")
146
+ expect(reparsed.body.text).to eq(["hello"])
147
+ end
148
+
149
+ it "spells default-namespace models unprefixed with xmlns on the root" do
150
+ instance = default_ns_model.new(name: "plain")
151
+
152
+ out = plan_to_xml(default_ns_model, instance)
153
+ expect(out).to include(%(xmlns="http://example.com/gate2/default"))
154
+ expect(out).to include("<root")
155
+ expect(out).not_to include("xmlns:xmlns")
156
+ end
157
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XML plan fast path: namespace-less models on namespaced documents" do
6
+ # lutaml-model#932: elements (unlike attributes) inherit the default
7
+ # xmlns, so the walk's strict no-URI default dropped every child of a
8
+ # namespaced document for models that declare no namespace. The
9
+ # ns_lenient flag restores the interpretive matcher's behavior:
10
+ # children match by local name under any document qualification.
11
+
12
+ let(:child_class) do
13
+ klass = Class.new(Lutaml::Model::Serializable) do
14
+ attribute :fileref, :string
15
+
16
+ xml do
17
+ element "imagedata"
18
+ map_attribute "fileref", to: :fileref
19
+ end
20
+ end
21
+ stub_const("NsLessDoc::Imagedata", klass)
22
+ klass
23
+ end
24
+
25
+ def define_parent
26
+ child_class
27
+ Class.new(Lutaml::Model::Serializable) do
28
+ attribute :imagedata, NsLessDoc::Imagedata
29
+ attribute :content, :string, collection: true
30
+
31
+ xml do
32
+ element "imageobject"
33
+ map_element "imagedata", to: :imagedata
34
+ map_content to: :content
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ around do |example|
41
+ old = Lutaml::Model::Config.instance.xml_plan_fast_path
42
+ Lutaml::Model::Config.instance.xml_plan_fast_path = true
43
+ Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
44
+ ensure
45
+ Lutaml::Model::Config.instance.xml_plan_fast_path = old
46
+ end
47
+
48
+ it "binds nested children under a default namespace" do
49
+ parent = define_parent
50
+ doc = '<imageobject xmlns="http://docbook.org/ns/docbook">' \
51
+ '<imagedata fileref="x.png"/></imageobject>'
52
+
53
+ expect(parent.from_xml(doc).imagedata.fileref).to eq("x.png")
54
+ end
55
+
56
+ it "binds nested children under a prefixed namespace" do
57
+ parent = define_parent
58
+ doc = '<db:imageobject xmlns:db="http://docbook.org/ns/docbook">' \
59
+ '<db:imagedata fileref="x.png"/></db:imageobject>'
60
+
61
+ expect(parent.from_xml(doc).imagedata.fileref).to eq("x.png")
62
+ end
63
+
64
+ it "keeps binding on namespace-less documents" do
65
+ parent = define_parent
66
+ doc = %(<imageobject><imagedata fileref="x.png"/></imageobject>)
67
+
68
+ expect(parent.from_xml(doc).imagedata.fileref).to eq("x.png")
69
+ end
70
+
71
+ it "binds nested children of mixed-content models under a default namespace" do
72
+ parent = define_parent
73
+ parent.xml do
74
+ element "imageobject"
75
+ mixed_content
76
+ map_content to: :content
77
+ map_element "imagedata", to: :imagedata
78
+ end
79
+ doc = '<imageobject xmlns="http://docbook.org/ns/docbook">' \
80
+ 'text<imagedata fileref="x.png"/>tail</imageobject>'
81
+
82
+ parsed = parent.from_xml(doc)
83
+ expect(parsed.imagedata.fileref).to eq("x.png")
84
+ expect(parsed.content).to include("text")
85
+ end
86
+ end
@@ -37,7 +37,7 @@ RSpec.describe "Plan fast path with when_attribute partitions" do
37
37
  end
38
38
 
39
39
  it "compiles the partitioned model with row tags" do
40
- plan = Lutaml::Xml::PlanCompiler.compile(
40
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
41
41
  model, Lutaml::Model::Config.default_register
42
42
  )
43
43
  expect(plan).not_to be_nil
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XML plan fast path: type-namespaced attribute rows" do
6
+ # Attribute identity is (URI, local) (lutaml-model#744). A map_attribute
7
+ # whose TYPE carries a namespace class used to opt the whole model out
8
+ # of the plan path; it now compiles into an exact-URI AttrPlan ns row
9
+ # (libleptris 1.9.289) with the sole-claimant lenient recovery keeping
10
+ # the interpretive exact-first-then-any-qualification precedence.
11
+
12
+ let(:wml_ns) do
13
+ Class.new(Lutaml::Xml::Namespace) do
14
+ uri "http://example.com/type-ns/wml"
15
+ prefix_default "w"
16
+ end
17
+ end
18
+
19
+ let(:val_type) do
20
+ stub_const("PlanTypeNsAttr::WmlNs", wml_ns)
21
+ type = Class.new(Lutaml::Model::Type::String) do
22
+ include Lutaml::Xml::Type::Configurable
23
+
24
+ xml do
25
+ namespace PlanTypeNsAttr::WmlNs
26
+ end
27
+ end
28
+ stub_const("PlanTypeNsAttr::ValType", type)
29
+ type
30
+ end
31
+
32
+ let(:klass) do
33
+ val_type
34
+ model = Class.new(Lutaml::Model::Serializable) do
35
+ attribute :val, PlanTypeNsAttr::ValType
36
+
37
+ xml do
38
+ element "keepNext"
39
+ namespace PlanTypeNsAttr::WmlNs
40
+ map_attribute "val", to: :val
41
+ end
42
+ end
43
+ stub_const("PlanTypeNsAttr::KeepNext", model)
44
+ model
45
+ end
46
+
47
+ around do |example|
48
+ old = Lutaml::Model::Config.instance.xml_plan_fast_path
49
+ Lutaml::Model::Config.instance.xml_plan_fast_path = true
50
+ Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
51
+ ensure
52
+ Lutaml::Model::Config.instance.xml_plan_fast_path = old
53
+ end
54
+
55
+ it "compiles a model whose attribute type carries a namespace" do
56
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
57
+ klass, Lutaml::Model::Config.default_register
58
+ )
59
+
60
+ expect(plan).not_to be_nil
61
+ ns_row = plan[:tree][:attributes].find { |a| a[:name] == "val" }
62
+ expect(ns_row[:ns]).to include(exact: "http://example.com/type-ns/wml")
63
+ expect(ns_row[:ns][:prefix]).to eq("w")
64
+ end
65
+
66
+ it "binds the attribute from the type's namespace" do
67
+ doc = %(<w:keepNext xmlns:w="http://example.com/type-ns/wml" w:val="yes"/>)
68
+
69
+ expect(klass.from_xml(doc).val).to eq("yes")
70
+ end
71
+
72
+ # The interpretive matcher refuses an ambiguous local name outright:
73
+ # a foreign spelling alongside the canonical one binds nothing. The
74
+ # exact-URI row mirrors that refusal (lutaml-model#744).
75
+ it "refuses to bind when a foreign spelling accompanies the canonical one" do
76
+ doc = '<w:keepNext xmlns:w="http://example.com/type-ns/wml" ' \
77
+ 'xmlns:f="http://example.com/foreign" val="foreign" w:val="canonical"/>'
78
+
79
+ expect(klass.from_xml(doc).val).to be_nil
80
+ end
81
+
82
+ # Interpretive parity: the plan path resolves exactly as the
83
+ # interpretive matcher (nokogiri) does on every spelling shape.
84
+ it "matches the interpretive path's resolution on all spelling shapes" do
85
+ canonical = %(<w:keepNext xmlns:w="http://example.com/type-ns/wml" w:val="yes"/>)
86
+ ambiguous = '<w:keepNext xmlns:w="http://example.com/type-ns/wml" ' \
87
+ 'xmlns:f="http://example.com/foreign" val="foreign" w:val="canonical"/>'
88
+ foreign_only = '<w:keepNext xmlns:f="http://example.com/foreign" ' \
89
+ 'xmlns:w="http://example.com/type-ns/wml" val="foreign"/>'
90
+
91
+ [canonical, ambiguous, foreign_only].each do |doc|
92
+ interpretive = Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
93
+ klass.from_xml(doc).val
94
+ end
95
+
96
+ expect(klass.from_xml(doc).val).to eq(interpretive)
97
+ end
98
+ end
99
+
100
+ # A type declaring alias URI families stays interpretive: the plan row
101
+ # holds one exact URI and non-sole-claimant alias families have no
102
+ # plan-side resolution order.
103
+ it "keeps alias-declaring types on the interpretive path" do
104
+ aliased_ns = Class.new(Lutaml::Xml::Namespace) do
105
+ uri "http://example.com/type-ns/canonical"
106
+ uri_aliases "http://example.com/type-ns/alias"
107
+ end
108
+ stub_const("PlanTypeNsAttr::AliasedNs", aliased_ns)
109
+ type = Class.new(Lutaml::Model::Type::String) do
110
+ include Lutaml::Xml::Type::Configurable
111
+
112
+ xml do
113
+ namespace PlanTypeNsAttr::AliasedNs
114
+ end
115
+ end
116
+ stub_const("PlanTypeNsAttr::AliasedValType", type)
117
+ model = Class.new(Lutaml::Model::Serializable) do
118
+ attribute :val, PlanTypeNsAttr::AliasedValType
119
+
120
+ xml do
121
+ element "keepNext"
122
+ map_attribute "val", to: :val
123
+ end
124
+ end
125
+ stub_const("PlanTypeNsAttr::AliasedKeepNext", model)
126
+
127
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(
128
+ model, Lutaml::Model::Config.default_register
129
+ )).to be_nil
130
+ end
131
+ end