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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +5 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +91 -22
- data/Gemfile +2 -0
- data/README.adoc +114 -2
- data/docs/_guides/index.adoc +18 -0
- data/docs/_guides/jsonld-serialization.adoc +217 -0
- data/docs/_guides/rdf-serialization.adoc +344 -0
- data/docs/_guides/turtle-serialization.adoc +224 -0
- data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
- data/docs/_pages/serialization_adapters.adoc +31 -0
- data/docs/_references/index.adoc +1 -0
- data/docs/_references/rdf-namespaces.adoc +243 -0
- data/docs/index.adoc +3 -2
- data/lib/lutaml/jsonld/adapter.rb +23 -0
- data/lib/lutaml/jsonld/context.rb +69 -0
- data/lib/lutaml/jsonld/term_definition.rb +39 -0
- data/lib/lutaml/jsonld/transform.rb +174 -0
- data/lib/lutaml/jsonld.rb +23 -0
- data/lib/lutaml/model/format_registry.rb +10 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +6 -0
- data/lib/lutaml/rdf/error.rb +7 -0
- data/lib/lutaml/rdf/iri.rb +44 -0
- data/lib/lutaml/rdf/language_tagged.rb +11 -0
- data/lib/lutaml/rdf/literal.rb +62 -0
- data/lib/lutaml/rdf/mapping.rb +71 -0
- data/lib/lutaml/rdf/mapping_rule.rb +35 -0
- data/lib/lutaml/rdf/member_rule.rb +13 -0
- data/lib/lutaml/rdf/namespace.rb +58 -0
- data/lib/lutaml/rdf/namespace_set.rb +69 -0
- data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
- data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces.rb +14 -0
- data/lib/lutaml/rdf/transform.rb +36 -0
- data/lib/lutaml/rdf.rb +19 -0
- data/lib/lutaml/turtle/adapter.rb +35 -0
- data/lib/lutaml/turtle/mapping.rb +7 -0
- data/lib/lutaml/turtle/transform.rb +158 -0
- data/lib/lutaml/turtle.rb +22 -0
- data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
- data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
- data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
- data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
- data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
- data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
- data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
- data/lib/lutaml/xml/adapter.rb +0 -1
- data/lib/lutaml/xml/adapter_element.rb +7 -1
- data/lib/lutaml/xml/builder/base.rb +0 -1
- data/lib/lutaml/xml/data_model.rb +9 -1
- data/lib/lutaml/xml/document.rb +3 -1
- data/lib/lutaml/xml/element.rb +13 -10
- data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
- data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
- data/lib/lutaml/xml/xml_element.rb +24 -20
- data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
- data/spec/lutaml/integration/multi_format_spec.rb +106 -0
- data/spec/lutaml/integration/round_trip_spec.rb +170 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
- data/spec/lutaml/jsonld/context_spec.rb +114 -0
- data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
- data/spec/lutaml/jsonld/transform_spec.rb +211 -0
- data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
- data/spec/lutaml/rdf/iri_spec.rb +73 -0
- data/spec/lutaml/rdf/literal_spec.rb +98 -0
- data/spec/lutaml/rdf/mapping_spec.rb +164 -0
- data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
- data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
- data/spec/lutaml/rdf/namespace_spec.rb +241 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
- data/spec/lutaml/turtle/adapter_spec.rb +47 -0
- data/spec/lutaml/turtle/mapping_spec.rb +123 -0
- data/spec/lutaml/turtle/transform_spec.rb +273 -0
- data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
- data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
- data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
- data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
- metadata +58 -3
- 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
|