lutaml-model 0.8.4 → 0.8.6

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +5 -0
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +91 -22
  5. data/Gemfile +2 -0
  6. data/README.adoc +114 -2
  7. data/docs/_guides/index.adoc +18 -0
  8. data/docs/_guides/jsonld-serialization.adoc +217 -0
  9. data/docs/_guides/rdf-serialization.adoc +344 -0
  10. data/docs/_guides/turtle-serialization.adoc +224 -0
  11. data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
  12. data/docs/_pages/serialization_adapters.adoc +31 -0
  13. data/docs/_references/index.adoc +1 -0
  14. data/docs/_references/rdf-namespaces.adoc +243 -0
  15. data/docs/index.adoc +3 -2
  16. data/lib/lutaml/jsonld/adapter.rb +23 -0
  17. data/lib/lutaml/jsonld/context.rb +69 -0
  18. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  19. data/lib/lutaml/jsonld/transform.rb +174 -0
  20. data/lib/lutaml/jsonld.rb +23 -0
  21. data/lib/lutaml/model/format_registry.rb +10 -1
  22. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  23. data/lib/lutaml/model/version.rb +1 -1
  24. data/lib/lutaml/model.rb +6 -0
  25. data/lib/lutaml/rdf/error.rb +7 -0
  26. data/lib/lutaml/rdf/iri.rb +44 -0
  27. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  28. data/lib/lutaml/rdf/literal.rb +62 -0
  29. data/lib/lutaml/rdf/mapping.rb +71 -0
  30. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  31. data/lib/lutaml/rdf/member_rule.rb +13 -0
  32. data/lib/lutaml/rdf/namespace.rb +58 -0
  33. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  34. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  35. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  36. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  37. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  38. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  39. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  40. data/lib/lutaml/rdf/namespaces.rb +14 -0
  41. data/lib/lutaml/rdf/transform.rb +36 -0
  42. data/lib/lutaml/rdf.rb +19 -0
  43. data/lib/lutaml/turtle/adapter.rb +35 -0
  44. data/lib/lutaml/turtle/mapping.rb +7 -0
  45. data/lib/lutaml/turtle/transform.rb +158 -0
  46. data/lib/lutaml/turtle.rb +22 -0
  47. data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
  48. data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
  49. data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
  50. data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
  51. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
  52. data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
  53. data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
  54. data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
  55. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
  56. data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
  57. data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
  58. data/lib/lutaml/xml/adapter.rb +0 -1
  59. data/lib/lutaml/xml/adapter_element.rb +7 -1
  60. data/lib/lutaml/xml/builder/base.rb +0 -1
  61. data/lib/lutaml/xml/data_model.rb +9 -1
  62. data/lib/lutaml/xml/document.rb +3 -1
  63. data/lib/lutaml/xml/element.rb +13 -10
  64. data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
  65. data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
  66. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
  67. data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
  68. data/lib/lutaml/xml/xml_element.rb +24 -20
  69. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  70. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  71. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  72. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  73. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  74. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  75. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  76. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  77. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  78. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  79. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  80. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  81. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  82. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  83. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  84. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  85. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  86. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  87. data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
  88. data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
  89. data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
  90. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
  91. data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
  92. data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
  93. metadata +58 -3
  94. data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/turtle"
5
+
6
+ RSpec.describe Lutaml::Turtle::Transform do
7
+ before do
8
+ stub_const("TurtleTestModel", Class.new(Lutaml::Model::Serializable) do
9
+ attribute :name, :string
10
+ attribute :description, :string
11
+ attribute :code, :string
12
+
13
+ turtle do
14
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace,
15
+ Lutaml::Rdf::Namespaces::DctermsNamespace
16
+
17
+ subject { |m| "http://example.org/concept/#{m.code}" }
18
+
19
+ type "skos:Concept"
20
+
21
+ predicate :prefLabel,
22
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
23
+ to: :name,
24
+ lang_tagged: true
25
+
26
+ predicate :definition,
27
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
28
+ to: :description
29
+
30
+ predicate :notation,
31
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
32
+ to: :code
33
+ end
34
+ end)
35
+ end
36
+
37
+ let(:instance) do
38
+ TurtleTestModel.new(name: "test concept",
39
+ description: "A test description",
40
+ code: "2119")
41
+ end
42
+
43
+ describe "model_to_data" do
44
+ let(:result) { instance.to_turtle }
45
+
46
+ it "generates prefix declarations for used namespaces" do
47
+ expect(result).to include("@prefix skos: <http://www.w3.org/2004/02/skos/core#>")
48
+ end
49
+
50
+ it "generates subject URI" do
51
+ expect(result).to include("<http://example.org/concept/2119>")
52
+ end
53
+
54
+ it "generates rdf:type triple using compact prefix" do
55
+ expect(result).to include("a skos:Concept")
56
+ end
57
+
58
+ it "generates predicate triples" do
59
+ expect(result).to include("skos:notation \"2119\"")
60
+ expect(result).to include("skos:definition \"A test description\"")
61
+ end
62
+
63
+ it "terminates with a period" do
64
+ expect(result.strip).to end_with(".")
65
+ end
66
+ end
67
+
68
+ describe "special character escaping" do
69
+ let(:instance) do
70
+ TurtleTestModel.new(name: "has \"quotes\" and\nnewlines",
71
+ description: "back\\slash",
72
+ code: "1")
73
+ end
74
+
75
+ it "escapes double quotes in literals" do
76
+ result = instance.to_turtle
77
+ expect(result).to include('\\"quotes\\"')
78
+ end
79
+
80
+ it "handles multiline literals via triple-quoted strings" do
81
+ result = instance.to_turtle
82
+ expect(result).to include("has")
83
+ expect(result).to include("newlines")
84
+ end
85
+
86
+ it "escapes backslashes in literals" do
87
+ result = instance.to_turtle
88
+ expect(result).to include("back\\\\slash")
89
+ end
90
+ end
91
+
92
+ describe "numeric and boolean values" do
93
+ before do
94
+ stub_const("TypedModel", Class.new(Lutaml::Model::Serializable) do
95
+ attribute :label, :string
96
+ attribute :count, :integer
97
+ attribute :active, :boolean
98
+
99
+ turtle do
100
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
101
+
102
+ subject { |_| "http://example.org/1" }
103
+
104
+ predicate :prefLabel,
105
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
106
+ to: :label
107
+
108
+ predicate :notation,
109
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
110
+ to: :count
111
+
112
+ predicate :note,
113
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
114
+ to: :active
115
+ end
116
+ end)
117
+ end
118
+
119
+ it "serializes integers as native Turtle literals" do
120
+ instance = TypedModel.new(label: "test", count: 42, active: true)
121
+ result = instance.to_turtle
122
+ expect(result).to match(/skos:notation 42/)
123
+ end
124
+
125
+ it "serializes booleans as native Turtle literals" do
126
+ instance = TypedModel.new(label: "test", count: 1, active: true)
127
+ result = instance.to_turtle
128
+ expect(result).to match(/skos:note true/)
129
+ end
130
+ end
131
+
132
+ describe "model without subject" do
133
+ before do
134
+ stub_const("NoSubjectModel", Class.new(Lutaml::Model::Serializable) do
135
+ attribute :name, :string
136
+
137
+ turtle do
138
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
139
+ type "skos:Concept"
140
+ predicate :prefLabel,
141
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
142
+ to: :name
143
+ end
144
+ end)
145
+ end
146
+
147
+ it "raises MissingSubjectError" do
148
+ instance = NoSubjectModel.new(name: "test")
149
+ expect { instance.to_turtle }
150
+ .to raise_error(Lutaml::Turtle::MissingSubjectError)
151
+ end
152
+ end
153
+
154
+ describe "model with nil values" do
155
+ let(:instance) { TurtleTestModel.new(name: "test", code: "2119") }
156
+
157
+ it "omits predicates for nil attributes" do
158
+ result = instance.to_turtle
159
+ expect(result).not_to include("definition")
160
+ expect(result).to include("prefLabel")
161
+ end
162
+ end
163
+
164
+ describe "model with no predicates producing data" do
165
+ before do
166
+ stub_const("EmptyPredModel", Class.new(Lutaml::Model::Serializable) do
167
+ attribute :name, :string
168
+
169
+ turtle do
170
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
171
+
172
+ subject { |m| "http://example.org/#{m.name}" }
173
+
174
+ predicate :prefLabel,
175
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
176
+ to: :name
177
+ end
178
+ end)
179
+ end
180
+
181
+ let(:instance) { EmptyPredModel.new(name: nil) }
182
+
183
+ it "returns empty string when no data" do
184
+ expect(instance.to_turtle).to eq("")
185
+ end
186
+ end
187
+
188
+ describe "collection predicates" do
189
+ before do
190
+ stub_const("CollectionModel", Class.new(Lutaml::Model::Serializable) do
191
+ attribute :labels, :string, collection: true
192
+
193
+ turtle do
194
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
195
+
196
+ subject { |_| "http://example.org/1" }
197
+
198
+ predicate :prefLabel,
199
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
200
+ to: :labels
201
+ end
202
+ end)
203
+ end
204
+
205
+ it "generates triples for all collection values" do
206
+ instance = CollectionModel.new(labels: ["en", "fr"])
207
+ result = instance.to_turtle
208
+ expect(result).to include('"en"')
209
+ expect(result).to include('"fr"')
210
+ expect(result).to include("skos:prefLabel")
211
+ end
212
+ end
213
+
214
+ describe "full URI type (no prefix)" do
215
+ before do
216
+ stub_const("FullUriTypeModel", Class.new(Lutaml::Model::Serializable) do
217
+ attribute :name, :string
218
+
219
+ turtle do
220
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
221
+
222
+ subject { |m| "http://example.org/#{m.name}" }
223
+
224
+ type "http://example.org/MyType"
225
+
226
+ predicate :prefLabel,
227
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
228
+ to: :name
229
+ end
230
+ end)
231
+ end
232
+
233
+ it "uses full URI as-is when no colon prefix" do
234
+ instance = FullUriTypeModel.new(name: "test")
235
+ result = instance.to_turtle
236
+ expect(result).to include("<http://example.org/MyType>")
237
+ end
238
+ end
239
+
240
+ describe "data_to_model (deserialization)" do
241
+ let(:turtle_input) do
242
+ <<~TURTLE
243
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
244
+ @prefix dcterms: <http://purl.org/dc/terms/> .
245
+
246
+ <http://example.org/concept/2119> a skos:Concept ;
247
+ skos:prefLabel "test concept"@en ;
248
+ skos:definition "A test description" ;
249
+ skos:notation "2119" .
250
+ TURTLE
251
+ end
252
+
253
+ it "deserializes string attributes" do
254
+ model = TurtleTestModel.from_turtle(turtle_input)
255
+ expect(model.code).to eq("2119")
256
+ expect(model.description).to eq("A test description")
257
+ end
258
+
259
+ it "deserializes language-tagged values" do
260
+ model = TurtleTestModel.from_turtle(turtle_input)
261
+ expect(model.name).to eq("test concept")
262
+ end
263
+ end
264
+
265
+ describe "round-trip" do
266
+ it "preserves data through model → Turtle → model" do
267
+ turtle = instance.to_turtle
268
+ restored = TurtleTestModel.from_turtle(turtle)
269
+ expect(restored.code).to eq("2119")
270
+ expect(restored.description).to eq("A test description")
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model"
5
+ require "lutaml/xml"
6
+ require "lutaml/xml/adapter/nokogiri_adapter"
7
+ require "lutaml/xml/adapter/ox_adapter"
8
+ require "lutaml/xml/adapter/oga_adapter"
9
+ require "lutaml/xml/adapter/rexml_adapter"
10
+
11
+ # Regression tests for BaseAdapter refactoring.
12
+ # Each test guards a specific bug fix to prevent re-introduction.
13
+ # Run against all 4 adapters to ensure consistent behavior.
14
+ RSpec.shared_examples "base adapter regressions" do |adapter_class|
15
+ let(:adapter) { adapter_class }
16
+
17
+ describe "CDATA preservation in mixed content" do
18
+ let(:xml_with_cdata) do
19
+ "<root><![CDATA[Hello]]><b>world</b><![CDATA[!]]></root>"
20
+ end
21
+
22
+ it "preserves CDATA wrapping for mixed content string nodes" do
23
+ doc = adapter.parse(xml_with_cdata)
24
+ output = doc.to_xml
25
+
26
+ expect(output).to include("<![CDATA[Hello]]>")
27
+ expect(output).to include("<![CDATA[!]]>")
28
+ end
29
+ end
30
+
31
+ describe "XML comment preservation in serialization" do
32
+ let(:xml_with_comment) do
33
+ "<root><a/><!--middle comment--><b/></root>"
34
+ end
35
+
36
+ it "preserves comments during round-trip" do
37
+ doc = adapter.parse(xml_with_comment)
38
+ output = doc.to_xml
39
+
40
+ expect(output).to include("<!--middle comment-->")
41
+ end
42
+ end
43
+
44
+ describe "processing instructions do not affect text shape" do
45
+ let(:xml_with_pi_and_text) do
46
+ "<root>hello<?pi data?></root>"
47
+ end
48
+
49
+ let(:xml_with_only_pi) do
50
+ "<root><?pi data?></root>"
51
+ end
52
+
53
+ it "returns text as a string when PI is alongside text" do
54
+ doc = adapter.parse(xml_with_pi_and_text)
55
+
56
+ # text should be a string, not an array —
57
+ # PIs should not make plain text look like mixed content
58
+ expect(doc.text).to be_a(String)
59
+ expect(doc.text).to eq("hello")
60
+ end
61
+
62
+ it "returns empty string for text when element has only PI" do
63
+ doc = adapter.parse(xml_with_only_pi)
64
+
65
+ expect(doc.text).to be_a(String)
66
+ expect(doc.text).to eq("")
67
+ end
68
+ end
69
+
70
+ describe "processing instructions excluded from parse_element" do
71
+ let(:xml_with_pi) do
72
+ "<root><?foo ignored?><bar>content</bar></root>"
73
+ end
74
+
75
+ it "does not include PI in elements hash" do
76
+ doc = adapter.parse(xml_with_pi)
77
+ hash = doc.to_h
78
+
79
+ elements = hash["elements"]
80
+ expect(elements).to have_key("bar")
81
+ # PI should not appear as an element key
82
+ expect(elements.keys).not_to include("foo")
83
+ end
84
+ end
85
+
86
+ describe "ASCII-8BIT encoding round-trip" do
87
+ let(:binary_xml) { "<root>\xC2\xB5</root>".b }
88
+
89
+ it "round-trips binary-tagged UTF-8 content without encoding error" do
90
+ doc = adapter.parse(binary_xml)
91
+
92
+ expect { doc.to_xml }.not_to raise_error
93
+ expect(doc.to_xml).to include("\u00B5") # micro sign
94
+ end
95
+
96
+ it "does not use ASCII-8BIT as output encoding" do
97
+ doc = adapter.parse(binary_xml)
98
+ output = doc.to_xml
99
+
100
+ expect(output.encoding).not_to eq(Encoding::ASCII_8BIT)
101
+ end
102
+ end
103
+
104
+ describe "namespaced_attr_name helper arity" do
105
+ it "resolves single-arg namespaced_attr_name from AdapterHelpers" do
106
+ # This verifies the AdapterHelpers version (single Moxml attribute arg)
107
+ # is accessible and not shadowed by a 2-arg def self. method
108
+ expect(adapter).to respond_to(:namespaced_attr_name)
109
+
110
+ # The method should accept 1 argument (a Moxml attribute object)
111
+ method = adapter.method(:namespaced_attr_name)
112
+ expect(method.arity).to eq(1)
113
+ end
114
+ end
115
+
116
+ describe "XmlParser helper methods are private" do
117
+ it "does not expose normalize_xml_for_parse as a public class method" do
118
+ expect(adapter.public_methods).not_to include(:normalize_xml_for_parse)
119
+ end
120
+
121
+ it "does not expose parse_with_moxml as a public class method" do
122
+ expect(adapter.public_methods).not_to include(:parse_with_moxml)
123
+ end
124
+
125
+ it "does not expose raise_empty_document_error as a public class method" do
126
+ expect(adapter.public_methods).not_to include(:raise_empty_document_error)
127
+ end
128
+
129
+ it "exposes parse as a public class method" do
130
+ expect(adapter.public_methods).to include(:parse)
131
+ end
132
+ end
133
+ end
134
+
135
+ RSpec.describe Lutaml::Xml::Adapter::BaseAdapter do
136
+ context "with NokogiriAdapter" do
137
+ it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::NokogiriAdapter
138
+ end
139
+
140
+ context "with OxAdapter" do
141
+ it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::OxAdapter
142
+ end
143
+
144
+ context "with OgaAdapter" do
145
+ it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::OgaAdapter
146
+ end
147
+
148
+ context "with RexmlAdapter" do
149
+ it_behaves_like "base adapter regressions", Lutaml::Xml::Adapter::RexmlAdapter
150
+ end
151
+ end
@@ -0,0 +1,150 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+ require "lutaml/xml"
4
+ require "lutaml/xml/adapter/nokogiri_adapter"
5
+ require "lutaml/xml/adapter/ox_adapter"
6
+ require "lutaml/xml/adapter/oga_adapter"
7
+ require "lutaml/xml/adapter/rexml_adapter"
8
+
9
+ module XmlAdapterSharedFeaturesSpec
10
+ class ProcessingInstructionLookupElement < Lutaml::Model::Serializable
11
+ attribute :foo, :string
12
+
13
+ xml do
14
+ root "root"
15
+ map_element "foo", to: :foo
16
+ end
17
+ end
18
+ end
19
+
20
+ RSpec.describe "XML adapter order metadata" do
21
+ shared_examples "consistent order metadata" do |adapter_class|
22
+ let(:xml) do
23
+ "<root>before<![CDATA[cdata text]]><?pi data?><child/>after</root>"
24
+ end
25
+
26
+ let(:document) { adapter_class.parse(xml) }
27
+
28
+ let(:expected_order) do
29
+ [
30
+ ["Text", "text", :text, "before"],
31
+ ["Text", "#cdata-section", :cdata, "cdata text"],
32
+ ["ProcessingInstruction", "pi", :processing_instruction, "data"],
33
+ ["Element", "child", :element, nil],
34
+ ["Text", "text", :text, "after"],
35
+ ]
36
+ end
37
+
38
+ def order_data(order)
39
+ order.map do |item|
40
+ [item.type, item.name, item.node_type, item.text_content]
41
+ end
42
+ end
43
+
44
+ it "uses the same order representation for root, document, and class order" do
45
+ expect(order_data(document.root.order)).to eq(expected_order)
46
+ expect(order_data(document.order)).to eq(expected_order)
47
+ expect(order_data(adapter_class.order_of(document.root))).to eq(expected_order)
48
+ end
49
+
50
+ it "parses processing instructions as first-class XML nodes" do
51
+ instruction = document.root.children.find(&:processing_instruction?)
52
+
53
+ expect(instruction).not_to be_nil
54
+ expect(instruction.name).to eq("pi")
55
+ expect(instruction.text).to eq("data")
56
+ expect(instruction.node_type).to eq(:processing_instruction)
57
+ end
58
+
59
+ it "preserves processing instructions and CDATA when serializing parsed XML" do
60
+ output = document.to_xml
61
+
62
+ expect(output).to include("<?pi data?>")
63
+ expect(output).to include("<![CDATA[cdata text]]>")
64
+ end
65
+
66
+ it "uses the same order representation through to_h item_order" do
67
+ parsed_hash = nil
68
+
69
+ expect { parsed_hash = document.to_h }.not_to raise_error
70
+ expect(order_data(parsed_hash.item_order)).to eq(expected_order)
71
+ end
72
+ end
73
+
74
+ shared_examples "consistent shared adapter features" do |adapter_class|
75
+ around do |example|
76
+ old_adapter = Lutaml::Model::Config.xml_adapter
77
+ Lutaml::Model::Config.xml_adapter = adapter_class
78
+ example.run
79
+ ensure
80
+ Lutaml::Model::Config.xml_adapter = old_adapter
81
+ end
82
+
83
+ it "uses shared parse metadata preservation" do
84
+ xml = <<~XML
85
+ <?xml version="1.0"?>
86
+ <!DOCTYPE root SYSTEM "root.dtd">
87
+ <root><child/></root>
88
+ XML
89
+
90
+ document = adapter_class.parse(xml)
91
+
92
+ expect(document.xml_declaration[:had_declaration]).to be true
93
+ expect(document.doctype).to include(name: "root",
94
+ public_id: nil,
95
+ system_id: "root.dtd")
96
+ expect(document.to_xml(declaration: true)).to include(
97
+ '<!DOCTYPE root SYSTEM "root.dtd">',
98
+ )
99
+ end
100
+
101
+ it "parses binary UTF-8 XML input through shared normalization" do
102
+ document = adapter_class.parse("<root><name>μ</name></root>".b)
103
+
104
+ expect(document.root.name).to eq("root")
105
+ expect(document.root.element_children.first.text).to eq("μ")
106
+ end
107
+
108
+ it "transcodes non-UTF-8 strings from their declared encoding" do
109
+ xml = "<root><name>\xC2\xA3</name></root>".b
110
+ xml.force_encoding("ISO-8859-1")
111
+ document = adapter_class.parse(xml)
112
+
113
+ expect(document.root.element_children.first.text).to eq("£")
114
+ end
115
+
116
+ it "preserves processing instructions without matching them as elements" do
117
+ xml = "<root><?foo ignored?><foo>bar</foo></root>"
118
+ document = adapter_class.parse(xml)
119
+
120
+ expect(document.root.children.any?(&:processing_instruction?)).to be true
121
+ expect(document.root.element_children.map(&:text)).to eq(["bar"])
122
+ expect(document.root.find_children_by_name("foo").map(&:text)).to eq(["bar"])
123
+ expect(
124
+ XmlAdapterSharedFeaturesSpec::ProcessingInstructionLookupElement
125
+ .from_xml(xml)
126
+ .foo,
127
+ ).to eq("bar")
128
+ end
129
+ end
130
+
131
+ describe Lutaml::Xml::Adapter::NokogiriAdapter do
132
+ it_behaves_like "consistent order metadata", described_class
133
+ it_behaves_like "consistent shared adapter features", described_class
134
+ end
135
+
136
+ describe Lutaml::Xml::Adapter::OxAdapter do
137
+ it_behaves_like "consistent order metadata", described_class
138
+ it_behaves_like "consistent shared adapter features", described_class
139
+ end
140
+
141
+ describe Lutaml::Xml::Adapter::OgaAdapter do
142
+ it_behaves_like "consistent order metadata", described_class
143
+ it_behaves_like "consistent shared adapter features", described_class
144
+ end
145
+
146
+ describe Lutaml::Xml::Adapter::RexmlAdapter do
147
+ it_behaves_like "consistent order metadata", described_class
148
+ it_behaves_like "consistent shared adapter features", described_class
149
+ end
150
+ end